From 9489cd0488688042f35102fcdb9878d7f96363fd Mon Sep 17 00:00:00 2001 From: neckrat Date: Fri, 23 Jan 2026 15:56:36 +0300 Subject: [PATCH 01/10] we can move under another (closest!!!!!!!!!) character cool!!!! --- lib/annotations.lua | 1 + lib/character/behaviors/ai.lua | 77 ++++++++++++++++++++++++++++++++++ lib/spellbook.lua | 6 ++- main.lua | 5 ++- 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 lib/character/behaviors/ai.lua diff --git a/lib/annotations.lua b/lib/annotations.lua index 6a0d688..cc32630 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -8,3 +8,4 @@ Tree.behaviors.light = require "character.behaviors.light" Tree.behaviors.positioned = require "character.behaviors.positioned" Tree.behaviors.tiled = require "character.behaviors.tiled" Tree.behaviors.cursor = require "character.behaviors.cursor" +Tree.behaviors.ai = require "lib.character.behaviors.ai" diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua new file mode 100644 index 0000000..99ca588 --- /dev/null +++ b/lib/character/behaviors/ai.lua @@ -0,0 +1,77 @@ +local AnimationNode = require "lib.animation_node" + +--- @class AIBehavior : Behavior +--- @field animationNode AnimationNode? +local behavior = {} +behavior.__index = behavior +behavior.id = "ai" + +function behavior.new() + return setmetatable({}, behavior) +end + +function behavior:update(dt) + self.owner:try(Tree.behaviors.spellcaster, function(b) + if b.state == "casting" then + b.cast:update(self.owner, dt) + end + end) + if self.animationNode and self.animationNode.state == "running" then + self.animationNode:update(dt) + -- print(self.animationNode.t) + end +end + +function behavior:draw() + self.owner:try(Tree.behaviors.spellcaster, function(b) + if b.state == "casting" then + b.cast:draw() + end + end) +end + +function behavior:makeMove() + self.owner:try(Tree.behaviors.spellcaster, function(spellB) + -- print('какещке') + self.animationNode = AnimationNode { + function(node) end, + onEnd = function() + -- print('kakeshke') + end, + children = { + AnimationNode { + function(node) + local caster = Vec3 {} + self.owner:try(Tree.behaviors.positioned, function(b) + caster = b.position + end) + local target = Vec3 {} + local charTarget + local minDist = 88005553535 -- spooky magic number + for k, v in pairs(Tree.level.characters) do + v:try(Tree.behaviors.positioned, function(b) + local dist = ((caster.x - b.position.x) ^ 2 + (caster.y - b.position.y) ^ 2) ^ 0.5 + if dist < minDist and dist ~= 0 then + minDist = dist + target = b.position + charTarget = v + end + -- print(k, b.position) + end) + break + end + -- print(target) + charTarget:try(Tree.behaviors.positioned, function(b) + target = Vec3 { target.x, target.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти + end) + spellB.spellbook[1]:cast(self.owner, target) + -- print(minDist, target) + end + } + } + } + self.animationNode:run() + end) +end + +return behavior diff --git a/lib/spellbook.lua b/lib/spellbook.lua index a4ec146..2aceae1 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -106,6 +106,9 @@ function regenerateMana:cast(caster, target) sprite:animate("hurt", node) Tree.audio:crossfade(audioPath.music.level1.battle, audioPath.music.level1.choral, 5000) + caster:try(Tree.behaviors.ai, function(b) + b:makeMove() + end) end, onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end }:run() @@ -176,7 +179,7 @@ function attack:cast(caster, target) volume = 1, lowgain = 0.1 } - Tree.audio:play(audioPath.sounds.hurt, settings) + Tree.audio:play(audioPath.sounds.hurt) end } } @@ -199,6 +202,7 @@ local spellbook = { function spellbook.of(list) local spb = {} for i, sp in ipairs(list) do + print(i) spb[i] = setmetatable({}, { __index = sp }) end return spb diff --git a/main.lua b/main.lua index 3cd3ddd..460acc5 100644 --- a/main.lua +++ b/main.lua @@ -26,12 +26,13 @@ function love.load() character.spawn("Baris") :addBehavior { Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, 1), + Tree.behaviors.stats.new(nil, nil, 2), Tree.behaviors.positioned.new(Vec3 { 5, 5 }), Tree.behaviors.tiled.new(), Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.shadowcaster.new(), - Tree.behaviors.spellcaster.new() + Tree.behaviors.spellcaster.new(), + Tree.behaviors.ai.new() }, } -- 2.47.2 From d48f1af1afd3b1032ae13dbe3c002588eb5d4e2c Mon Sep 17 00:00:00 2001 From: neckrat Date: Fri, 23 Jan 2026 15:57:10 +0300 Subject: [PATCH 02/10] micro refactoring --- lib/character/behaviors/ai.lua | 45 +++++++++++++++++----------------- main.lua | 20 +++++++++++++++ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 99ca588..fbd9fcf 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -1,5 +1,25 @@ local AnimationNode = require "lib.animation_node" +local function closestCharacter(char) + local caster = Vec3 {} + char:try(Tree.behaviors.positioned, function(b) + caster = b.position + end) + local charTarget + local minDist = 88005553535 -- spooky magic number + for k, v in pairs(Tree.level.characters) do + v:try(Tree.behaviors.positioned, function(b) + local dist = ((caster.x - b.position.x) ^ 2 + (caster.y - b.position.y) ^ 2) ^ 0.5 + if dist < minDist and dist ~= 0 then + minDist = dist + charTarget = v + end + -- print(k, b.position) + end) + end + return charTarget +end + --- @class AIBehavior : Behavior --- @field animationNode AnimationNode? local behavior = {} @@ -41,31 +61,12 @@ function behavior:makeMove() children = { AnimationNode { function(node) - local caster = Vec3 {} - self.owner:try(Tree.behaviors.positioned, function(b) - caster = b.position - end) - local target = Vec3 {} - local charTarget - local minDist = 88005553535 -- spooky magic number - for k, v in pairs(Tree.level.characters) do - v:try(Tree.behaviors.positioned, function(b) - local dist = ((caster.x - b.position.x) ^ 2 + (caster.y - b.position.y) ^ 2) ^ 0.5 - if dist < minDist and dist ~= 0 then - minDist = dist - target = b.position - charTarget = v - end - -- print(k, b.position) - end) - break - end - -- print(target) + local charTarget = closestCharacter(self.owner) + local target charTarget:try(Tree.behaviors.positioned, function(b) - target = Vec3 { target.x, target.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти + target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти end) spellB.spellbook[1]:cast(self.owner, target) - -- print(minDist, target) end } } diff --git a/main.lua b/main.lua index 460acc5..dff5af9 100644 --- a/main.lua +++ b/main.lua @@ -23,6 +23,26 @@ function love.load() Tree.behaviors.shadowcaster.new(), Tree.behaviors.spellcaster.new() }, + character.spawn("Foodor") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 1), + Tree.behaviors.positioned.new(Vec3 { 4, 3 }), + Tree.behaviors.tiled.new(), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, + character.spawn("Foodor") + :addBehavior { + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.stats.new(nil, nil, 1), + Tree.behaviors.positioned.new(Vec3 { 5, 3 }), + Tree.behaviors.tiled.new(), + Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), + Tree.behaviors.shadowcaster.new(), + Tree.behaviors.spellcaster.new() + }, character.spawn("Baris") :addBehavior { Tree.behaviors.residentsleeper.new(), -- 2.47.2 From 3f694ccec9d29fef63b34e10757ad1288b5bfb8c Mon Sep 17 00:00:00 2001 From: neckrat Date: Sat, 24 Jan 2026 01:46:05 +0300 Subject: [PATCH 03/10] attempt to save our souls (callback hell my beloved) Co-authored-by: Ivan Yuriev --- lib/character/behaviors/ai.lua | 60 +++++++++++++++++++++++++--------- lib/level/selector.lua | 4 ++- lib/level/turn_order.lua | 5 +++ lib/spellbook.lua | 14 ++++---- main.lua | 2 +- 5 files changed, 59 insertions(+), 26 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index fbd9fcf..20a3a83 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -22,6 +22,7 @@ end --- @class AIBehavior : Behavior --- @field animationNode AnimationNode? +--- @field target Vec3? local behavior = {} behavior.__index = behavior behavior.id = "ai" @@ -52,26 +53,53 @@ end function behavior:makeMove() self.owner:try(Tree.behaviors.spellcaster, function(spellB) - -- print('какещке') - self.animationNode = AnimationNode { - function(node) end, - onEnd = function() - -- print('kakeshke') - end, - children = { + local charTarget = closestCharacter(self.owner) + charTarget:try(Tree.behaviors.positioned, function(b) + self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти + end) + + spellB.spellbook[1]:cast(self.owner, self.target):chain { + spellB.spellbook[1]:cast(self.owner, Vec3 { 10, 10 }):chain { AnimationNode { - function(node) - local charTarget = closestCharacter(self.owner) - local target - charTarget:try(Tree.behaviors.positioned, function(b) - target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти - end) - spellB.spellbook[1]:cast(self.owner, target) + onEnd = function() + Tree.level.turnOrder:next() end } } - } - self.animationNode:run() + }:run() + + + + -- -- print('какещке') + -- self.animationNode = AnimationNode { -- кринж + -- function(node) end, + -- onEnd = function() + -- -- print('kakeshke') + -- end, + -- children = { + -- AnimationNode { + -- function(node) --тяжело + -- local charTarget = closestCharacter(self.owner) + -- charTarget:try(Tree.behaviors.positioned, function(b) + -- self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти + -- end) + -- spellB.spellbook[1]:cast(self.owner, self.target) + -- end, + -- onEnd = function() --база + -- end, + -- children = { + -- AnimationNode { + -- function(node) + -- -- if not self.target then return end + -- print("пупупупупупупупупупуупупуууууу") + -- print(spellB.spellbook[3]:cast(self.owner, self.target)) + -- end + -- } + -- } + -- } + -- } + -- } + -- self.animationNode:run() end) end diff --git a/lib/level/selector.lua b/lib/level/selector.lua index a5d8f98..fd93bbd 100644 --- a/lib/level/selector.lua +++ b/lib/level/selector.lua @@ -37,7 +37,9 @@ function selector:update(dt) if not selectedId then self:select(nil) end return end - if b.cast:cast(char, mousePosition) then + local task = b.cast:cast(char, mousePosition) + if task then + task:run() self:lock() b.state = "running" end diff --git a/lib/level/turn_order.lua b/lib/level/turn_order.lua index f03c836..97e04d1 100644 --- a/lib/level/turn_order.lua +++ b/lib/level/turn_order.lua @@ -34,6 +34,11 @@ function turnOrder:next() local next = self.pendingQueue:peek() if not next then return self:endRound() end self.current = self.pendingQueue:pop() + + local char = Tree.level.characters[self.current] + char:try(Tree.behaviors.ai, function(ai) + ai:makeMove() + end) end --- Меняем местами очередь сходивших и не сходивших (пустую) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 2aceae1..1df36f3 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -14,7 +14,7 @@ local easing = require "lib.utils.easing" --- @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): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать +--- @field cast fun(self: Spell, caster: Character, target: Vec3): AnimationNode | nil Вызывается в момент каста, изменяет мир. local spell = {} spell.__index = spell spell.tag = "base" @@ -35,12 +35,12 @@ function walk:cast(caster, target) if not caster:try(Tree.behaviors.stats, function(stats) return stats.mana >= 2 end) then - return false + return end local path = require "lib.pathfinder" (caster:has(Tree.behaviors.positioned).position:floor(), target) path:pop_front() - if path:is_empty() then return false end + if path:is_empty() then return end for p in path:values() do print(p) end @@ -50,15 +50,13 @@ function walk:cast(caster, target) end) local sprite = caster:has(Tree.behaviors.sprite) - if not sprite then return true end - AnimationNode { + if not sprite then return end + return AnimationNode { function(node) caster:has(Tree.behaviors.tiled):followPath(path, node) end, onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, - }:run() - - return true + } end function walk:update(caster, dt) diff --git a/main.lua b/main.lua index dff5af9..b2f120c 100644 --- a/main.lua +++ b/main.lua @@ -36,7 +36,7 @@ function love.load() character.spawn("Foodor") :addBehavior { Tree.behaviors.residentsleeper.new(), - Tree.behaviors.stats.new(nil, nil, 1), + Tree.behaviors.stats.new(nil, nil, 3), Tree.behaviors.positioned.new(Vec3 { 5, 3 }), Tree.behaviors.tiled.new(), Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), -- 2.47.2 From 93a4961419a53f4bb44c8e2c24714e2f18b353a9 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 25 Jan 2026 07:05:45 +0300 Subject: [PATCH 04/10] high-order functions my beloved --- .luarc.json | 3 +- lib/animation_node.lua | 2 + lib/character/behaviors/ai.lua | 91 ++++++++----------------------- lib/character/behaviors/light.lua | 27 +++++++-- lib/character/behaviors/tiled.lua | 17 ++++-- lib/level/selector.lua | 9 ++- lib/level/turn_order.lua | 4 +- lib/simple_ui/level/end_turn.lua | 2 +- lib/spellbook.lua | 57 +++++++------------ lib/task.lua | 0 lib/utils/task.lua | 36 ++++++++++++ 11 files changed, 127 insertions(+), 121 deletions(-) create mode 100644 lib/task.lua create mode 100644 lib/utils/task.lua diff --git a/.luarc.json b/.luarc.json index 0bef050..25bbd47 100644 --- a/.luarc.json +++ b/.luarc.json @@ -6,5 +6,6 @@ "love.filesystem.load": "loadfile" }, "workspace.ignoreDir": ["dev_utils"], - "diagnostics.ignoredFiles": "Disable" + "diagnostics.ignoredFiles": "Disable", + "completion.autoRequire": false } diff --git a/lib/animation_node.lua b/lib/animation_node.lua index e3f5aff..90bcf1b 100644 --- a/lib/animation_node.lua +++ b/lib/animation_node.lua @@ -23,6 +23,7 @@ local easing = require "lib.utils.easing" --- } --- }:run() --- ``` +--- @deprecated --- @class AnimationNode --- @field count integer --- @field run animationRunner @@ -73,6 +74,7 @@ function animation:update(dt) end end +--- @deprecated --- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]} --- @return AnimationNode local function new(data) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 20a3a83..7c52624 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -1,4 +1,5 @@ local AnimationNode = require "lib.animation_node" +local easing = require "lib.utils.easing" local function closestCharacter(char) local caster = Vec3 {} @@ -31,76 +32,28 @@ function behavior.new() return setmetatable({}, behavior) end -function behavior:update(dt) - self.owner:try(Tree.behaviors.spellcaster, function(b) - if b.state == "casting" then - b.cast:update(self.owner, dt) - end - end) - if self.animationNode and self.animationNode.state == "running" then - self.animationNode:update(dt) - -- print(self.animationNode.t) +--- @return Task +function behavior:makeTurn() + return function(callback) -- почему так, описано в Task + self.owner:try(Tree.behaviors.spellcaster, function(spellB) + local charTarget = closestCharacter(self.owner) + charTarget:try(Tree.behaviors.positioned, function(b) + self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти + end) + + spellB.spellbook[1]:cast(self.owner, self.target)(function() + -- здесь мы оказываемся после того, как сходили в первый раз + print("[AI]: finished move 1") + local newTarget = Vec3 { 1, 1 } + -- поэтому позиция персонажа для нового каста пересчитается динамически + spellB.spellbook[1]:cast(self.owner, newTarget)(function() + print("[AI]: finished move 2") + -- дергаем функцию после завершения хода + callback() + end) + end) + end) end end -function behavior:draw() - self.owner:try(Tree.behaviors.spellcaster, function(b) - if b.state == "casting" then - b.cast:draw() - end - end) -end - -function behavior:makeMove() - self.owner:try(Tree.behaviors.spellcaster, function(spellB) - local charTarget = closestCharacter(self.owner) - charTarget:try(Tree.behaviors.positioned, function(b) - self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти - end) - - spellB.spellbook[1]:cast(self.owner, self.target):chain { - spellB.spellbook[1]:cast(self.owner, Vec3 { 10, 10 }):chain { - AnimationNode { - onEnd = function() - Tree.level.turnOrder:next() - end - } - } - }:run() - - - - -- -- print('какещке') - -- self.animationNode = AnimationNode { -- кринж - -- function(node) end, - -- onEnd = function() - -- -- print('kakeshke') - -- end, - -- children = { - -- AnimationNode { - -- function(node) --тяжело - -- local charTarget = closestCharacter(self.owner) - -- charTarget:try(Tree.behaviors.positioned, function(b) - -- self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти - -- end) - -- spellB.spellbook[1]:cast(self.owner, self.target) - -- end, - -- onEnd = function() --база - -- end, - -- children = { - -- AnimationNode { - -- function(node) - -- -- if not self.target then return end - -- print("пупупупупупупупупупуупупуууууу") - -- print(spellB.spellbook[3]:cast(self.owner, self.target)) - -- end - -- } - -- } - -- } - -- } - -- } - -- self.animationNode:run() - end) -end - return behavior diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua index 1c629c6..16301e4 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -1,13 +1,17 @@ +local AnimationNode = require "lib.animation_node" +local easing = require "lib.utils.easing" + --- @class LightBehavior : Behavior --- @field intensity number --- @field color Vec3 --- @field seed integer --- @field colorAnimationNode? AnimationNode +--- @field private animateColorCallback? fun(): nil --- @field targetColor? Vec3 --- @field sourceColor? Vec3 -local behavior = {} -behavior.__index = behavior -behavior.id = "light" +local behavior = {} +behavior.__index = behavior +behavior.id = "light" ---@param values {intensity: number?, color: Vec3?, seed: integer?} ---@return LightBehavior @@ -26,11 +30,24 @@ function behavior:update(dt) self.colorAnimationNode:update(dt) end -function behavior:animateColor(targetColor, animationNode) +--- @TODO: refactor +function behavior:animateColor(targetColor) if self.colorAnimationNode then self.colorAnimationNode:finish() end - self.colorAnimationNode = animationNode + self.colorAnimationNode = AnimationNode { + function(_) end, + easing = easing.easeInQuad, + duration = 800, + onEnd = function() + if self.animateColorCallback then self.animateColorCallback() end + end + } + self.colorAnimationNode:run() self.sourceColor = self.color self.targetColor = targetColor + + return function(callback) + self.animateColorCallback = callback + end end function behavior:draw() diff --git a/lib/character/behaviors/tiled.lua b/lib/character/behaviors/tiled.lua index 3c8fbc5..b7b937b 100644 --- a/lib/character/behaviors/tiled.lua +++ b/lib/character/behaviors/tiled.lua @@ -5,7 +5,7 @@ local utils = require "lib.utils.utils" --- @field private runSource? Vec3 точка, из которой бежит персонаж --- @field private runTarget? Vec3 точка, в которую в данный момент бежит персонаж --- @field private path? Deque путь, по которому сейчас бежит персонаж ---- @field private animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения +--- @field private followPathCallback? fun() --- @field private t0 number время начала движения --- @field size Vec3 local behavior = {} @@ -20,10 +20,8 @@ function behavior.new(size) end --- @param path Deque ---- @param animationNode AnimationNode -function behavior:followPath(path, animationNode) - if path:is_empty() then return animationNode:finish() end - self.animationNode = animationNode +--- @return Task +function behavior:followPath(path) self.owner:try(Tree.behaviors.sprite, function(sprite) sprite:loop("run") end) @@ -32,6 +30,10 @@ function behavior:followPath(path, animationNode) local nextCell = path:peek_front() self:runTo(nextCell) path:pop_front() + + return function(callback) + self.followPathCallback = callback + end end --- @param target Vec3 @@ -72,7 +74,10 @@ function behavior:update(dt) sprite:loop("idle") end) self.runTarget = nil - if self.animationNode then self.animationNode:finish() end + + if self.followPathCallback then + self.followPathCallback() + end end else -- анимация перемещения не завершена positioned.position = utils.lerp(self.runSource, self.runTarget, fraction) -- линейный интерполятор diff --git a/lib/level/selector.lua b/lib/level/selector.lua index fd93bbd..3c60a6f 100644 --- a/lib/level/selector.lua +++ b/lib/level/selector.lua @@ -37,11 +37,16 @@ function selector:update(dt) if not selectedId then self:select(nil) end return end - local task = b.cast:cast(char, mousePosition) + local task = b.cast:cast(char, mousePosition) -- в task функция, которая запускает анимацию спелла if task then - task:run() self:lock() b.state = "running" + + task( + function(_) -- это коллбэк, который сработает по окончании анимации спелла + b:endCast() + end + ) end end) end diff --git a/lib/level/turn_order.lua b/lib/level/turn_order.lua index 97e04d1..d4d09d9 100644 --- a/lib/level/turn_order.lua +++ b/lib/level/turn_order.lua @@ -37,7 +37,9 @@ function turnOrder:next() local char = Tree.level.characters[self.current] char:try(Tree.behaviors.ai, function(ai) - ai:makeMove() + ai:makeTurn()(function() + self:next() + end) end) end diff --git a/lib/simple_ui/level/end_turn.lua b/lib/simple_ui/level/end_turn.lua index 8e36e29..ddc0c0b 100644 --- a/lib/simple_ui/level/end_turn.lua +++ b/lib/simple_ui/level/end_turn.lua @@ -57,7 +57,7 @@ function endTurnButton:onClick() end, duration = 1500, easing = easing.easeInOutCubic, - onEnd = function() Tree.level.selector:select(cid) end + onEnd = function() if not playing:has(Tree.behaviors.ai) then Tree.level.selector:select(cid) end end }:run() end diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 1df36f3..6c8c51e 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -14,7 +14,7 @@ local easing = require "lib.utils.easing" --- @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): AnimationNode | nil Вызывается в момент каста, изменяет мир. +--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task | nil Вызывается в момент каста, изменяет мир. local spell = {} spell.__index = spell spell.tag = "base" @@ -38,25 +38,25 @@ function walk:cast(caster, target) return end - local path = require "lib.pathfinder" (caster:has(Tree.behaviors.positioned).position:floor(), target) + local initialPos = caster:has(Tree.behaviors.positioned).position:floor() + local path = require "lib.pathfinder" (initialPos, target) path:pop_front() - if path:is_empty() then return end - - for p in path:values() do print(p) end + 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 - print(stats.mana) end) local sprite = caster:has(Tree.behaviors.sprite) - if not sprite then return end - return AnimationNode { - function(node) - caster:has(Tree.behaviors.tiled):followPath(path, node) - end, - onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, - } + assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE") + if not sprite then + return + end + + return caster:has(Tree.behaviors.tiled):followPath(path) end function walk:update(caster, dt) @@ -90,7 +90,7 @@ function regenerateMana:cast(caster, target) end) print(caster.id, "has regenerated mana and gained initiative") local sprite = caster:has(Tree.behaviors.sprite) - if not sprite then return true end + if not sprite then return nil end local light = require "lib/character/character".spawn("Light Effect") light:addBehavior { @@ -98,29 +98,14 @@ function regenerateMana:cast(caster, target) Tree.behaviors.residentsleeper.new(), Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), } - AnimationNode { - function(node) - local audioPath = Tree.assets.files.audio - sprite:animate("hurt", node) - Tree.audio:crossfade(audioPath.music.level1.battle, - audioPath.music.level1.choral, 5000) - caster:try(Tree.behaviors.ai, function(b) - b:makeMove() - end) - end, - onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end - }:run() - AnimationNode { - function(node) - light:has(Tree.behaviors.light):animateColor(Vec3 {}, node) - end, - easing = easing.easeInQuad, - duration = 800, - onEnd = function() light:die() end - }:run() - - return true + 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) diff --git a/lib/task.lua b/lib/task.lua new file mode 100644 index 0000000..e69de29 diff --git a/lib/utils/task.lua b/lib/utils/task.lua new file mode 100644 index 0000000..a64ba49 --- /dev/null +++ b/lib/utils/task.lua @@ -0,0 +1,36 @@ +--- Обобщенная асинхронная функция +--- +--- Использование в общих чертах выглядит так: +--- ```lua +--- local multiplyByTwoCallback = nil +--- local n = nil +--- local function multiplyByTwoAsync(number) +--- -- императивно сохраняем/обрабатываем параметр +--- n = number +--- return function(callback) -- это функция, которая запускает задачу +--- multiplyByTwoCallback = callback +--- end +--- end +--- +--- local function update(dt) +--- --- ждем нужного момента времени... +--- +--- if multiplyByTwoCallback then -- завершаем вычисление +--- local result = n * 2 +--- multiplyByTwoCallback(result) -- результат асинхронного вычисления идет в параметр коллбека! +--- multiplyByTwoCallback = nil +--- end +--- end +--- +--- +--- --- потом это можно вызывать так: +--- local task = multiplyByTwoAsync(21) +--- -- это ленивое вычисление, так что в этот момент ничего не произойдет +--- -- запускаем +--- task( +--- function(result) print(result) end -- выведет 42 после завершения вычисления, т.е. аналогично `task.then((res) => print(res))` на JS +--- ) +--- +--- ``` +--- @generic T +--- @alias Task fun(callback: fun(value: T): nil): nil -- 2.47.2 From 86a599723e5293000a117c54a77d4395a29bd227 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Thu, 29 Jan 2026 02:32:58 +0300 Subject: [PATCH 05/10] TLDR: higher-order functions my beloved 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. --- lib/level/turn_order.lua | 2 ++ lib/spellbook.lua | 32 +++++++++++++++++++++++++++++++- lib/task.lua | 0 lib/utils/counter.lua | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) delete mode 100644 lib/task.lua create mode 100644 lib/utils/counter.lua diff --git a/lib/level/turn_order.lua b/lib/level/turn_order.lua index d4d09d9..33200e6 100644 --- a/lib/level/turn_order.lua +++ b/lib/level/turn_order.lua @@ -37,7 +37,9 @@ function turnOrder:next() local char = Tree.level.characters[self.current] char:try(Tree.behaviors.ai, function(ai) + Tree.level.selector:lock() ai:makeTurn()(function() + Tree.level.selector:unlock() self:next() end) end) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 6c8c51e..1767c4f 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -56,7 +56,37 @@ function walk:cast(caster, target) return end - return caster:has(Tree.behaviors.tiled):followPath(path) + 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) diff --git a/lib/task.lua b/lib/task.lua deleted file mode 100644 index e69de29..0000000 diff --git a/lib/utils/counter.lua b/lib/utils/counter.lua new file mode 100644 index 0000000..9e8273f --- /dev/null +++ b/lib/utils/counter.lua @@ -0,0 +1,38 @@ +--- @class Counter +--- @field private count integer +--- @field private onFinish fun(): nil +--- @field private isAlive boolean +--- @field push fun():nil добавить 1 к счетчику +--- @field pop fun():nil убавить 1 у счетчика +local counter = {} +counter.__index = counter + + +--- @private +function counter:_push() + self.count = self.count + 1 +end + +--- @private +function counter:_pop() + self.count = self.count - 1 + if self.count == 0 and self.isAlive then + self.isAlive = false + self.onFinish() + end +end + +--- @param onFinish fun(): nil +local function new(onFinish) + local t = { + count = 0, + onFinish = onFinish, + isAlive = true, + } + t.push = function() counter._push(t) end + t.pop = function() counter._pop(t) end + + return setmetatable(t, counter) +end + +return new -- 2.47.2 From 59cc0fba0bbd4d385a2ae8d46ee54aacb56f3ec8 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Fri, 30 Jan 2026 00:32:05 +0300 Subject: [PATCH 06/10] rewrite sprite:animate, residentsleeper:sleep, attack:cast to use callback trees --- lib/animation_node.lua | 1 - lib/annotations.lua | 2 + lib/character/behaviors/residentsleeper.lua | 32 ++++++++--- lib/character/behaviors/sprite.lua | 21 ++++--- lib/spellbook.lua | 63 +++++++-------------- 5 files changed, 58 insertions(+), 61 deletions(-) diff --git a/lib/animation_node.lua b/lib/animation_node.lua index 90bcf1b..ece3aa7 100644 --- a/lib/animation_node.lua +++ b/lib/animation_node.lua @@ -1,6 +1,5 @@ local easing = require "lib.utils.easing" ---- @alias voidCallback fun(): nil --- @alias animationRunner fun(node: AnimationNode) --- Узел дерева анимаций. diff --git a/lib/annotations.lua b/lib/annotations.lua index cc32630..a6be927 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -9,3 +9,5 @@ Tree.behaviors.positioned = require "character.behaviors.positioned" Tree.behaviors.tiled = require "character.behaviors.tiled" Tree.behaviors.cursor = require "character.behaviors.cursor" Tree.behaviors.ai = require "lib.character.behaviors.ai" + +--- @alias voidCallback fun(): nil diff --git a/lib/character/behaviors/residentsleeper.lua b/lib/character/behaviors/residentsleeper.lua index c46b701..a1e1aef 100644 --- a/lib/character/behaviors/residentsleeper.lua +++ b/lib/character/behaviors/residentsleeper.lua @@ -1,21 +1,37 @@ --- Умеет асинхронно ждать какое-то время (для анимаций) --- @class ResidentSleeperBehavior : Behavior ---- @field animationNode? AnimationNode +--- @field private t0 number? +--- @field private sleepTime number? +--- @field private callback voidCallback? +--- @field private state 'running' | 'finished' 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 - self.animationNode:update(dt) +function behavior:update(_) + if self.state ~= 'running' then return end + + local t = love.timer.getTime() + if t >= self.t0 + self.sleepTime then + self.state = 'finished' + self.callback() + end end ---- @param node AnimationNode -function behavior:sleep(node) - if self.animationNode then self.animationNode:finish() end - self.animationNode = node +--- @return Task +function behavior:sleep(ms) + self.sleepTime = ms / 1000 + return function(callback) + if self.state == 'running' then + self.callback() + end + + self.t0 = love.timer.getTime() + self.callback = callback + self.state = 'running' + end end return behavior diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 33f076b..96eac8f 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -69,17 +69,20 @@ function sprite:draw() ) end ---- @param node AnimationNode +--- @return Task function sprite:animate(state, node) - if not self.animationGrid[state] then - return print("[SpriteBehavior]: no animation for '" .. state .. "'") + return function(callback) + if not self.animationGrid[state] then + print("[SpriteBehavior]: no animation for '" .. state .. "'") + callback() + end + self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED, + function() + self:loop("idle") + callback() + end) + self.state = 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) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 1767c4f..34af175 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,17 +7,16 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async -local AnimationNode = require "lib.animation_node" -local easing = require "lib.utils.easing" +local Counter = require 'lib.utils.counter' --- @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 Вызывается в момент каста, изменяет мир. -local spell = {} -spell.__index = spell -spell.tag = "base" +local spell = {} +spell.__index = spell +spell.tag = "base" function spell:update(caster, dt) end @@ -59,9 +58,6 @@ function walk:cast(caster, target) local testChar = Tree.level.characters[1]; - - - return function(callback) -- <- вызовется после всех анимаций local counter = require 'lib.utils.counter' (callback) counter.push() @@ -147,7 +143,7 @@ function attack:cast(caster, target) print("dist:", dist) return dist > 2 end) then - return false + return end caster:try(Tree.behaviors.stats, function(stats) @@ -156,7 +152,7 @@ function attack:cast(caster, target) --- @type Character local targetCharacterId = Tree.level.characterGrid:get(target) - if not targetCharacterId or targetCharacterId == caster.id then return false end + if not targetCharacterId or targetCharacterId == caster.id then return end local targetCharacter = Tree.level.characters[targetCharacterId] targetCharacter:try(Tree.behaviors.stats, function(stats) stats.hp = stats.hp - 4 @@ -164,43 +160,24 @@ function attack:cast(caster, target) local sprite = caster:has(Tree.behaviors.sprite) local targetSprite = targetCharacter:has(Tree.behaviors.sprite) - if not sprite or not targetSprite then return true end + if not sprite or not targetSprite then return 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 function(callback) + local c = Counter(callback) - return true + c.push() + sprite:animate("attack")(c.pop) + + c.push() + targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200)( + function() + targetSprite:animate("hurt")(c.pop) + Tree.audio:play(Tree.assets.files.audio.sounds.hurt) + end + ) + end end ---------------------------------------- -- 2.47.2 From e02c221e3116bf84a30e6eaa0a91a13def644712 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Fri, 30 Jan 2026 00:55:08 +0300 Subject: [PATCH 07/10] make all 3 demo spells work with tasks --- lib/spellbook.lua | 43 +++++++++++-------------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 34af175..8be53ac 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -22,7 +22,7 @@ function spell:update(caster, dt) end function spell:draw() end -function spell:cast(caster, target) return true end +function spell:cast(caster, target) return end local walk = setmetatable({ --- @type Deque @@ -55,32 +55,9 @@ function walk:cast(caster, target) return end - local testChar = Tree.level.characters[1]; - - - return function(callback) -- <- вызовется после всех анимаций - local counter = require 'lib.utils.counter' (callback) - counter.push() + return function(callback) 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 ) -- <- callback вызовется после followPath end end @@ -114,9 +91,10 @@ function regenerateMana:cast(caster, target) 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 + print(caster.id, "has regenerated mana and gained initiative") local light = require "lib/character/character".spawn("Light Effect") light:addBehavior { @@ -126,11 +104,12 @@ function regenerateMana:cast(caster, target) } return function(callback) - print(light:has(Tree.behaviors.light).animateColor) - light:has(Tree.behaviors.light):animateColor(Vec3 {})(function() - light:die() - callback() - end) + light:has(Tree.behaviors.light):animateColor(Vec3 {})( + function() + light:die() + end + ) + sprite:animate("hurt")(callback) end end -- 2.47.2 From 403ba5a03f428dbc6a8f217712a184b7f0732de8 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sat, 31 Jan 2026 02:11:55 +0300 Subject: [PATCH 08/10] Add Task.wait to combine multiple tasks into one Add TestRunner for running asynchronous tests with update support Add test for Task.wait to verify concurrent task completion Add set method to Counter for explicit value assignment --- lib/utils/counter.lua | 2 ++ lib/utils/task.lua | 30 +++++++++++++++++++++ main.lua | 4 +++ test/runner.lua | 46 ++++++++++++++++++++++++++++++++ test/task.lua | 61 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 test/runner.lua create mode 100644 test/task.lua diff --git a/lib/utils/counter.lua b/lib/utils/counter.lua index 9e8273f..5f75337 100644 --- a/lib/utils/counter.lua +++ b/lib/utils/counter.lua @@ -4,6 +4,7 @@ --- @field private isAlive boolean --- @field push fun():nil добавить 1 к счетчику --- @field pop fun():nil убавить 1 у счетчика +--- @field set fun(count: integer): nil установить значение на счетчике local counter = {} counter.__index = counter @@ -31,6 +32,7 @@ local function new(onFinish) } t.push = function() counter._push(t) end t.pop = function() counter._pop(t) end + t.set = function(count) t.count = count end return setmetatable(t, counter) end diff --git a/lib/utils/task.lua b/lib/utils/task.lua index a64ba49..009f0e7 100644 --- a/lib/utils/task.lua +++ b/lib/utils/task.lua @@ -34,3 +34,33 @@ --- ``` --- @generic T --- @alias Task fun(callback: fun(value: T): nil): nil + +--- Возвращает новый Task, который завершится после завершения всех переданных `tasks`. +--- +--- Значение созданного Task будет содержать список значений `tasks` в том же порядке. +--- +--- См. также https://api.dart.dev/dart-async/Future/wait.html +--- @generic T +--- @param tasks Task[] +--- @return Task +local function wait(tasks) + local count = #tasks + local results = {} + + return function(callback) + for i, task in ipairs(tasks) do + task( + function(result) + results[i] = result + + count = count - 1 + if count == 0 then callback(results) end + end + ) + end + end +end + +return { + wait = wait, +} diff --git a/main.lua b/main.lua index b2f120c..b5064d8 100644 --- a/main.lua +++ b/main.lua @@ -2,6 +2,8 @@ local character = require "lib/character/character" local testLayout +local TestRunner = require "test.runner" +TestRunner:register(require "test.task") function love.conf(t) t.console = true @@ -67,6 +69,8 @@ end local lt = "0" function love.update(dt) + TestRunner:update(dt) -- закомментировать для отключения тестов + local t1 = love.timer.getTime() Tree.controls:poll() Tree.level.camera:update(dt) -- сначала логика камеры, потому что на нее завязан UI diff --git a/test/runner.lua b/test/runner.lua new file mode 100644 index 0000000..e865e1e --- /dev/null +++ b/test/runner.lua @@ -0,0 +1,46 @@ +--- @class Test +local test = {} +function test:run(complete) end + +function test:update(dt) end + +--- @class TestRunner +--- @field private tests Test[] +--- @field private state "loading" | "running" | "completed" +--- @field private completedCount integer +local runner = {} +runner.tests = {} +runner.state = "loading" +runner.completedCount = 0 + +--- глобальный update для тестов, нужен для тестирования фич, зависимых от времени +function runner:update(dt) + if self.state == "loading" then + print("[TestRunner]: running " .. #self.tests .. " tests") + + for _, t in ipairs(self.tests) do + t:run( + function() + self.completedCount = self.completedCount + 1 + if self.completedCount == #self.tests then + self.state = "completed" + print("[TestRunner]: tests completed") + end + end + ) + end + self.state = "running" + end + + for _, t in ipairs(self.tests) do + if t.update then t:update(dt) end + end +end + +--- добавляет тест для прохождения +--- @param t Test +function runner:register(t) + table.insert(self.tests, t) +end + +return runner diff --git a/test/task.lua b/test/task.lua new file mode 100644 index 0000000..93908bf --- /dev/null +++ b/test/task.lua @@ -0,0 +1,61 @@ +local task = require "lib.utils.task" + +local test = {} + +local t0 +local task1Start, task2Start +local task1Callback, task2Callback + +--- @return Task +local function task1() + return function(callback) + task1Start = love.timer.getTime() + task1Callback = callback + end +end + +--- @return Task +local function task2() + return function(callback) + task2Start = love.timer.getTime() + task2Callback = callback + end +end + +function test:run(complete) + t0 = love.timer.getTime() + + task.wait { + task1(), + task2() + } (function(values) + local tWait = love.timer.getTime() + local dt = tWait - t0 + + local t1 = values[1] + local t2 = values[2] + + assert(type(t1) == "number" and type(t2) == "number") + assert(t2 > t1) + assert(dt >= 2, "dt = " .. dt) + + print("task.wait completed in " .. dt .. " sec", "t1 = " .. t1 - t0, "t2 = " .. t2 - t0) + + complete() + end) +end + +function test:update(dt) + local t = love.timer.getTime() + if task1Start and t - task1Start >= 1 then + task1Callback(t) + task1Start = nil + end + + if task2Start and t - task2Start >= 2 then + task2Callback(t) + task2Start = nil + end +end + +return test -- 2.47.2 From 1986228670bdf3e3af1bc15889512fa0857d457c Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 1 Feb 2026 03:56:18 +0300 Subject: [PATCH 09/10] Refactor spell casts to use task utilities and simplify callbacks Add task.chain utility for chaining asynchronous tasks --- lib/character/behaviors/sprite.lua | 2 +- lib/spellbook.lua | 42 +++++++++++------------------- lib/utils/task.lua | 17 ++++++++++++ test/task.lua | 16 +++++++++++- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/lib/character/behaviors/sprite.lua b/lib/character/behaviors/sprite.lua index 96eac8f..0f6710c 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -70,7 +70,7 @@ function sprite:draw() end --- @return Task -function sprite:animate(state, node) +function sprite:animate(state) return function(callback) if not self.animationGrid[state] then print("[SpriteBehavior]: no animation for '" .. state .. "'") diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 8be53ac..0843174 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,7 +7,7 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async -local Counter = require 'lib.utils.counter' +local task = require 'lib.utils.task' --- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell --- @field tag string @@ -55,11 +55,7 @@ function walk:cast(caster, target) return end - return function(callback) - return caster:has(Tree.behaviors.tiled):followPath(path)( - callback - ) -- <- callback вызовется после followPath - end + return caster:has(Tree.behaviors.tiled):followPath(path) end function walk:update(caster, dt) @@ -103,14 +99,10 @@ function regenerateMana:cast(caster, target) Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), } - return function(callback) - light:has(Tree.behaviors.light):animateColor(Vec3 {})( - function() - light:die() - end - ) - sprite:animate("hurt")(callback) - end + return task.wait { + light:has(Tree.behaviors.light):animateColor(Vec3 {}), + sprite:animate("hurt") + } end local attack = setmetatable({}, spell) @@ -143,20 +135,16 @@ function attack:cast(caster, target) caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end) - return function(callback) - local c = Counter(callback) - - c.push() - sprite:animate("attack")(c.pop) - - c.push() - targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200)( - function() - targetSprite:animate("hurt")(c.pop) + return + task.wait { + sprite:animate("attack"), + task.wait { + task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200), + function() return targetSprite:animate("hurt") end + ), Tree.audio:play(Tree.assets.files.audio.sounds.hurt) - end - ) - end + } + } end ---------------------------------------- diff --git a/lib/utils/task.lua b/lib/utils/task.lua index 009f0e7..04ef2ae 100644 --- a/lib/utils/task.lua +++ b/lib/utils/task.lua @@ -61,6 +61,23 @@ local function wait(tasks) end end + +--- Последовательно объединяет два `Task` в один. +--- @generic T +--- @generic R +--- @param task Task `Task`, который выполнится первым +--- @param onCompleted fun(value: T): Task Конструктор второго `Task`. Принимает результат выполнения первого `Task` +--- @return Task +local function chain(task, onCompleted) + return function(callback) + task(function(value) + local task2 = onCompleted(value) + task2(callback) + end) + end +end + return { wait = wait, + chain = chain } diff --git a/test/task.lua b/test/task.lua index 93908bf..1c25616 100644 --- a/test/task.lua +++ b/test/task.lua @@ -41,7 +41,21 @@ function test:run(complete) print("task.wait completed in " .. dt .. " sec", "t1 = " .. t1 - t0, "t2 = " .. t2 - t0) - complete() + + t0 = love.timer.getTime() + task.chain(task1(), function(value) + t1 = value + assert(t1 - t0 >= 1) + return task2() + end)( + function(value) + t2 = value + assert(t2 - t0 >= 2) + print("task.chain completed in " .. t2 - t0 .. " sec") + + complete() + end + ) end) end -- 2.47.2 From c0bab85b2fbefd75fa3034c19d511c45361f57df Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Mon, 2 Feb 2026 02:33:23 +0300 Subject: [PATCH 10/10] revert "completion.autoRequire": false --- .luarc.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.luarc.json b/.luarc.json index 25bbd47..0bef050 100644 --- a/.luarc.json +++ b/.luarc.json @@ -6,6 +6,5 @@ "love.filesystem.load": "loadfile" }, "workspace.ignoreDir": ["dev_utils"], - "diagnostics.ignoredFiles": "Disable", - "completion.autoRequire": false + "diagnostics.ignoredFiles": "Disable" } -- 2.47.2