• There is NO official Otland's Discord server and NO official Otland's server list. The Otland's Staff does not manage any Discord server or server list. Moderators or administrator of any Discord server or server lists have NO connection to the Otland's Staff. Do not get scammed!

[TFS 1.5] ⚡ Bounce Spell ⚡

Sarah Wesker

ƐƖєgαηт Sуηтαx ❤
Staff member
TFS Developer
Support Team
Joined
Mar 16, 2017
Messages
1,422
Solutions
155
Reaction score
1,986
Location
London
GitHub
MillhioreBT
Twitch
millhiorebt
Some time ago I published a rebound spell, but it had many details that could be improved, and of course I heard the complaints and decided to recreate it from scratch, trying to make it better and more precise and realistic, so here you have the rebound spell rewritten.

Lua:
--[[
    ⚡ Bounce Spell ⚡
    Author: 𝓜𝓲𝓵𝓵𝓱𝓲𝓸𝓻𝓮 𝓑𝓣 - @millhiorebt
    Version: 1.0
    Engine: TFS 1.5+
]] --
local config = {
    name = "Bounce Spell",
    words = "exevo pum",
    level = 20,
    magicLevel = 5,
    range = 4,
    cooldown = 1000,
    manaCost = 100,

    maxTargets = 5,
    specRange = 4,

    combatType = COMBAT_ENERGYDAMAGE,
    combatEffect = CONST_ME_ENERGYHIT
}

local setmetatable = setmetatable
local contains = table.contains
local insert = table.insert
local sort = table.sort

---@class BounceSpell
---@field playerId integer
---@field targets integer[]
---@field currentTargetId integer
---@field lastTargetPos Position
local BounceSpell = {}

---@param player Player
---@param target Creature
---@return BounceSpell BounceSpell
local function getBounceSpell(player, target)
    local bounceSpell = setmetatable({}, {__index = BounceSpell})
    bounceSpell.playerId = player:getId()
    local targetId = target:getId()
    bounceSpell.targets = {targetId}
    bounceSpell.currentTargetId = targetId
    bounceSpell.lastTargetPos = player:getPosition()
    return bounceSpell
end

---@param target Creature|Player
---@return boolean
function BounceSpell:checkTarget(target)
    local player = Player(self.playerId)
    if not player then return false end
    local targetPlayer = target:getPlayer()
    return not targetPlayer or
               (not player:hasSecureMode() and not targetPlayer:getGroup():getAccess() and
                   not self:isFriend(targetPlayer))
end

do
    local getSpectators = Game.getSpectators
    local x, y = config.specRange, config.specRange

    ---@param pos Position
    ---@return Creature[]
    function BounceSpell:getSpectators(pos)
        local spectators = getSpectators(pos, false, false, x, x, y, y)
        local spects = {}
        for _, spectator in ipairs(spectators) do
            local spectatorId = spectator:getId()
            if spectatorId ~= self.playerId and
                not Tile(spectator:getPosition()):hasFlag(TILESTATE_PROTECTIONZONE) and
                not contains(self.targets, spectatorId) and self:checkTarget(spectator) then
                insert(spects, spectator)
            end
        end
        return spects
    end
end

---@param target Player
---@return boolean
function BounceSpell:isFriend(target)
    local player = Player(self.playerId)
    if not player then return false end
    local party = player:getParty()
    if not party then return false end

    local targetId = target:getId()
    if targetId == party:getLeader():getId() then return true end
    for _, member in ipairs(party:getMembers()) do
        if targetId == member:getId() then return true end
    end
    return false
end

---@param from Position
---@param to Position
---@return integer
local function getDistanceTo(from, to) return from:getDistance(to) end

