implement AnimationNode
This commit is contained in:
parent
c27089d23e
commit
7884b13b70
73
lib/animation_node.lua
Normal file
73
lib/animation_node.lua
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
--- @alias voidCallback fun(): nil
|
||||||
|
--- @alias animationRunner fun(node: AnimationNode)
|
||||||
|
|
||||||
|
--- Узел дерева анимаций.
|
||||||
|
---
|
||||||
|
--- Отслеживает завершение всех анимаций всех дочерних узлов и оповещает вышестоящий узел.
|
||||||
|
---
|
||||||
|
--- Дочерние узлы одного уровня запускают свою анимацию одновременно после завершения анимации родителя.
|
||||||
|
--- Example:
|
||||||
|
--- ```lua
|
||||||
|
--- AnimationNode {
|
||||||
|
--- function (node) residentsleeper:sleep(1000, node) end, -- must pass itself down as the parameter
|
||||||
|
--- onEnd = function() print("completed") end,
|
||||||
|
--- children = { -- children run in parallel after the parent animation is completed
|
||||||
|
--- AnimationNode {
|
||||||
|
--- function (node) sprite:animate("attack", node) end
|
||||||
|
--- },
|
||||||
|
--- AnimationNode {
|
||||||
|
--- function (node) other_sprite:animate("hurt", node) end
|
||||||
|
--- },
|
||||||
|
--- }
|
||||||
|
--- }:run()
|
||||||
|
--- ```
|
||||||
|
--- @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] or function(self)
|
||||||
|
self:finish()
|
||||||
|
end
|
||||||
|
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
|
||||||
@ -1,5 +1,6 @@
|
|||||||
--- @meta _
|
--- @meta _
|
||||||
Tree.behaviors.map = require "lib.character.behaviors.map"
|
Tree.behaviors.map = require "lib.character.behaviors.map"
|
||||||
Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster"
|
Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster"
|
||||||
Tree.behaviors.sprite = require "lib.character.behaviors.sprite"
|
Tree.behaviors.sprite = require "lib.character.behaviors.sprite"
|
||||||
Tree.behaviors.stats = require "lib.character.behaviors.stats"
|
Tree.behaviors.stats = require "lib.character.behaviors.stats"
|
||||||
|
Tree.behaviors.residentsleeper = require "lib.character.behaviors.residentsleeper"
|
||||||
|
|||||||
@ -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
|
||||||
@ -62,17 +63,13 @@ function mapBehavior:update(dt)
|
|||||||
if fraction >= 1 then -- анимация перемещена завершена
|
if fraction >= 1 then -- анимация перемещена завершена
|
||||||
self.position = self.runTarget
|
self.position = self.runTarget
|
||||||
if not self.path:is_empty() then -- еще есть, куда бежать
|
if not self.path:is_empty() then -- еще есть, куда бежать
|
||||||
self:runTo(self.path:peek_front())
|
self:runTo(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.animationNode:finish() end
|
||||||
self.onWalkEnd()
|
|
||||||
self.onWalkEnd = nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
else -- анимация перемещения не завершена
|
else -- анимация перемещения не завершена
|
||||||
self.displayedPosition = utils.lerp(self.position, self.runTarget, fraction) -- линейный интерполятор
|
self.displayedPosition = utils.lerp(self.position, self.runTarget, fraction) -- линейный интерполятор
|
||||||
|
|||||||
28
lib/character/behaviors/residentsleeper.lua
Normal file
28
lib/character/behaviors/residentsleeper.lua
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
--- Умеет асинхронно ждать какое-то время (для анимаций)
|
||||||
|
--- @class ResidentSleeperBehavior : Behavior
|
||||||
|
--- @field animationNode? AnimationNode
|
||||||
|
--- @field endsAt? number
|
||||||
|
local behavior = {}
|
||||||
|
behavior.__index = behavior
|
||||||
|
behavior.id = "residentsleeper"
|
||||||
|
|
||||||
|
function behavior.new() return setmetatable({}, behavior) end
|
||||||
|
|
||||||
|
function behavior:update(dt)
|
||||||
|
if not self.animationNode then return end
|
||||||
|
if love.timer.getTime() >= self.endsAt then
|
||||||
|
self.animationNode:finish()
|
||||||
|
self.animationNode = nil
|
||||||
|
self.endsAt = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param ms number time to wait in milliseconds
|
||||||
|
--- @param node AnimationNode
|
||||||
|
function behavior:sleep(ms, node)
|
||||||
|
if self.animationNode then node:finish() end
|
||||||
|
self.animationNode = node
|
||||||
|
self.endsAt = love.timer.getTime() + ms / 1000
|
||||||
|
end
|
||||||
|
|
||||||
|
return behavior
|
||||||
@ -27,7 +27,7 @@ function sprite.new(spriteDir)
|
|||||||
|
|
||||||
anim.state = "idle"
|
anim.state = "idle"
|
||||||
anim.side = sprite.RIGHT
|
anim.side = sprite.RIGHT
|
||||||
anim:play("idle")
|
anim:loop("idle")
|
||||||
return anim
|
return anim
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -54,18 +54,25 @@ function sprite:draw()
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param state string
|
--- @param node AnimationNode
|
||||||
--- @param loop nil | boolean | fun(): nil
|
function sprite:animate(state, node)
|
||||||
function sprite:play(state, loop)
|
|
||||||
if not self.animationGrid[state] then
|
if not self.animationGrid[state] then
|
||||||
return print("[SpriteBehavior]: no animation for '" .. state .. "'")
|
return print("[SpriteBehavior]: no animation for '" .. state .. "'")
|
||||||
end
|
end
|
||||||
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
|
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
|
||||||
type(loop) == "function" and loop or
|
|
||||||
function()
|
function()
|
||||||
if not loop then self:play("idle", true) end
|
self:loop("idle")
|
||||||
|
node:finish()
|
||||||
end)
|
end)
|
||||||
self.state = state
|
self.state = state
|
||||||
end
|
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
|
||||||
|
|||||||
@ -27,6 +27,7 @@ local function spawn(name, spriteDir, position, size, level)
|
|||||||
char._behaviorsIdx = {}
|
char._behaviorsIdx = {}
|
||||||
|
|
||||||
char:addBehavior {
|
char:addBehavior {
|
||||||
|
Tree.behaviors.residentsleeper.new(),
|
||||||
Tree.behaviors.stats.new(),
|
Tree.behaviors.stats.new(),
|
||||||
Tree.behaviors.map.new(position, size),
|
Tree.behaviors.map.new(position, size),
|
||||||
Tree.behaviors.sprite.new(spriteDir),
|
Tree.behaviors.sprite.new(spriteDir),
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
|
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
|
||||||
--- Да, это Future/Promise/await/async
|
--- Да, это Future/Promise/await/async
|
||||||
|
|
||||||
|
local AnimationNode = 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,15 +39,19 @@ 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()
|
|
||||||
end)
|
|
||||||
-- TODO: списать деньги за каст (антиутопия какая-то)
|
|
||||||
-- TODO: привязка тинькоффа
|
|
||||||
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)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local sprite = caster:has(Tree.behaviors.sprite)
|
||||||
|
if not sprite then return true end
|
||||||
|
AnimationNode {
|
||||||
|
function(node) caster:has(Tree.behaviors.map):followPath(path, node) end,
|
||||||
|
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
|
||||||
|
}:run()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -73,14 +79,15 @@ function regenerateMana:cast(caster, target)
|
|||||||
stats.mana = 10
|
stats.mana = 10
|
||||||
end)
|
end)
|
||||||
print(caster.id, "has regenerated mana")
|
print(caster.id, "has regenerated mana")
|
||||||
caster:try(Tree.behaviors.sprite, function(sprite) -- бойлерплейт (временный)
|
local sprite = caster:has(Tree.behaviors.sprite)
|
||||||
-- В данный момент заклинание не позволяет отслеживать состояние последствий своего применения, так что надо повесить хоть какую-то анимашку просто для того, чтобы отложить завершение каста куда-то в будущее
|
if not sprite then return true end
|
||||||
-- См. также https://learn.javascript.ru/settimeout-setinterval
|
AnimationNode {
|
||||||
sprite:play("hurt", function()
|
function(node)
|
||||||
sprite:play("idle")
|
sprite:animate("hurt", node)
|
||||||
caster:has(Tree.behaviors.spellcaster):endCast()
|
end,
|
||||||
end)
|
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end
|
||||||
end)
|
}:run()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -107,18 +114,32 @@ function attack:cast(caster, target)
|
|||||||
stats.hp = stats.hp - 4
|
stats.hp = stats.hp - 4
|
||||||
end)
|
end)
|
||||||
|
|
||||||
caster:try(Tree.behaviors.sprite, function(sprite)
|
local sprite = caster:has(Tree.behaviors.sprite)
|
||||||
sprite:play("attack", function()
|
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
|
||||||
sprite:play("idle")
|
if not sprite or not targetSprite then return true end
|
||||||
targetCharacter:try(Tree.behaviors.sprite, function(targetSprite)
|
|
||||||
targetSprite:play("hurt", function()
|
|
||||||
targetSprite:play("idle")
|
|
||||||
caster:has(Tree.behaviors.spellcaster):endCast()
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
|
AnimationNode {
|
||||||
|
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
|
||||||
|
children = {
|
||||||
|
AnimationNode {
|
||||||
|
function(node)
|
||||||
|
sprite:animate("attack", node)
|
||||||
|
end
|
||||||
|
},
|
||||||
|
AnimationNode {
|
||||||
|
function(node)
|
||||||
|
targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200, node)
|
||||||
|
end,
|
||||||
|
children = {
|
||||||
|
AnimationNode {
|
||||||
|
function(node)
|
||||||
|
targetSprite:animate("hurt", node)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}:run()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user