Compare commits

...

14 Commits

Author SHA1 Message Date
7f1c31f67e Merge pull request 'feature/task' (#29) from feature/task into main
Reviewed-on: #29
2026-02-02 02:36:30 +03:00
c0bab85b2f revert "completion.autoRequire": false 2026-02-02 02:33:23 +03:00
4f436a3d3f Merge pull request 'fix/death-logic' (#28) from fix/death-logic into main
Reviewed-on: #28
2026-02-02 01:56:51 +03:00
ec290eb18b Merge pull request 'fix: correctly add values to self.__grid instead of the grid class table' (#27) from fix/grid-add-to-self into main
Reviewed-on: #27
2026-02-02 01:06:22 +03:00
958d3bc245 fix: correctly add values to self.__grid instead of the grid class table 2026-02-01 22:57:33 +01:00
1986228670 Refactor spell casts to use task utilities and simplify callbacks
Add task.chain utility for chaining asynchronous tasks
2026-02-01 03:56:18 +03:00
403ba5a03f 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
2026-01-31 02:11:55 +03:00
e02c221e31 make all 3 demo spells work with tasks 2026-01-30 00:55:08 +03:00
59cc0fba0b rewrite sprite:animate, residentsleeper:sleep, attack:cast to use
callback trees
2026-01-30 00:32:05 +03:00
86a599723e 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.
2026-01-29 02:32:58 +03:00
93a4961419 high-order functions my beloved 2026-01-25 07:05:45 +03:00
3f694ccec9 attempt to save our souls (callback hell my beloved)
Co-authored-by: Ivan Yuriev <ivanyr44@gmail.com>
2026-01-24 01:46:05 +03:00
d48f1af1af micro refactoring 2026-01-23 15:57:10 +03:00
9489cd0488 we can move under another (closest!!!!!!!!!) character
cool!!!!
2026-01-23 15:56:36 +03:00
17 changed files with 461 additions and 111 deletions

View File

@ -1,6 +1,5 @@
local easing = require "lib.utils.easing" local easing = require "lib.utils.easing"
--- @alias voidCallback fun(): nil
--- @alias animationRunner fun(node: AnimationNode) --- @alias animationRunner fun(node: AnimationNode)
--- Узел дерева анимаций. --- Узел дерева анимаций.
@ -23,6 +22,7 @@ local easing = require "lib.utils.easing"
--- } --- }
--- }:run() --- }:run()
--- ``` --- ```
--- @deprecated
--- @class AnimationNode --- @class AnimationNode
--- @field count integer --- @field count integer
--- @field run animationRunner --- @field run animationRunner
@ -73,6 +73,7 @@ function animation:update(dt)
end end
end end
--- @deprecated
--- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]} --- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]}
--- @return AnimationNode --- @return AnimationNode
local function new(data) local function new(data)

View File

@ -8,3 +8,6 @@ Tree.behaviors.light = require "character.behaviors.light"
Tree.behaviors.positioned = require "character.behaviors.positioned" Tree.behaviors.positioned = require "character.behaviors.positioned"
Tree.behaviors.tiled = require "character.behaviors.tiled" Tree.behaviors.tiled = require "character.behaviors.tiled"
Tree.behaviors.cursor = require "character.behaviors.cursor" Tree.behaviors.cursor = require "character.behaviors.cursor"
Tree.behaviors.ai = require "lib.character.behaviors.ai"
--- @alias voidCallback fun(): nil

View File

@ -0,0 +1,59 @@
local AnimationNode = require "lib.animation_node"
local easing = require "lib.utils.easing"
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?
--- @field target Vec3?
local behavior = {}
behavior.__index = behavior
behavior.id = "ai"
function behavior.new()
return setmetatable({}, behavior)
end
--- @return Task<nil>
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
return behavior

View File

@ -1,13 +1,17 @@
local AnimationNode = require "lib.animation_node"
local easing = require "lib.utils.easing"
--- @class LightBehavior : Behavior --- @class LightBehavior : Behavior
--- @field intensity number --- @field intensity number
--- @field color Vec3 --- @field color Vec3
--- @field seed integer --- @field seed integer
--- @field colorAnimationNode? AnimationNode --- @field colorAnimationNode? AnimationNode
--- @field private animateColorCallback? fun(): nil
--- @field targetColor? Vec3 --- @field targetColor? Vec3
--- @field sourceColor? Vec3 --- @field sourceColor? Vec3
local behavior = {} local behavior = {}
behavior.__index = behavior behavior.__index = behavior
behavior.id = "light" behavior.id = "light"
---@param values {intensity: number?, color: Vec3?, seed: integer?} ---@param values {intensity: number?, color: Vec3?, seed: integer?}
---@return LightBehavior ---@return LightBehavior
@ -26,11 +30,24 @@ function behavior:update(dt)
self.colorAnimationNode:update(dt) self.colorAnimationNode:update(dt)
end end
function behavior:animateColor(targetColor, animationNode) --- @TODO: refactor
function behavior:animateColor(targetColor)
if self.colorAnimationNode then self.colorAnimationNode:finish() end 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.sourceColor = self.color
self.targetColor = targetColor self.targetColor = targetColor
return function(callback)
self.animateColorCallback = callback
end
end end
function behavior:draw() function behavior:draw()

