Add counter utility and chain async animations in spell cast Introduce a Counter module to coordinate multiple asynchronous animation callbacks and update spellbook cast to run chained animations sequentially. Also lock selector during AI turns.
225 lines
9.0 KiB
Lua
225 lines
9.0 KiB
Lua
--- Алгоритм обработки заклинания (for dummies):
|
||
--- 1) ПОКА выделен персонаж И он находится в режиме каста, вызывать spell:update() и spell:draw() каждый кадр (это отвечает за обработку и отрисовку превьюшки каста, например, превью пути или зоны поражения; реализуется через установку spellcaster.cast, см. код в кнопке)
|
||
--- ЕСЛИ выбран тайл, ТО вызвать spell:cast() (это запрос на обработку последствий применения заклинания, например, старт анимации ходьбы, выпуск снаряда и т.д.; реализовано в selector)
|
||
--- ЕСЛИ spell:cast() ИСТИНА, ТО вызвать selector:lock() (отключить обработку выделения всего на уровне; реализовано в selector)
|
||
---
|
||
--- 2) ПОКА анимация каста НЕ завершена, ничего не делать, ИНАЧЕ вызвать behaviors.spellcaster:endCast() (вот это сейчас нужно вызывать самостоятельно, т.к. нет возможности обобщенно отследить завершение анимаций)
|
||
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
|
||
--- Да, это Future/Promise/await/async
|
||
|
||
local AnimationNode = require "lib.animation_node"
|
||
local easing = require "lib.utils.easing"
|
||
|
||
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell
|
||
--- @field tag string
|
||
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
|
||
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
|
||
--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task<nil> | nil Вызывается в момент каста, изменяет мир.
|
||
local spell = {}
|
||
spell.__index = spell
|
||
spell.tag = "base"
|
||
|
||
function spell:update(caster, dt) end
|
||
|
||
function spell:draw() end
|
||
|
||
function spell:cast(caster, target) return true end
|
||
|
||
local walk = setmetatable({
|
||
--- @type Deque
|
||
path = nil
|
||
}, spell)
|
||
walk.tag = "dev_move"
|
||
|
||
function walk:cast(caster, target)
|
||
if not caster:try(Tree.behaviors.stats, function(stats)
|
||
return stats.mana >= 2
|
||
end) then
|
||
return
|
||
end
|
||
|
||
local initialPos = caster:has(Tree.behaviors.positioned).position:floor()
|
||
local path = require "lib.pathfinder" (initialPos, target)
|
||
path:pop_front()
|
||
if path:is_empty() then
|
||
print("[Walk]: the path is empty", initialPos, target)
|
||
return
|
||
end
|
||
|
||
caster:try(Tree.behaviors.stats, function(stats)
|
||
stats.mana = stats.mana - 2
|
||
end)
|
||
|
||
local sprite = caster:has(Tree.behaviors.sprite)
|
||
assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE")
|
||
if not sprite then
|
||
return
|
||
end
|
||
|
||
local testChar = Tree.level.characters[1];
|
||
|
||
|
||
|
||
|
||
|
||
return function(callback) -- <- вызовется после всех анимаций
|
||
local counter = require 'lib.utils.counter' (callback)
|
||
counter.push()
|
||
return caster:has(Tree.behaviors.tiled):followPath(path)(
|
||
function()
|
||
do
|
||
counter.push()
|
||
local initialPos2 = caster:has(Tree.behaviors.positioned).position:floor()
|
||
local path2 = require "lib.pathfinder" (initialPos2, Vec3 { 10, math.random(1, 10) })
|
||
path:pop_front()
|
||
caster:has(Tree.behaviors.tiled):followPath(path2)(counter.pop)
|
||
end
|
||
|
||
do
|
||
counter.push()
|
||
local testInitialPos = testChar:has(Tree.behaviors.positioned).position:floor()
|
||
local testPath = require "lib.pathfinder" (testInitialPos, Vec3 { 10, math.random(20, 5) })
|
||
path:pop_front()
|
||
testChar:has(Tree.behaviors.tiled):followPath(testPath)(counter.pop)
|
||
end
|
||
|
||
counter.pop()
|
||
end
|
||
) -- <- callback вызовется после followPath
|
||
end
|
||
end
|
||
|
||
function walk:update(caster, dt)
|
||
local charPos = caster:has(Tree.behaviors.positioned).position:floor()
|
||
--- @type Vec3
|
||
local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
|
||
self.path = require "lib.pathfinder" (charPos, mpos)
|
||
end
|
||
|
||
function walk:draw()
|
||
if not self.path then return end
|
||
--- Это отрисовка пути персонажа к мышке
|
||
Tree.level.camera:attach()
|
||
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer)
|
||
love.graphics.setColor(0.6, 0.75, 0.5)
|
||
for p in self.path:values() do
|
||
love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1)
|
||
end
|
||
love.graphics.setCanvas()
|
||
Tree.level.camera:detach()
|
||
love.graphics.setColor(1, 1, 1)
|
||
end
|
||
|
||
local regenerateMana = setmetatable({}, spell)
|
||
regenerateMana.tag = "dev_mana"
|
||
|
||
function regenerateMana:cast(caster, target)
|
||
caster:try(Tree.behaviors.stats, function(stats)
|
||
stats.mana = 10
|
||
stats.initiative = stats.initiative + 10
|
||
end)
|
||
print(caster.id, "has regenerated mana and gained initiative")
|
||
local sprite = caster:has(Tree.behaviors.sprite)
|
||
if not sprite then return nil end
|
||
|
||
local light = require "lib/character/character".spawn("Light Effect")
|
||
light:addBehavior {
|
||
Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 },
|
||
Tree.behaviors.residentsleeper.new(),
|
||
Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
|
||
}
|
||
|
||
return function(callback)
|
||
print(light:has(Tree.behaviors.light).animateColor)
|
||
light:has(Tree.behaviors.light):animateColor(Vec3 {})(function()
|
||
light:die()
|
||
callback()
|
||
end)
|
||
end
|
||
end
|
||
|
||
local attack = setmetatable({}, spell)
|
||
attack.tag = "dev_attack"
|
||
|
||
function attack:cast(caster, target)
|
||
if caster:try(Tree.behaviors.positioned, function(p)
|
||
local dist = math.max(math.abs(p.position.x - target.x), math.abs(p.position.y - target.y))
|
||
print("dist:", dist)
|
||
return dist > 2
|
||
end) then
|
||
return false
|
||
end
|
||
|
||
caster:try(Tree.behaviors.stats, function(stats)
|
||
stats.mana = stats.mana - 2
|
||
end)
|
||
|
||
--- @type Character
|
||
local targetCharacterId = Tree.level.characterGrid:get(target)
|
||
if not targetCharacterId or targetCharacterId == caster.id then return false end
|
||
local targetCharacter = Tree.level.characters[targetCharacterId]
|
||
targetCharacter:try(Tree.behaviors.stats, function(stats)
|
||
stats.hp = stats.hp - 4
|
||
end)
|
||
|
||
local sprite = caster:has(Tree.behaviors.sprite)
|
||
local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
|
||
if not sprite or not targetSprite then return true end
|
||
|
||
caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) 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(node)
|
||
end,
|
||
duration = 200,
|
||
children = {
|
||
AnimationNode {
|
||
function(node)
|
||
local audioPath = Tree.assets.files.audio
|
||
targetSprite:animate("hurt", node)
|
||
--- @type SourceFilter
|
||
local settings = {
|
||
type = "highpass",
|
||
volume = 1,
|
||
lowgain = 0.1
|
||
}
|
||
Tree.audio:play(audioPath.sounds.hurt)
|
||
end
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}:run()
|
||
|
||
return true
|
||
end
|
||
|
||
----------------------------------------
|
||
local spellbook = {
|
||
walk = walk,
|
||
regenerateMana = regenerateMana,
|
||
attack = attack
|
||
}
|
||
|
||
--- Создает новый спеллбук с уникальными спеллами (а не ссылками на шаблоны)
|
||
--- @param list Spell[]
|
||
function spellbook.of(list)
|
||
local spb = {}
|
||
for i, sp in ipairs(list) do
|
||
print(i)
|
||
spb[i] = setmetatable({}, { __index = sp })
|
||
end
|
||
return spb
|
||
end
|
||
|
||
return spellbook
|