diff --git a/lib/animation_node.lua b/lib/animation_node.lua index e3f5aff..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) --- Узел дерева анимаций. @@ -23,6 +22,7 @@ local easing = require "lib.utils.easing" --- } --- }:run() --- ``` +--- @deprecated --- @class AnimationNode --- @field count integer --- @field run animationRunner @@ -73,6 +73,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/annotations.lua b/lib/annotations.lua index 6a0d688..a6be927 100644 --- a/lib/annotations.lua +++ b/lib/annotations.lua @@ -8,3 +8,6 @@ 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" + +--- @alias voidCallback fun(): nil diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua new file mode 100644 index 0000000..7c52624 --- /dev/null +++ b/lib/character/behaviors/ai.lua @@ -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 +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 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/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..0f6710c 100644 --- a/lib/character/behaviors/sprite.lua +++ b/lib/character/behaviors/sprite.lua @@ -69,17 +69,20 @@ function sprite:draw() ) end ---- @param node AnimationNode -function sprite:animate(state, node) - if not self.animationGrid[state] then - return print("[SpriteBehavior]: no animation for '" .. state .. "'") +--- @return Task +function sprite:animate(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/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 a5d8f98..3c60a6f 100644 --- a/lib/level/selector.lua +++ b/lib/level/selector.lua @@ -37,9 +37,16 @@ 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) -- в task функция, которая запускает анимацию спелла + if task then 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 f03c836..33200e6 100644 --- a/lib/level/turn_order.lua +++ b/lib/level/turn_order.lua @@ -34,6 +34,15 @@ 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) + Tree.level.selector:lock() + ai:makeTurn()(function() + Tree.level.selector:unlock() + 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 a4ec146..0843174 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,23 +7,22 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async -local AnimationNode = require "lib.animation_node" -local easing = require "lib.utils.easing" +local task = require 'lib.utils.task' --- @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): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать -local spell = {} -spell.__index = spell -spell.tag = "base" +--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task | 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 +function spell:cast(caster, target) return end local walk = setmetatable({ --- @type Deque @@ -35,30 +34,28 @@ 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) + 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 false 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 true end - AnimationNode { - function(node) - caster:has(Tree.behaviors.tiled):followPath(path, node) - end, - onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, - }:run() + assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE") + if not sprite then + return + end - return true + return caster:has(Tree.behaviors.tiled):followPath(path) end function walk:update(caster, dt) @@ -90,9 +87,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 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") light:addBehavior { @@ -100,26 +98,11 @@ 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) - 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 task.wait { + light:has(Tree.behaviors.light):animateColor(Vec3 {}), + sprite:animate("hurt") + } end local attack = setmetatable({}, spell) @@ -131,7 +114,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) @@ -140,7 +123,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 @@ -148,43 +131,20 @@ 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, settings) - end - } - } + 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) } } - }:run() - - return true end ---------------------------------------- @@ -199,6 +159,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/lib/utils/counter.lua b/lib/utils/counter.lua new file mode 100644 index 0000000..5f75337 --- /dev/null +++ b/lib/utils/counter.lua @@ -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 diff --git a/lib/utils/task.lua b/lib/utils/task.lua new file mode 100644 index 0000000..04ef2ae --- /dev/null +++ b/lib/utils/task.lua @@ -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 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 + + +--- Последовательно объединяет два `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/main.lua b/main.lua index 3cd3ddd..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 @@ -23,16 +25,37 @@ function love.load() Tree.behaviors.shadowcaster.new(), Tree.behaviors.spellcaster.new() }, - character.spawn("Baris") + character.spawn("Foodor") :addBehavior { Tree.behaviors.residentsleeper.new(), 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.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, 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 @@ -46,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..1c25616 --- /dev/null +++ b/test/task.lua @@ -0,0 +1,75 @@ +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) + + + 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