diff --git a/lib/animation_node.lua b/lib/animation_node.lua new file mode 100644 index 0000000..3b0be94 --- /dev/null +++ b/lib/animation_node.lua @@ -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 diff --git a/lib/character/behaviors/map.lua b/lib/character/behaviors/map.lua index 7083ac0..07e8708 100644 --- a/lib/character/behaviors/map.lua +++ b/lib/character/behaviors/map.lua @@ -7,7 +7,7 @@ local utils = require "lib.utils.utils" --- @field displayedPosition Vec3 точка, в которой персонаж отображается --- @field t0 number время начала движения для анимациии --- @field path Deque путь, по которому сейчас бежит персонаж ---- @field onWalkEnd nil | fun() : nil Функция, которая будет вызвана по завершению [followPath] +--- @field animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения --- @field size Vec3 local mapBehavior = {} mapBehavior.__index = mapBehavior @@ -25,12 +25,13 @@ function mapBehavior.new(position, size) end --- @param path Deque -function mapBehavior:followPath(path, onEnd) - if path:is_empty() then return onEnd() end - self.onWalkEnd = onEnd +--- @param animationNode AnimationNode +function mapBehavior:followPath(path, animationNode) + if path:is_empty() then return animationNode:finish() end + self.animationNode = animationNode self.position = self.displayedPosition self.owner:try(Tree.behaviors.sprite, function(sprite) - sprite:play("run", true) + sprite:loop("run") end) self.path = path; ---@type Vec3 @@ -66,11 +67,11 @@ function mapBehavior:update(dt) self.path:pop_front() else -- мы добежали до финальной цели self.owner:try(Tree.behaviors.sprite, function(sprite) - sprite:play("idle", true) + sprite:loop("idle") end) self.runTarget = nil - if self.onWalkEnd then - self.onWalkEnd() + if self.animationNode then + self.animationNode:finish() self.onWalkEnd = nil end end diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 2cb65bf..3b99843 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -68,4 +68,25 @@ function sprite:play(state, loop) self.state = state 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 diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 93d6368..5989514 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,6 +7,8 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async +local Animation = require "lib.animation_node" + --- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell --- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла --- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире @@ -37,11 +39,33 @@ function walk:cast(caster, target) if path:is_empty() then return false end for p in path:values() do print(p) end - caster:has(Tree.behaviors.map):followPath(path, function() - caster:has(Tree.behaviors.spellcaster):endCast() - end) - -- TODO: списать деньги за каст (антиутопия какая-то) - -- TODO: привязка тинькоффа + + Animation { + function(node) caster:has(Tree.behaviors.sprite):animate("hurt", node) end, + onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, + 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) stats.mana = stats.mana - 2 print(stats.mana)