• 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!

Spell [TFS 1.X] Animated Spells (Dynamic vs Static)

Leo32

Getting back into it...
Joined
Sep 21, 2007
Messages
987
Solutions
14
Reaction score
532
I know spell animations are nothing new, but the way they've been done in the past calls multiple combat objects.
This can be cool in how each square / animation has its own damage and the damage is applied with the animation, but this can introduce issues where; if the creature moves to a non-damaging square between the animation delays, then they won't receive damage at all.

Here is an example of an animated spell using this dynamic, multiple combat object method:

fooKMM5.gif


fire nova (animated with dynamic damage)
Lua:
-- Delay between animations.
local animationDelay = 200
local combat = {}

-- Frames (1 = Area, 2 = Player, 3 = Player + Self Damaging)
local area = {
    {
        {0, 1, 0},
        {1, 2, 1},
        {0, 1, 0}
    },
    {
        {1, 0, 1},
        {0, 2, 0},
        {1, 0, 1}
    },
    {
        {0, 1, 1, 1, 0},
        {1, 0, 0, 0, 1},
        {1, 0, 2, 0, 1},
        {1, 0, 0, 0, 1},
        {0, 1, 1, 1, 0}
    }
}

for i = 1, #area do
    combat[i] = Combat()
    combat[i]:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)
    combat[i]:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_FIREAREA)
end

for x, _ in ipairs(area) do
    combat[x]:setArea(createCombatArea(area[x]))
end

function executeCombat(p, i)
    if not p.player then
        return false
    end
    if not p.player:isPlayer() then
            return false
    end
    p.combat[i]:execute(p.player, p.var)
end

function onCastSpell(player, var)

    local p = {player = player, var = var, combat = combat}

    -- Damage formula
    local level = player:getLevel()
    local maglevel = player:getMagicLevel()
    local min = (level / 5) + (maglevel * 1.4) + 8
    local max = (level / 5) + (maglevel * 2.2) + 14

    for i = 1, #area do
        combat[i]:setFormula(COMBAT_FORMULA_LEVELMAGIC, 0, -min, 0, -max)
        if i == 1 then
            combat[i]:execute(player, var)
        else
            addEvent(executeCombat, (animationDelay * i) - animationDelay, p, i)
        end
    end

    return true
end

We can achieve the same animation effect, but have the damage roll behave normally.
This guarantees that nothing misses.

The cons of this setup are:
  • The damage won't apply in a delayed fashion, synchronized with the animation - it applies immediately.
  • Each animation frame doesn't have separate combat rolls, so all squares will do the same damage (like vanilla tibia spells)
fire nova.lua (animated, but static damage)
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)

-- Set area
local arr = {
    {0, 1, 1, 1, 0},
    {1, 1, 1, 1, 1},
    {1, 1, 2, 1, 1},
    {1, 1, 1, 1, 1},
    {0, 1, 1, 1, 0}
}

local animationarea = {
    {
        {0, 0, 0, 0, 0},
        {0, 0, 1, 0, 0},
        {0, 1, 2, 1, 0},
        {0, 0, 1, 0, 0},
        {0, 0, 0, 0, 0}
    },
    {
        {0, 0, 0, 0, 0},
        {0, 1, 0, 1, 0},
        {0, 0, 0, 0, 0},
        {0, 1, 0, 1, 0},
        {0, 0, 0, 0, 0}
    },
    {
        {0, 1, 1, 1, 0},
        {1, 0, 0, 0, 1},
        {1, 0, 0, 0, 1},
        {1, 0, 0, 0, 1},
        {0, 1, 1, 1, 0}
    }
}
 
local area = createCombatArea(arr)
combat:setArea(area)

function onGetFormulaValues(player, level, maglevel)
    local min = (level / 5) + (maglevel * 1.4) + 8
    local max = (level / 5) + (maglevel * 2.2) + 14
    return -min, -max
end

combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

local function animation(pos, playerpos)
    --if not Tile(Position(pos)):hasProperty(CONST_PROP_BLOCKPROJECTILE) then
        if Position(pos):isSightClear(playerpos) then
            Position(pos):sendMagicEffect(CONST_ME_FIREAREA)
        end
    --end
end

