Add AnimationNode system and integrate into character behaviors and

spell casting
This commit is contained in:
PeaAshMeter 2025-10-20 03:36:03 +03:00
parent a0ddd5f7cd
commit 5de9d9eb9c
4 changed files with 110 additions and 13 deletions

51
lib/animation_node.lua Normal file
View File

@ -0,0 +1,51 @@
--- @alias voidCallback fun(): nil
--- @alias animationRunner fun(node: AnimationNode)
--- @class AnimationNode
--- @field count integer
--- @field run animationRunner
--- @field parent AnimationNode?
--- @field children AnimationNode[]
--- @field finish voidCallback
--- @field onEnd voidCallback?
local animation = {}
animation.__index = animation
--- Регистрация завершения дочерней анимации
function animation:bubbleUp()
self.count = self.count - 1
if self.count > 0 then return end
if self.onEnd then self.onEnd() end
if self.parent then self.parent:bubbleUp() end
end
--- @param children AnimationNode[]
--- Запланировать анимации после текущей, которые запустятся одновременно друг с другом
function animation:chain(children)
for _, child in ipairs(children) do
child.parent = self
table.insert(self.children, child)
self.count = self.count + 1
end
return self
end
--- @param data {[1]: animationRunner, onEnd?: voidCallback, children?: AnimationNode[]}
--- @return AnimationNode
local function new(data)
local t = setmetatable({}, animation)
t.run = data[1]
t.onEnd = data.onEnd
t.count = 1 -- своя анимация
t.children = {}
t:chain(data.children or {})
t.finish = function()
t:bubbleUp()
for _, anim in ipairs(t.children) do
anim:run()
end
end
return t
end
return new

View File

@ -7,7 +7,7 @@ local utils = require "lib.utils.utils"
--- @field displayedPosition Vec3 точка, в которой персонаж отображается --- @field displayedPosition Vec3 точка, в которой персонаж отображается
--- @field t0 number время начала движения для анимациии --- @field t0 number время начала движения для анимациии
--- @field path Deque путь, по которому сейчас бежит персонаж --- @field path Deque путь, по которому сейчас бежит персонаж
--- @field onWalkEnd nil | fun() : nil Функция, которая будет вызвана по завершению [followPath] --- @field animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения
--- @field size Vec3 --- @field size Vec3
local mapBehavior = {} local mapBehavior = {}
mapBehavior.__index = mapBehavior mapBehavior.__index = mapBehavior
@ -25,12 +25,13 @@ function mapBehavior.new(position, size)
end end
--- @param path Deque --- @param path Deque
function mapBehavior:followPath(path, onEnd) --- @param animationNode AnimationNode
if path:is_empty() then return onEnd() end function mapBehavior:followPath(path, animationNode)
self.onWalkEnd = onEnd if path:is_empty() then return animationNode:finish() end
self.animationNode = animationNode
self.position = self.displayedPosition self.position = self.displayedPosition
self.owner:try(Tree.behaviors.sprite, function(sprite) self.owner:try(Tree.behaviors.sprite, function(sprite)
sprite:play("run", true) sprite:loop("run")
end) end)
self.path = path; self.path = path;
---@type Vec3 ---@type Vec3
@ -66,11 +67,11 @@ function mapBehavior:update(dt)
self.path:pop_front() self.path:pop_front()
else -- мы добежали до финальной цели else -- мы добежали до финальной цели
self.owner:try(Tree.behaviors.sprite, function(sprite) self.owner:try(Tree.behaviors.sprite, function(sprite)
sprite:play("idle", true) sprite:loop("idle")
end) end)
self.runTarget = nil self.runTarget = nil
if self.onWalkEnd then if self.animationNode then
self.onWalkEnd() self.animationNode:finish()
self.onWalkEnd = nil self.onWalkEnd = nil
end end
end end

View File

@ -68,4 +68,25 @@ function sprite:play(state, loop)
self.state = state self.state = state
end end
--- @param node AnimationNode
function sprite:animate(state, node)
if not self.animationGrid[state] then
return print("[SpriteBehavior]: no animation for '" .. state .. "'")
end
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
function()
self:loop("idle")
node:finish()
end)
self.state = state
end
function sprite:loop(state)
if not self.animationGrid[state] then
return print("[SpriteBehavior]: no animation for '" .. state .. "'")
end
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED)
self.state = state
end
return sprite return sprite

View File

@ -7,6 +7,8 @@
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
--- Да, это Future/Promise/await/async --- Да, это Future/Promise/await/async
local Animation = require "lib.animation_node"
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell --- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла --- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире --- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
@ -37,11 +39,33 @@ function walk:cast(caster, target)
if path:is_empty() then return false end if path:is_empty() then return false end
for p in path:values() do print(p) end for p in path:values() do print(p) end
caster:has(Tree.behaviors.map):followPath(path, function()
caster:has(Tree.behaviors.spellcaster):endCast() Animation {
end) function(node) caster:has(Tree.behaviors.sprite):animate("hurt", node) end,
-- TODO: списать деньги за каст (антиутопия какая-то) onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
-- TODO: привязка тинькоффа children = {
Animation {
function(node)
caster:has(Tree.behaviors.map):followPath(path, node)
end,
},
Animation {
function(node)
Tree.level.characters[2]:has(Tree.behaviors.sprite):animate("hurt", node)
end,
children = {
Animation {
function(node)
local from = Tree.level.characters[2]:has(Tree.behaviors.map).position
local p = (require "lib.pathfinder")(from, Vec3 { 10, 10 })
Tree.level.characters[2]:has(Tree.behaviors.map):followPath(p, node)
end
}
}
}
}
}:run()
caster:try(Tree.behaviors.stats, function(stats) caster:try(Tree.behaviors.stats, function(stats)
stats.mana = stats.mana - 2 stats.mana = stats.mana - 2
print(stats.mana) print(stats.mana)