View File

@ -1,21 +1,37 @@
--- Умеет асинхронно ждать какое-то время (для анимаций) --- Умеет асинхронно ждать какое-то время (для анимаций)
--- @class ResidentSleeperBehavior : Behavior --- @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 = {} local behavior = {}
behavior.__index = behavior behavior.__index = behavior
behavior.id = "residentsleeper" behavior.id = "residentsleeper"
function behavior.new() return setmetatable({}, behavior) end function behavior.new() return setmetatable({}, behavior) end
function behavior:update(dt) function behavior:update(_)
if not self.animationNode then return end if self.state ~= 'running' then return end
self.animationNode:update(dt)
local t = love.timer.getTime()
if t >= self.t0 + self.sleepTime then
self.state = 'finished'
self.callback()
end
end end
--- @param node AnimationNode --- @return Task<nil>
function behavior:sleep(node) function behavior:sleep(ms)
if self.animationNode then self.animationNode:finish() end self.sleepTime = ms / 1000
self.animationNode = node return function(callback)
if self.state == 'running' then
self.callback()
end
self.t0 = love.timer.getTime()
self.callback = callback
self.state = 'running'
end
end end
return behavior return behavior

View File

@ -69,17 +69,20 @@ function sprite:draw()
) )
end end
--- @param node AnimationNode --- @return Task<nil>
function sprite:animate(state, node) function sprite:animate(state)
if not self.animationGrid[state] then return function(callback)
return print("[SpriteBehavior]: no animation for '" .. state .. "'") 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 end
self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED,
function()
self:loop("idle")
node:finish()
end)
self.state = state
end end
function sprite:loop(state) function sprite:loop(state)

View File

@ -5,7 +5,7 @@ local utils = require "lib.utils.utils"
--- @field private runSource? Vec3 точка, из которой бежит персонаж --- @field private runSource? Vec3 точка, из которой бежит персонаж
--- @field private runTarget? Vec3 точка, в которую в данный момент бежит персонаж --- @field private runTarget? Vec3 точка, в которую в данный момент бежит персонаж
--- @field private path? Deque путь, по которому сейчас бежит персонаж --- @field private path? Deque путь, по которому сейчас бежит персонаж
--- @field private animationNode? AnimationNode AnimationNode, с которым связана анимация перемещения --- @field private followPathCallback? fun()
--- @field private t0 number время начала движения --- @field private t0 number время начала движения
--- @field size Vec3 --- @field size Vec3
local behavior = {} local behavior = {}
@ -20,10 +20,8 @@ function behavior.new(size)
end end
--- @param path Deque --- @param path Deque
--- @param animationNode AnimationNode --- @return Task<nil>
function behavior:followPath(path, animationNode) function behavior:followPath(path)
if path:is_empty() then return animationNode:finish() end
self.animationNode = animationNode
self.owner:try(Tree.behaviors.sprite, function(sprite) self.owner:try(Tree.behaviors.sprite, function(sprite)
sprite:loop("run") sprite:loop("run")
end) end)
@ -32,6 +30,10 @@ function behavior:followPath(path, animationNode)
local nextCell = path:peek_front() local nextCell = path:peek_front()
self:runTo(nextCell) self:runTo(nextCell)
path:pop_front() path:pop_front()
return function(callback)
self.followPathCallback = callback
end
end end
--- @param target Vec3 --- @param target Vec3
@ -72,7 +74,10 @@ function behavior:update(dt)
sprite:loop("idle") sprite:loop("idle")
end) end)
self.runTarget = nil self.runTarget = nil
if self.animationNode then self.animationNode:finish() end
if self.followPathCallback then
self.followPathCallback()
end
end end
else -- анимация перемещения не завершена else -- анимация перемещения не завершена
positioned.position = utils.lerp(self.runSource, self.runTarget, fraction) -- линейный интерполятор positioned.position = utils.lerp(self.runSource, self.runTarget, fraction) -- линейный интерполятор

View File