function onCastSpell(creature, var)

    local creaturepos = creature:getPosition()
    local playerpos = Position(creaturepos)
    local delay = 200
 
    local centre = {}
    local damagearea = {}
    for j = 1, #animationarea do
        for k,v in ipairs(animationarea[j]) do
            for i = 1, #v do
                --print(v[i])
                if v[i] == 3 or v[i] == 2 then
                    centre.Row = k
                    centre.Column = i
                    table.insert(damagearea, centre)
                elseif v[i] == 1 then
                    local darea = {}
                    darea.Row = k
                    darea.Column = i
                    darea.Delay = j * delay
                    table.insert(damagearea, darea)
                end
            end
        end
        --print(centre.Column .. "," .. centre.Row)
    end
    for i = 1,#damagearea do
        --print(damagearea[i].Column .. "," .. damagearea[i].Row)
        local modifierx = damagearea[i].Column - centre.Column
        local modifiery = damagearea[i].Row - centre.Row
        --print("x " .. modifierx .. " " .. "y " .. modifiery)
        local damagepos = Position(creaturepos)
        damagepos.x = damagepos.x + modifierx
        damagepos.y = damagepos.y + modifiery
        --print("Damage: " .. damagepos.x .. "," .. damagepos.y .. "," .. damagepos.z)
        local animationDelay = damagearea[i].Delay or 0
        addEvent(animation, animationDelay, damagepos, playerpos)
    end
    return combat:execute(creature, var)
end

You may want to use both of these systems.
Maybe you want to animate fire wave or a stoneshower/gfb area, but you want the damage to apply immediately just like in vanilla tibia:

Wxh3Y98.gif


Well here is how you would do that:

fire wave (animated):
Lua:
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_FIREDAMAGE)

-- Set area
local arr = AREA_WAVE4
 
local area = createCombatArea(arr)
combat:setArea(area)

function onGetFormulaValues(player, level, maglevel)
    local min = (level / 5) + (maglevel * 1.2) + 7
    local max = (level / 5) + (maglevel * 2) + 12
    return -min, -max
end

combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

local function animation(pos, playerpos)
    if not Tile(Position(pos)):hasProperty(CONST_PROP_BLOCKPROJECTILE) then
        if Position(pos):isSightClear(playerpos) then
            Position(pos):sendMagicEffect(CONST_ME_HITBYFIRE)
        end
    end
end

function onCastSpell(creature, var)

    local animationarea = arr
    local creaturepos = creature:getPosition()
    local playerpos = Position(creaturepos)
    local directional = true -- is spell directional?
    local directionalx = 0
    local directionaly = 0
    local delay = 100
 
    -- Rotate based on direction
    if directional then
        local direction = creature:getDirection()
        if direction == 1 then
            animationarea = rotate_CW_90(animationarea)
            directionalx = 1
        elseif direction == 2 then
            animationarea = rotate_180(animationarea)
            directionaly = 1
        elseif direction == 3 then
            animationarea = rotate_CCW_90(animationarea)
            directionalx = -1
        else
            directionaly = -1
        end
    end
 
    local centre = {}
    local damagearea = {}
    for k,v in ipairs(animationarea) do
        for i = 1, #v do
            --print(v[i])
            if v[i] == 3 or v[i] == 2 then
                centre.Row = k
                centre.Column = i
                table.insert(damagearea, centre)
            elseif v[i] == 1 then
                local darea = {}
                darea.Row = k
                darea.Column = i
                table.insert(damagearea, darea)
            end
        end
    end
    --print(centre.Column .. "," .. centre.Row)
    for i = 1,#damagearea do
        local animationDelay = delay
        --print(damagearea[i].Column .. "," .. damagearea[i].Row)
        local modifierx = damagearea[i].Column - centre.Column
        local modifiery = damagearea[i].Row - centre.Row
        --print("x " .. modifierx .. " " .. "y " .. modifiery)
        local damagepos = Position(creaturepos)
        damagepos.x = damagepos.x + modifierx + directionalx
        damagepos.y = damagepos.y + modifiery + directionaly
        --print("Damage: " .. damagepos.x .. "," .. damagepos.y .. "," .. damagepos.z)
        if directional then
            local direction = creature:getDirection()
            if direction == 1 then
                animationDelay = (modifierx + directionalx) * delay
            elseif direction == 2 then
                animationDelay = (modifiery + directionaly) * delay
            elseif direction == 3 then
                animationDelay = ((modifierx + directionalx) * -1) * delay
            else
                animationDelay = ((modifiery + directionaly) * -1) * delay
            end
            animationDelay = animationDelay - delay
            --print(animationDelay)
        end
        addEvent(animation, animationDelay, damagepos, playerpos)
    end
    return combat:execute(creature, var)
end

With fire wave (or any directional spell), the direction of the wave needs to be rotated depending on the direction you are facing.
This is done automatically for the combat object/area but for the animation logic you'd need to do this again.

Here is the rotation code I use, you'll need to add the rotate function to the bottom of your spells\lib\spells.lua file if you want to use the fire wave script from this thread:
Lua:
function rotate_CCW_90(m)
   local rotated = {}
   for c, m_1_c in ipairs(m[1]) do
      local col = {m_1_c}
      for r = 2, #m do
         col[r] = m[r][c]
      end
      table.insert(rotated, 1, col)
   end
   return rotated
end

function rotate_180(m)
   return rotate_CCW_90(rotate_CCW_90(m))
end

function rotate_CW_90(m)
   return rotate_CCW_90(rotate_CCW_90(rotate_CCW_90(m)))
end