---@param pos Position
---@return boolean
function BounceSpell:getTargets(pos)
    local spects = self:getSpectators(pos)
    if #spects > 1 then
        sort(spects, function(a, b)
            return getDistanceTo(pos, a:getPosition()) <
                       getDistanceTo(pos, b:getPosition())
        end)
    end

    local target = nil
    for _, spectator in ipairs(spects) do
        if pos:isSightClear(spectator:getPosition(), true) then
            target = spectator
            break
        end
    end

    if not target then return false end

    local targetId = target:getId()
    insert(self.targets, targetId)
    self.currentTargetId = targetId
    self.lastTargetPos = pos
    return true
end

local combats = {}

for divisor = 1, config.maxTargets do
    local combat = Combat()
    combat:setParameter(COMBAT_PARAM_TYPE, config.combatType)
    combat:setParameter(COMBAT_PARAM_EFFECT, config.combatEffect)

    ---@param player Player
    function onGetFormulaValues(player, level, magicLevel)
        local min = (level * 4.3) + (magicLevel * 190) + 52
        local max = (level * 4.6) + (magicLevel * 280) + 79
        return -min / divisor, -max / divisor
    end

    combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

    combats[#combats + 1] = combat
end

---@return boolean
function BounceSpell:execute()
    local player = Player(self.playerId)
    if not player then return false end

    local target = Creature(self.currentTargetId)
    if not target then return false end

    local combat = combats[#self.targets] or combats[#combats]
    if not combat then error("Bounce Spell: Invalid combat.") end

    local targetPos = target:getPosition()
    self.lastTargetPos:sendDistanceEffect(targetPos, CONST_ANI_ENERGY)
    combat:execute(player, Variant(targetPos))

    if #self.targets >= config.maxTargets then return false end

    self.currentTargetId = nil
    if self:getTargets(target:getPosition()) then
        addEvent(self.execute, 100 + (#self.targets * 10), self)
    end
    return true
end

local spell = Spell(SPELL_INSTANT)

function spell.onCastSpell(player, variant, isHotkey)
    local target = player:getTarget()
    if not target then error("Bounce Spell: Invalid target.") end

    return getBounceSpell(player, target):execute()
end

spell:name(config.name)
spell:words(config.words)
spell:group("attack")
spell:vocation("sorcerer", "master sorcerer")
spell:id(1)
spell:cooldown(config.cooldown)
spell:level(config.level)
spell:mana(config.manaCost)
spell:magicLevel(config.magicLevel)
spell:isPremium(true)
spell:blockWalls(true)
spell:isAggressive(true)
spell:isBlocking(true)
spell:needTarget(true)
spell:range(config.range)
spell:register()


Here is the healing version for party members:
Lua:
--[[
    Bounce Heal
    Author: 𝓜𝓲𝓵𝓵𝓱𝓲𝓸𝓻𝓮 𝓑𝓣 - @millhiorebt
    Version: 1.0
    Engine: TFS
]] --
local config = {
    name = "Bounce Heal",
    words = "exura pum",
    level = 20,
    magicLevel = 5,
    range = 4,
    cooldown = 1000,
    manaCost = 100,

    maxTargets = 5,
    specRange = 4,

    combatType = COMBAT_HEALING,
    combatEffect = CONST_ME_MAGIC_BLUE,
    defaultMultiplier = 1.0,
    decreaseMultiplier = 0.1
}

local setmetatable = setmetatable
local contains = table.contains
local insert = table.insert
local sort = table.sort

---@class BounceHeal
---@field playerId integer
---@field targets integer[]
---@field currentTargetId integer
---@field lastTargetPos Position
local BounceHeal = {}

---@param player Player
---@return BounceHeal BounceHeal
local function getBounceHeal(player)
    local bounceSpell = setmetatable({}, {__index = BounceHeal})
    bounceSpell.playerId = player:getId()
    bounceSpell.targets = {}
    bounceSpell.currentTargetId = 0
    bounceSpell.lastTargetPos = player:getPosition()
    return bounceSpell
end

---@param target Creature|Player
---@return boolean
function BounceHeal:checkTarget(target)
    local player = Player(self.playerId)
    if not player then return false end
    local targetPlayer = target:getPlayer()
    return targetPlayer and self:isFriend(targetPlayer) or false
end

do
    local getSpectators = Game.getSpectators
    local x, y = config.specRange, config.specRange

    ---@param pos Position
    ---@return Player[]
    function BounceHeal:getSpectators(pos)
        local spectators = getSpectators(pos, false, true, x, x, y, y)
        local spects = {}
        for _, spectator in ipairs(spectators) do
            local spectatorId = spectator:getId()
            if spectatorId ~= self.playerId and not contains(self.targets, spectatorId) and self:checkTarget(spectator) then
                insert(spects, spectator)
            end
        end
        return spects
    end
end

---@param target Player
---@return boolean
function BounceHeal:isFriend(target)
    local player = Player(self.playerId)
    if not player then return false end

    local party = player:getParty()
    if not party then return false end

    local targetId = target:getId()
    if targetId == party:getLeader():getId() then return true end
    for _, member in ipairs(party:getMembers()) do if targetId == member:getId() then return true end end
    return false
end

---@param from Position
---@param to Position
---@return integer
local function getDistanceTo(from, to) return from:getDistance(to) end

---@param pos Position
---@return boolean
function BounceHeal:getTargets(pos)
    local spects = self:getSpectators(pos)
    if #spects > 1 then
        sort(spects, function(a, b) return getDistanceTo(pos, a:getPosition()) < getDistanceTo(pos, b:getPosition()) end)
    end

    local target = nil
    for _, spectator in ipairs(spects) do
        if pos:isSightClear(spectator:getPosition(), true) then
            target = spectator
            break
        end
    end

    if not target then return false end
    local targetId = target:getId()
    insert(self.targets, targetId)
    self.currentTargetId = targetId
    self.lastTargetPos = pos
    return true
end

local combats = {}
for divisor = 1, config.maxTargets do
    local combat = Combat()
    combat:setParameter(COMBAT_PARAM_TYPE, config.combatType)
    combat:setParameter(COMBAT_PARAM_EFFECT, config.combatEffect)
    combat:setParameter(COMBAT_PARAM_DISPEL, CONDITION_PARALYZE)
    combat:setParameter(COMBAT_PARAM_AGGRESSIVE, 0)

    ---@param player Player
    function onGetFormulaValues(player, level, magicLevel)
        local min = (level * 4.3) + (magicLevel * 190) + 52
        local max = (level * 4.6) + (magicLevel * 280) + 79
        return min / divisor, max / divisor
    end

    combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")
    combats[#combats + 1] = combat
end

---@return boolean
function BounceHeal:execute()
    local player = Player(self.playerId)
    if not player then return false end

    local target = Creature(self.currentTargetId)
    if not target then return false end

    local combat = combats[#self.targets] or combats[#combats]
    if not combat then error("Bounce Heal: Invalid combat.") end

    local targetPos = target:getPosition()
    self.lastTargetPos:sendDistanceEffect(targetPos, CONST_ANI_ENERGY)
    combat:execute(player, Variant(targetPos))

    if #self.targets >= config.maxTargets then return false end
    self.currentTargetId = nil
    if self:getTargets(target:getPosition()) then
        addEvent(self.execute, 100 + (#self.targets * 10), self)
    end
    return true
end

local spell = Spell(SPELL_INSTANT)

function spell.onCastSpell(player, variant, isHotkey)
    local bounceHeal = getBounceHeal(player)
    if bounceHeal:getTargets(player:getPosition()) then
        return bounceHeal:execute()
    end

    player:sendCancelMessage("There is no one to heal.")
    return false
end

spell:name(config.name)
spell:words(config.words)
spell:group("support")
spell:vocation("druid", "elder druid")
spell:id(1)
spell:cooldown(config.cooldown)
spell:level(config.level)
spell:mana(config.manaCost)
spell:magicLevel(config.magicLevel)
spell:isPremium(true)
spell:blockWalls(true)
spell:isAggressive(false)
spell:isBlocking(true)
spell:register()
 
Last edited:
Thank you. Now I can use this without worrying about anything.
 
Some time ago I published a rebound spell, but it had many details that could be improved, and of course I heard the complaints and decided to recreate it from scratch, trying to make it better and more precise and realistic, so here you have the rebound spell rewritten.

Lua:
--[[
    ⚡ Bounce Spell ⚡
    Author: 𝓜𝓲𝓵𝓵𝓱𝓲𝓸𝓻𝓮 𝓑𝓣 - @millhiorebt
    Version: 1.0
    Engine: TFS 1.5+
]] --
local config = {
    name = "Bounce Spell",
    words = "exevo pum",
    level = 20,
    magicLevel = 5,
    range = 4,
    cooldown = 1000,
    manaCost = 100,

    maxTargets = 5,
    specRange = 4,

    combatType = COMBAT_ENERGYDAMAGE,
    combatEffect = CONST_ME_ENERGYHIT
}

local setmetatable = setmetatable
local contains = table.contains
local insert = table.insert
local sort = table.sort

---@class BounceSpell
---@field playerId integer
---@field targets integer[]
---@field currentTargetId integer
---@field lastTargetPos Position
local BounceSpell = {}

---@param player Player
---@param target Creature
---@return BounceSpell BounceSpell
local function getBounceSpell(player, target)
    local bounceSpell = setmetatable({}, {__index = BounceSpell})
    bounceSpell.playerId = player:getId()
    local targetId = target:getId()
    bounceSpell.targets = {targetId}
    bounceSpell.currentTargetId = targetId
    bounceSpell.lastTargetPos = player:getPosition()
    return bounceSpell
end

---@param target Creature|Player
---@return boolean
function BounceSpell:checkTarget(target)
    local player = Player(self.playerId)
    if not player then return false end
    local targetPlayer = target:getPlayer()
    return not targetPlayer or
               (not player:hasSecureMode() and not targetPlayer:getGroup():getAccess() and
                   not self:isFriend(targetPlayer))
end

do
    local getSpectators = Game.getSpectators
    local x, y = config.specRange, config.specRange

    ---@param pos Position
    ---@return Creature[]
    function BounceSpell:getSpectators(pos)
        local spectators = getSpectators(pos, false, false, x, x, y, y)
        local spects = {}
        for _, spectator in ipairs(spectators) do
            local spectatorId = spectator:getId()
            if spectatorId ~= self.playerId and
                not Tile(spectator:getPosition()):hasFlag(TILESTATE_PROTECTIONZONE) and
                not contains(self.targets, spectatorId) and self:checkTarget(spectator) then
                insert(spects, spectator)
            end
        end
        return spects
    end
end

---@param target Player
---@return boolean
function BounceSpell:isFriend(target)
    local player = Player(self.playerId)
    if not player then return false end
    local party = player:getParty()
    if not party then return false end

    local targetId = target:getId()
    if targetId == party:getLeader():getId() then return true end
    for _, member in ipairs(party:getMembers()) do
        if targetId == member:getId() then return true end
    end
    return false
end

---@param from Position
---@param to Position
---@return integer
local function getDistanceTo(from, to) return from:getDistance(to) end

---@param pos Position
---@return boolean
function BounceSpell:getTargets(pos)
    local spects = self:getSpectators(pos)
    if #spects > 1 then
        sort(spects, function(a, b)
            return getDistanceTo(pos, a:getPosition()) <
                       getDistanceTo(pos, b:getPosition())
        end)
    end

    local target = nil
    for _, spectator in ipairs(spects) do
        if pos:isSightClear(spectator:getPosition(), true) then
            target = spectator
            break
        end
    end

    if not target then return false end

    local targetId = target:getId()
    insert(self.targets, targetId)
    self.currentTargetId = targetId
    self.lastTargetPos = pos
    return true
end

local combats = {}

for divisor = 1, config.maxTargets do
    local combat = Combat()
    combat:setParameter(COMBAT_PARAM_TYPE, config.combatType)
    combat:setParameter(COMBAT_PARAM_EFFECT, config.combatEffect)

    ---@param player Player
    function onGetFormulaValues(player, level, magicLevel)
        local min = (level * 4.3) + (magicLevel * 190) + 52
        local max = (level * 4.6) + (magicLevel * 280) + 79
        return -min / divisor, -max / divisor
    end

    combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

    combats[#combats + 1] = combat
end

---@return boolean
function BounceSpell:execute()
    local player = Player(self.playerId)
    if not player then return false end

    local target = Creature(self.currentTargetId)
    if not target then return false end

    local combat = combats[#self.targets] or combats[#combats]
    if not combat then error("Bounce Spell: Invalid combat.") end

    local targetPos = target:getPosition()
    self.lastTargetPos:sendDistanceEffect(targetPos, CONST_ANI_ENERGY)
    combat:execute(player, Variant(targetPos))

    if #self.targets >= config.maxTargets then return false end

    self.currentTargetId = nil
    if self:getTargets(target:getPosition()) then
        addEvent(self.execute, 100 + (#self.targets * 10), self)
    end
    return true
end

local spell = Spell(SPELL_INSTANT)

function spell.onCastSpell(player, variant, isHotkey)
    local target = player:getTarget()
    if not target then error("Bounce Spell: Invalid target.") end

    return getBounceSpell(player, target):execute()
end

spell:name(config.name)
spell:words(config.words)
spell:group("attack")
spell:vocation("sorcerer", "master sorcerer")
spell:id(1)
spell:cooldown(config.cooldown)
spell:level(config.level)
spell:mana(config.manaCost)
spell:magicLevel(config.magicLevel)
spell:isPremium(true)
spell:blockWalls(true)
spell:isAggressive(true)
spell:isBlocking(true)
spell:needTarget(true)
spell:range(config.range)
spell:register()

it's not only a spell, it's a coding class ^^ thanks
 
Thank you very much Sarah for your contribution. With a little tweaks implemented a summon which heals master and nearby friends! <3
buffer.gif

@Unknown Soldier In my environment the isFriend function differs too much than in regular OTS but I modified it a little to make sense so should be good for you too. However you might want to modify this function as well because I didn't tested in every scenario for regular TFS.
Lua:
--[[
    Bounce Heal
    Author: 𝓜𝓲𝓵𝓵𝓱𝓲𝓸𝓻𝓮 𝓑𝓣 - @millhiorebt
    Version: 1.0
    Engine: TFS
]] --
local config = {
    name = "Buffer Healing",
    words = "Buffer Healing",
    level = 20,
    magicLevel = 5,
    range = 4,
    cooldown = 1000,
    manaCost = 100000,
   
    distanceEffect = 8,
    effect = CONST_ME_MAGIC_BLUE,
    minHeal = 200,
    maxHeal = 400,
    maxTargets = 5,
    specRange = 4
}

local setmetatable = setmetatable
local contains = table.contains
local insert = table.insert
local sort = table.sort

---@class BounceHeal
---@field creatureId integer
---@field targets integer[]
---@field currentTargetId integer
---@field lastTargetPos Position
local BounceHeal = {}

---@param creature creature
---@return BounceHeal BounceHeal
local function getBounceHeal(creature)
    local bounceSpell = setmetatable({}, {__index = BounceHeal})
    bounceSpell.creatureId = creature:getId()
    bounceSpell.targets = {}
    bounceSpell.currentTargetId = 0
    bounceSpell.lastTargetPos = creature:getPosition()
    return bounceSpell
end

---@param target Creature|Player
---@return boolean
function BounceHeal:checkTarget(target)
    local creature = Creature(self.creatureId)
    if not creature then return false end
    return self:isFriend(target)
end

do
    local getSpectators = Game.getSpectators
    local x, y = config.specRange, config.specRange

    ---@param pos Position
    ---@return Player[]
    function BounceHeal:getSpectators(pos)
        local spectators = getSpectators(pos, false, false, x, x, y, y)
        local spects = {}
        for _, spectator in ipairs(spectators) do
            local spectatorId = spectator:getId()
            if spectatorId ~= self.creatureId and not contains(self.targets, spectatorId) and self:checkTarget(spectator) then
                insert(spects, spectator)
            end
        end
        return spects
    end
end

---@param target Creature
---@return boolean
function BounceHeal:isFriend(target)
    local creature = Creature(self.creatureId)
    if not creature then return false end
   
    if target == creature:getMaster() then
        return true
    end
   
    if target:getMaster() == creature:getMaster() then
        return true
    end
   
    return false
end

---@param from Position
---@param to Position
---@return integer
local function getDistanceTo(from, to) return from:getDistance(to) end

---@param pos Position
---@return boolean
function BounceHeal:getTargets(pos)
    local spects = self:getSpectators(pos)
    if #spects > 1 then
        sort(spects, function(a, b) return getDistanceTo(pos, a:getPosition()) < getDistanceTo(pos, b:getPosition()) end)
    end

    local target = nil
    for _, spectator in ipairs(spects) do
        if pos:isSightClear(spectator:getPosition(), true) then
            target = spectator
            break
        end
    end

    if not target then return false end
    local targetId = target:getId()
    insert(self.targets, targetId)
    self.currentTargetId = targetId
    self.lastTargetPos = pos
    return true
end

---@return boolean
function BounceHeal:execute()
    local creature = Creature(self.creatureId)
    if not creature then return false end

    local target = Creature(self.currentTargetId)
    if not target then return false end

    local targetPos = target:getPosition()
    self.lastTargetPos:sendDistanceEffect(targetPos, config.distanceEffect)
    target:getPosition():sendMagicEffect(config.effect)
    doTargetCombat(creature, target, COMBAT_HEALING, config.minHeal, config.maxHeal)
   
    if #self.targets >= config.maxTargets then return false end
    self.currentTargetId = nil
    if self:getTargets(target:getPosition()) then
        addEvent(self.execute, 100 + (#self.targets * 10), self)
    end
    return true
end

local spell = Spell(SPELL_INSTANT)

function spell.onCastSpell(creature, variant, isHotkey)
    local bounceHeal = getBounceHeal(creature)
    if bounceHeal:getTargets(creature:getPosition()) then
        return bounceHeal:execute()
    end

    return false
end

spell:name(config.name)
spell:words(config.words)
spell:group("support")
spell:vocation("druid")
spell:needLearn(true)
spell:id(1)
spell:cooldown(config.cooldown)
spell:level(config.level)
spell:mana(config.manaCost)
spell:magicLevel(config.magicLevel)
spell:isPremium(true)
spell:blockWalls(true)
spell:isAggressive(false)
spell:isBlocking(true)
spell:register()
 
Last edited:
If anyone want to set a condition too like ignite or something, you can add this :

Lua:
    if not target:hasCondition(CONDITION_ENERGY) then
        local sparkConfig = {
            combatType = COMBAT_ENERGYDAMAGE,
        }
        local sparkCombat = Combat()
        sparkCombat:setParameter(COMBAT_PARAM_TYPE, sparkConfig.combatType)
        local sparkCondition = Condition(CONDITION_ENERGY)
        sparkCondition:setParameter(CONDITION_PARAM_DELAYED, 1)
        sparkCondition:addDamage(10, 3000, -45)
        sparkCombat:addCondition(sparkCondition)
        sparkCombat:execute(player, Variant(targetPos))
    end

after this line
Lua:
    if self:getTargets(target:getPosition()) then
        addEvent(self.execute, 100 + (#self.targets * 10), self)
    end

You can do something similar with the Healing version too.


1698572554069.png
 
Back
Top