@ -8,7 +8,7 @@ grid.__index = grid
--- adds a value to the grid --- adds a value to the grid
--- @param value any --- @param value any
function grid:add(value) function grid:add(value)
grid[tostring(value.position)] = value self.__grid[tostring(value.position)] = value
end end
--- @param position Vec3 --- @param position Vec3

View File

@ -37,9 +37,16 @@ function selector:update(dt)
if not selectedId then self:select(nil) end if not selectedId then self:select(nil) end
return return
end end
if b.cast:cast(char, mousePosition) then local task = b.cast:cast(char, mousePosition) -- в task функция, которая запускает анимацию спелла
if task then
self:lock() self:lock()
b.state = "running" b.state = "running"
task(
function(_) -- это коллбэк, который сработает по окончании анимации спелла
b:endCast()
end
)
end end
end) end)
end end

View File

@ -34,6 +34,15 @@ function turnOrder:next()
local next = self.pendingQueue:peek() local next = self.pendingQueue:peek()
if not next then return self:endRound() end if not next then return self:endRound() end
self.current = self.pendingQueue:pop() self.current = self.pendingQueue:pop()
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)
end end
--- Меняем местами очередь сходивших и не сходивших (пустую) --- Меняем местами очередь сходивших и не сходивших (пустую)

View File

@ -57,7 +57,7 @@ function endTurnButton:onClick()
end, end,
duration = 1500, duration = 1500,
easing = easing.easeInOutCubic, 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() }:run()
end end

View File