Here is the animated stoneshower script too:
rock slide.lua
Lua:
-- Frames (1 = Area, 2 = Player, 3 = Player + Self Damaging)
local combat = Combat()
combat:setParameter(COMBAT_PARAM_TYPE, COMBAT_EARTHDAMAGE)

local arr = {
        {0, 1, 1, 1, 0},
        {1, 1, 1, 1, 1},
        {1, 1, 3, 1, 1},
        {1, 1, 1, 1, 1},
        {0, 1, 1, 1, 0}
}

local area = createCombatArea(arr)
    combat:setArea(area)

function onGetFormulaValues(player, level, maglevel)
    local min = (level / 5) + (maglevel * 1.6) + 16
    local max = (level / 5) + (maglevel * 3.0) + 28
    return -min, -max
end

combat:setCallback(CALLBACK_PARAM_LEVELMAGICVALUE, "onGetFormulaValues")

local function animation(pos, playerpos)
    if not Tile(Position(pos)):hasProperty(CONST_PROP_BLOCKPROJECTILE) then
        -- This is only applicable for directional spells
        --if Position(pos):isSightClear(playerpos) then
        Position(pos):sendMagicEffect(CONST_ME_STONES)
        Position(pos):sendMagicEffect(CONST_ME_GROUNDSHAKER)
        --end
    end
end

function onCastSpell(creature, var)

    local animationarea = arr
    local creaturepos = creature:getPosition()
    local playerpos = Position(creaturepos)
    local targeted = true -- is this a targettable spell? (strike or gfb-like spell etc)
    local delay = 100
 
    if targeted then
        creaturepos = creature:getTarget():getPosition()
    end
 
    local centre = {}
    local damagearea = {}
    for k,v in ipairs(animationarea) do
        for i = 1, #v do
            if v[i] == 3 or v[i] == 2 then
                centre.Row = k
                centre.Column = i
                table.insert(damagearea, centre)
            elseif v[i] == 1 then
                local darea = {}
                darea.Row = k
                darea.Column = i
                table.insert(damagearea, darea)
            end
        end
    end

    for i = 1,#damagearea do
        -- adjust delay randomizer here, different animations have different "sweet-spot" delays
        local animationDelay = math.random(1,6) * delay
        local modifierx = damagearea[i].Column - centre.Column
        local modifiery = damagearea[i].Row - centre.Row
        local damagepos = Position(creaturepos)
        damagepos.x = damagepos.x + modifierx
        damagepos.y = damagepos.y + modifiery
        addEvent(animation, animationDelay, damagepos, playerpos)
    end
    return combat:execute(creature, var)
end
Basically what you're doing is casting the spell with NO animation parameter:
combat:setParameter(COMBAT_PARAM_EFFECT, CONST_ME_HITBYFIRE)

And dealing with the animations yourself:
Lua:
addEvent(animation, animationDelay, damagepos, playerpos)

local function animation(pos, playerpos)
    if not Tile(Position(pos)):hasProperty(CONST_PROP_BLOCKPROJECTILE) then
        if Position(pos):isSightClear(playerpos) then
            Position(pos):sendMagicEffect(CONST_ME_HITBYFIRE)
        end
    end
end

Using either of these methods, and by cutting and splicing this code, you should be able to make more creative and interesting spells :)
 
Last edited:
As someone that doesn't know how to code by itself, your posts are of GREAT help to understand and make my own little changes here and there.
 
Great Job Man....Perfect Spells, but....
In my distro, i receive this msg if i don't target the creature and spend the spell:

Code:
Lua Script Error: [Spell Interface]
data/spells/scripts/attack/arrowshower.lua:onCastSpell
data/spells/scripts/attack/arrowshower.lua:43: attempt to index a nil value
stack traceback:
        [C]: in function '__index'
        data/spells/scripts/attack/arrowshower.lua:43: in function <data/spells/
scripts/attack/arrowshower.lua:34>

And i wanna use CONST_ANI_ARROW as SENDMAGICEFFECT but don't work.
 
mega nova.png
Spell casts 1sqm in front of player how to fix? Tfs 1.2
First Script.
Post automatically merged:

And other bug, if i stand in front of houses/temple i can't cast spell (now i use second script static)
meganova2.png
Post automatically merged:

Can merge for example sendMagicEffect emm first animation death and second vis?
 
Last edited:
I have just tested it, and it works perfectly.
This system is so great that I'm trying a lot of new things;

Thanks, @Leo32
 
Last edited:
This is such a cool script to build more complex spells. I will try this.
 
View attachment 43752
Spell casts 1sqm in front of player how to fix? Tfs 1.2
First Script.
Post automatically merged:

And other bug, if i stand in front of houses/temple i can't cast spell (now i use second script static)
View attachment 43753
Post automatically merged:

Can merge for example sendMagicEffect emm first animation death and second vis?

your spells.xml casterTargetOrDirection="1" change to 0
or direction change to 0
 
Back
Top