@ -7,23 +7,22 @@
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
--- Да, это Future/Promise/await/async --- Да, это Future/Promise/await/async
local AnimationNode = require "lib.animation_node" local task = require 'lib.utils.task'
local easing = require "lib.utils.easing"
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell --- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell
--- @field tag string --- @field tag string
--- @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 Рисует превью каста, ничего не должна изменять в идеальном мире
--- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать --- @field cast fun(self: Spell, caster: Character, target: Vec3): Task<nil> | nil Вызывается в момент каста, изменяет мир.
local spell = {} local spell = {}
spell.__index = spell spell.__index = spell
spell.tag = "base" spell.tag = "base"
function spell:update(caster, dt) end function spell:update(caster, dt) end
function spell:draw() end function spell:draw() end
function spell:cast(caster, target) return true end function spell:cast(caster, target) return end
local walk = setmetatable({ local walk = setmetatable({
--- @type Deque --- @type Deque
@ -35,30 +34,28 @@ function walk:cast(caster, target)
if not caster:try(Tree.behaviors.stats, function(stats) if not caster:try(Tree.behaviors.stats, function(stats)
return stats.mana >= 2 return stats.mana >= 2
end) then end) then
return false return
end 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() path:pop_front()
if path:is_empty() then return false end if path:is_empty() then
print("[Walk]: the path is empty", initialPos, target)
for p in path:values() do print(p) end return
end
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)
end) end)
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
if not sprite then return true end assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE")
AnimationNode { if not sprite then
function(node) return
caster:has(Tree.behaviors.tiled):followPath(path, node) end
end,
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
}:run()
return true return caster:has(Tree.behaviors.tiled):followPath(path)
end end
function walk:update(caster, dt) function walk:update(caster, dt)
@ -90,9 +87,10 @@ function regenerateMana:cast(caster, target)
stats.mana = 10 stats.mana = 10
stats.initiative = stats.initiative + 10 stats.initiative = stats.initiative + 10
end) end)
print(caster.id, "has regenerated mana and gained initiative")
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
if not sprite then return true end 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") local light = require "lib/character/character".spawn("Light Effect")
light:addBehavior { light:addBehavior {
@ -100,26 +98,11 @@ function regenerateMana:cast(caster, target)
Tree.behaviors.residentsleeper.new(), Tree.behaviors.residentsleeper.new(),
Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), 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)
end,
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end
}:run()
AnimationNode { return task.wait {
function(node) light:has(Tree.behaviors.light):animateColor(Vec3 {}),
light:has(Tree.behaviors.light):animateColor(Vec3 {}, node) sprite:animate("hurt")
end, }
easing = easing.easeInQuad,
duration = 800,
onEnd = function() light:die() end
}:run()
return true
end end
local attack = setmetatable({}, spell) local attack = setmetatable({}, spell)
@ -131,7 +114,7 @@ function attack:cast(caster, target)
print("dist:", dist) print("dist:", dist)
return dist > 2 return dist > 2
end) then end) then
return false return
end end
caster:try(Tree.behaviors.stats, function(stats) caster:try(Tree.behaviors.stats, function(stats)
@ -140,7 +123,7 @@ function attack:cast(caster, target)
--- @type Character --- @type Character
local targetCharacterId = Tree.level.characterGrid:get(target) 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] local targetCharacter = Tree.level.characters[targetCharacterId]
targetCharacter:try(Tree.behaviors.stats, function(stats) targetCharacter:try(Tree.behaviors.stats, function(stats)
stats.hp = stats.hp - 4 stats.hp = stats.hp - 4
@ -148,43 +131,20 @@ function attack:cast(caster, target)
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
local targetSprite = targetCharacter: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) caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end)
AnimationNode { return
onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, task.wait {
children = { sprite:animate("attack"),
AnimationNode { task.wait {
function(node) task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200),
sprite:animate("attack", node) function() return targetSprite:animate("hurt") end
end ),
}, Tree.audio:play(Tree.assets.files.audio.sounds.hurt)
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, settings)
end
}
}
} }
} }
}:run()
return true
end end
---------------------------------------- ----------------------------------------
@ -199,6 +159,7 @@ local spellbook = {
function spellbook.of(list) function spellbook.of(list)
local spb = {} local spb = {}
for i, sp in ipairs(list) do for i, sp in ipairs(list) do
print(i)
spb[i] = setmetatable({}, { __index = sp }) spb[i] = setmetatable({}, { __index = sp })
end end
return spb return spb

40
lib/utils/counter.lua Normal file
View File

@ -0,0 +1,40 @@
--- @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 у счетчика
--- @field set fun(count: integer): nil установить значение на счетчике
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
t.set = function(count) t.count = count end
return setmetatable(t, counter)
end
return new

83
lib/utils/task.lua Normal file
View File

@ -0,0 +1,83 @@
--- Обобщенная асинхронная функция
---
--- Использование в общих чертах выглядит так:
--- ```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<T> 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<T[]>
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
--- Последовательно объединяет два `Task` в один.
--- @generic T
--- @generic R
--- @param task Task<T> `Task`, который выполнится первым
--- @param onCompleted fun(value: T): Task<R> Конструктор второго `Task`. Принимает результат выполнения первого `Task`
--- @return Task<R>
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
}

View File

@ -2,6 +2,8 @@
local character = require "lib/character/character" local character = require "lib/character/character"
local testLayout local testLayout
local TestRunner = require "test.runner"
TestRunner:register(require "test.task")
function love.conf(t) function love.conf(t)
t.console = true t.console = true
@ -23,16 +25,37 @@ function love.load()
Tree.behaviors.shadowcaster.new(), Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new() Tree.behaviors.spellcaster.new()
}, },
character.spawn("Baris") character.spawn("Foodor")
:addBehavior { :addBehavior {
Tree.behaviors.residentsleeper.new(), Tree.behaviors.residentsleeper.new(),
Tree.behaviors.stats.new(nil, nil, 1), Tree.behaviors.stats.new(nil, nil, 1),
Tree.behaviors.positioned.new(Vec3 { 5, 5 }), Tree.behaviors.positioned.new(Vec3 { 4, 3 }),
Tree.behaviors.tiled.new(), Tree.behaviors.tiled.new(),
Tree.behaviors.sprite.new(Tree.assets.files.sprites.character), Tree.behaviors.sprite.new(Tree.assets.files.sprites.character),
Tree.behaviors.shadowcaster.new(), Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new() Tree.behaviors.spellcaster.new()
}, },
character.spawn("Foodor")
:addBehavior {
Tree.behaviors.residentsleeper.new(),
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),
Tree.behaviors.shadowcaster.new(),
Tree.behaviors.spellcaster.new()
},
character.spawn("Baris")
:addBehavior {
Tree.behaviors.residentsleeper.new(),
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.ai.new()
},
} }
for id, _ in pairs(chars) do for id, _ in pairs(chars) do
@ -46,6 +69,8 @@ end
local lt = "0" local lt = "0"
function love.update(dt) function love.update(dt)
TestRunner:update(dt) -- закомментировать для отключения тестов
local t1 = love.timer.getTime() local t1 = love.timer.getTime()
Tree.controls:poll() Tree.controls:poll()
Tree.level.camera:update(dt) -- сначала логика камеры, потому что на нее завязан UI Tree.level.camera:update(dt) -- сначала логика камеры, потому что на нее завязан UI

46
test/runner.lua Normal file
View File

@ -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

75
test/task.lua Normal file
View File

@ -0,0 +1,75 @@
local task = require "lib.utils.task"
local test = {}
local t0
local task1Start, task2Start
local task1Callback, task2Callback
--- @return Task<number>
local function task1()
return function(callback)
task1Start = love.timer.getTime()
task1Callback = callback
end
end
--- @return Task<number>
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)
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
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