From 4277c6c3100360d9f85658580c51d7cef00200e1 Mon Sep 17 00:00:00 2001 From: Kitsune-Jesus Date: Thu, 5 Feb 2026 09:06:51 +0100 Subject: [PATCH] refactor: replace AnimationNode with Task system (tweens/async) --- assets/shaders/divine_ripple.glsl | 47 ++++++++++++++ lib/animation_node.lua | 103 ------------------------------ lib/audio.lua | 59 +++++++++++------ lib/character/behaviors/ai.lua | 2 - lib/character/behaviors/light.lua | 34 ++-------- lib/character/behaviors/tiled.lua | 67 +++++++------------ lib/level/camera.lua | 1 + lib/simple_ui/level/cpanel.lua | 42 ++++-------- lib/simple_ui/level/end_turn.lua | 13 ++-- lib/utils/task.lua | 45 +++++++++---- 10 files changed, 166 insertions(+), 247 deletions(-) create mode 100644 assets/shaders/divine_ripple.glsl delete mode 100644 lib/animation_node.lua diff --git a/assets/shaders/divine_ripple.glsl b/assets/shaders/divine_ripple.glsl new file mode 100644 index 0000000..d45debe --- /dev/null +++ b/assets/shaders/divine_ripple.glsl @@ -0,0 +1,47 @@ +extern vec2 center; +extern number time; +extern number intensity; +extern vec2 screenSize; + +// Hexagon grid logic +// Returns distance to nearest hex center +float hexDist(vec2 p) { + p = abs(p); + float c = dot(p, normalize(vec2(1.0, 1.73))); + c = max(c, p.x); + return c; +} + +vec4 effect(vec4 color, Image texture, vec2 texture_coords, vec2 screen_coords) { + // Normalize coordinates to -1..1, correcting for aspect ratio + vec2 aspect = vec2(screenSize.x / screenSize.y, 1.0); + vec2 uv = (screen_coords / screenSize.y) - (center / screenSize.y); + + // Wave parameters + float dist = length(uv); + float speed = 5.0; + float waveWidth = 0.1; + float decay = 1.0 - smoothstep(0.0, 1.5, dist); // Decay over distance + + // Calculate wave pulse + float wavePhase = time * speed; + float pulse = smoothstep(waveWidth, 0.0, abs(dist - mod(wavePhase, 2.0))); + + // Hex grid pattern (visual only) + vec2 hexUV = screen_coords * 0.05; // Scale grid + // Basic hex grid approximation + vec2 q = vec2(hexUV.x * 2.0/3.0, hexUV.y); + + // Distortion + vec2 distort = normalize(uv) * pulse * intensity * 0.02 * decay; + vec2 finalUV = texture_coords - distort; + + // Sample texture with distortion + vec4 texColor = Texel(texture, finalUV); + + // Add divine glow at the wavefront + vec4 glowColor = vec4(1.0, 0.8, 0.4, 1.0); // Gold/Kitsune-fire color + texColor += glowColor * pulse * decay * 0.5; + + return texColor * color; +} diff --git a/lib/animation_node.lua b/lib/animation_node.lua deleted file mode 100644 index ece3aa7..0000000 --- a/lib/animation_node.lua +++ /dev/null @@ -1,103 +0,0 @@ -local easing = require "lib.utils.easing" - ---- @alias animationRunner fun(node: AnimationNode) - ---- Узел дерева анимаций. ---- ---- Отслеживает завершение всех анимаций всех дочерних узлов и оповещает вышестоящий узел. ---- ---- Дочерние узлы одного уровня запускают свою анимацию одновременно после завершения анимации родителя. ---- Example: ---- ```lua ---- AnimationNode { ---- function (node) residentsleeper:sleep(1000, node) end, -- must pass itself down as the parameter ---- onEnd = function() print("completed") end, ---- children = { -- children run in parallel after the parent animation is completed ---- AnimationNode { ---- function (node) sprite:animate("attack", node) end ---- }, ---- AnimationNode { ---- function (node) other_sprite:animate("hurt", node) end ---- }, ---- } ---- }:run() ---- ``` ---- @deprecated ---- @class AnimationNode ---- @field count integer ---- @field run animationRunner ---- @field parent AnimationNode? ---- @field children AnimationNode[] ---- @field finish voidCallback ---- @field onEnd voidCallback? ---- @field duration number продолжительность в миллисекундах ---- @field easing ease функция смягчения ---- @field t number прогресс анимации ---- @field state "running" | "waiting" | "finished" -local animation = {} -animation.__index = animation - ---- Регистрация завершения дочерней анимации -function animation:bubbleUp() - self.count = self.count - 1 - if self.count > 0 then return end - self.state = "finished" - if self.onEnd then self.onEnd() end - if self.parent then self.parent:bubbleUp() end -end - ---- @param children AnimationNode[] ---- Запланировать анимации после текущей, которые запустятся одновременно друг с другом -function animation:chain(children) - for _, child in ipairs(children) do - child.parent = self - table.insert(self.children, child) - self.count = self.count + 1 - end - return self -end - ---- Возвращает текущий прогресс анимации с учетом смягчения -function animation:getValue() - return self.easing(self.t) -end - -function animation:update(dt) - if self.state ~= "running" then return end - - if self.t < 1 then - self.t = self.t + dt * 1000 / self.duration -- в знаменателе продолжительность анимации в секундах - else - self.t = 1 - self:finish() - end -end - ---- @deprecated ---- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]} ---- @return AnimationNode -local function new(data) - local t = setmetatable({}, animation) - t.run = data[1] or function(self) - self:finish() - end - t.onEnd = data.onEnd - t.count = 1 -- своя анимация - t.children = {} - t:chain(data.children or {}) - t.duration = data.duration or 1000 - t.easing = data.easing or easing.linear - t.t = 0 - t.state = "running" - t.finish = function() - if t.state ~= "running" then return end - t.state = "waiting" - t:bubbleUp() - for _, anim in ipairs(t.children) do - anim:run() - end - end - return t -end - -return new diff --git a/lib/audio.lua b/lib/audio.lua index a480272..f6fa637 100644 --- a/lib/audio.lua +++ b/lib/audio.lua @@ -1,5 +1,5 @@ +local task = require "lib.utils.task" local ease = require "lib.utils.easing" -local AnimationNode = require "lib.animation_node" local EFFECTS_SUPPORTED = love.audio.isEffectsSupported() @@ -9,7 +9,6 @@ local EFFECTS_SUPPORTED = love.audio.isEffectsSupported() --- @field musicVolume number --- @field soundVolume number --- @field looped boolean ---- @field animationNode AnimationNode? --- @field from love.Source? --- @field to love.Source? audio = {} @@ -25,11 +24,11 @@ local function new(musicVolume, soundVolume) end function audio:update(dt) - if not self.animationNode then return end - self.from:setVolume(self.musicVolume - self.animationNode:getValue() * self.musicVolume) - self.to:setVolume(self.animationNode:getValue() * self.musicVolume) - self.animationNode:update(dt) - -- print(self.animationNode.t) + if self.fader then + local t = self.fader.value + if self.from then self.from:setVolume(self.musicVolume * (1 - t)) end + if self.to then self.to:setVolume(self.musicVolume * t) end + end end --- if from is nil, than we have fade in to; @@ -45,19 +44,39 @@ function audio:crossfade(from, to, ms) to:setVolume(0) self.from = from self.to = to - self.animationNode = AnimationNode { - function(node) end, - onEnd = function() - self.from:setVolume(0) - self.to:setVolume(self.musicVolume) - self.from:stop() - self.animationNode = nil - print("[Audio]: Crossfade done") - end, - duration = ms or 1000, - easing = ease.easeOutCubic, - } - self.animationNode:run() + + -- Using a dummy object to tween a value from 0 to 1 + local fade = { value = 0 } + + task.tween(fade, { value = 1 }, ms or 1000, ease.easeOutCubic)(function() + self.from:setVolume(0) + self.to:setVolume(self.musicVolume) + self.from:stop() + print("[Audio]: Crossfade done") + end) + + -- We need a custom update loop for the volume during the tween + -- Since task.tween updates properties, we can use a "monitor" task or just rely on `update` if we expose the task? + -- Actually, task.tween updates `fade.value` every frame. + -- We need to apply this value to the audio sources every frame. + -- BUT task.tween doesn't provide an "onUpdate" callback easily. + -- Workaround: create a task that runs every frame? Or modify task.tween? + + -- Let's check task.lua... it updates properties on the target object. + -- So `fade.value` will change. We need to apply it. + -- We can hook into `update` or use a metatable on `fade`? + + -- Let's use a "reactive" table with metatable setters? Overkill. + -- Simplest way: Add `onUpdate` to task.tween or just use `audio:update` to poll `fade.value`? + -- But `fade` is local. + + -- Better approach: Tween `self` properties? No, volume is on Source userdate. + -- Let's add a `fader` object to `self` that we tween, and in `update` we apply it. + + self.fader = { value = 0 } + task.tween(self.fader, { value = 1 }, ms or 1000, ease.easeOutCubic)(function() + self.fader = nil + end) end --- @param source love.Source diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 7c52624..5aea6d3 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -1,4 +1,3 @@ -local AnimationNode = require "lib.animation_node" local easing = require "lib.utils.easing" local function closestCharacter(char) @@ -22,7 +21,6 @@ local function closestCharacter(char) end --- @class AIBehavior : Behavior ---- @field animationNode AnimationNode? --- @field target Vec3? local behavior = {} behavior.__index = behavior diff --git a/lib/character/behaviors/light.lua b/lib/character/behaviors/light.lua index 16301e4..acf67f0 100644 --- a/lib/character/behaviors/light.lua +++ b/lib/character/behaviors/light.lua @@ -1,14 +1,10 @@ -local AnimationNode = require "lib.animation_node" -local easing = require "lib.utils.easing" +local task = require "lib.utils.task" --- @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 +--- @field private animateColorTask? Task local behavior = {} behavior.__index = behavior behavior.id = "light" @@ -24,30 +20,12 @@ function behavior.new(values) end function behavior:update(dt) - if not self.colorAnimationNode then return end - local delta = self.targetColor - self.sourceColor - self.color = self.sourceColor + delta * self.colorAnimationNode:getValue() - self.colorAnimationNode:update(dt) + -- All logic moved to tasks end ---- @TODO: refactor -function behavior:animateColor(targetColor) - if self.colorAnimationNode then self.colorAnimationNode:finish() end - 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 +function behavior:animateColor(targetColor, duration, easing) + -- If there's support for canceling tasks, we should do it here + return task.tween(self, { color = targetColor }, duration or 800, easing) end function behavior:draw() diff --git a/lib/character/behaviors/tiled.lua b/lib/character/behaviors/tiled.lua index b7b937b..420ccbd 100644 --- a/lib/character/behaviors/tiled.lua +++ b/lib/character/behaviors/tiled.lua @@ -1,12 +1,8 @@ local utils = require "lib.utils.utils" +local task = require "lib.utils.task" --- Отвечает за перемещение по тайлам --- @class TiledBehavior : Behavior ---- @field private runSource? Vec3 точка, из которой бежит персонаж ---- @field private runTarget? Vec3 точка, в которую в данный момент бежит персонаж ---- @field private path? Deque путь, по которому сейчас бежит персонаж ---- @field private followPathCallback? fun() ---- @field private t0 number время начала движения --- @field size Vec3 local behavior = {} behavior.__index = behavior @@ -22,29 +18,33 @@ end --- @param path Deque --- @return Task function behavior:followPath(path) + if path:is_empty() then return task.fromValue(nil) end + self.owner:try(Tree.behaviors.sprite, function(sprite) sprite:loop("run") end) - self.path = path; - ---@type Vec3 - local nextCell = path:peek_front() - self:runTo(nextCell) - path:pop_front() - return function(callback) - self.followPathCallback = callback + -- Рекурсивная функция для прохода по пути + local function nextStep() + if path:is_empty() then + self.owner:try(Tree.behaviors.sprite, function(sprite) + sprite:loop("idle") + end) + return task.fromValue(nil) + end + + local nextCell = path:pop_front() + return task.chain(self:runTo(nextCell), nextStep) end + + return nextStep() end --- @param target Vec3 +--- @return Task function behavior:runTo(target) local positioned = self.owner:has(Tree.behaviors.positioned) - if not positioned then return end - - self.t0 = love.timer.getTime() - self.runTarget = target - - self.runSource = positioned.position + if not positioned then return task.fromValue(nil) end self.owner:try(Tree.behaviors.sprite, function(sprite) @@ -55,34 +55,15 @@ function behavior:runTo(target) end end ) + + local distance = target:subtract(positioned.position):length() + local duration = distance * 500 -- 500ms per unit + + return task.tween(positioned, { position = target }, duration) end function behavior:update(dt) - if self.runTarget then - local positioned = self.owner:has(Tree.behaviors.positioned) - if not positioned then return end - - local delta = love.timer.getTime() - self.t0 or love.timer.getTime() - local fraction = delta / - (0.5 * self.runTarget:subtract(self.runSource):length()) -- бежим одну клетку за 500 мс, по диагонали больше - if fraction >= 1 then -- анимация перемещена завершена - positioned.position = self.runTarget - if not self.path:is_empty() then -- еще есть, куда бежать - self:runTo(self.path:pop_front()) - else -- мы добежали до финальной цели - self.owner:try(Tree.behaviors.sprite, function(sprite) - sprite:loop("idle") - end) - self.runTarget = nil - - if self.followPathCallback then - self.followPathCallback() - end - end - else -- анимация перемещения не завершена - positioned.position = utils.lerp(self.runSource, self.runTarget, fraction) -- линейный интерполятор - end - end + -- Logic moved to tasks end return behavior diff --git a/lib/level/camera.lua b/lib/level/camera.lua index f2bb368..20bb0e2 100644 --- a/lib/level/camera.lua +++ b/lib/level/camera.lua @@ -1,6 +1,7 @@ local Vec3 = require "lib.utils.vec3" local utils = require "lib.utils.utils" local task = require "lib.utils.task" +local easing = require "lib.utils.easing" local EPSILON = 0.001 diff --git a/lib/simple_ui/level/cpanel.lua b/lib/simple_ui/level/cpanel.lua index c04fe58..2e489f9 100644 --- a/lib/simple_ui/level/cpanel.lua +++ b/lib/simple_ui/level/cpanel.lua @@ -1,5 +1,5 @@ +local task = require "lib.utils.task" local easing = require "lib.utils.easing" -local AnimationNode = require "lib.animation_node" local Element = require "lib.simple_ui.element" local Rect = require "lib.simple_ui.rect" local SkillRow = require "lib.simple_ui.level.skill_row" @@ -7,7 +7,8 @@ local Bars = require "lib.simple_ui.level.bottom_bars" local EndTurnButton = require "lib.simple_ui.level.end_turn" --- @class CharacterPanel : UIElement ---- @field animationNode AnimationNode +--- @field animationTask Task +--- @field alpha number --- @field state "show" | "idle" | "hide" --- @field skillRow SkillRow --- @field bars BottomBars @@ -21,41 +22,26 @@ function characterPanel.new(characterId) t.skillRow = SkillRow(characterId) t.bars = Bars(characterId) t.endTurnButton = EndTurnButton {} + t.alpha = 0 -- starts hidden/animating return setmetatable(t, characterPanel) end function characterPanel:show() - AnimationNode { - function(animationNode) - if self.animationNode then self.animationNode:finish() end - self.animationNode = animationNode - self.state = "show" - end, - duration = 300, - onEnd = function() - self.state = "idle" - end, - easing = easing.easeOutCubic - }:run() + self.state = "show" + self.animationTask = task.tween(self, { alpha = 1 }, 300, easing.easeOutCubic) + self.animationTask(function() self.state = "idle" end) end function characterPanel:hide() - AnimationNode { - function(animationNode) - if self.animationNode then self.animationNode:finish() end - self.animationNode = animationNode - self.state = "hide" - end, - duration = 300, - easing = easing.easeOutCubic - }:run() + self.state = "hide" + self.animationTask = task.tween(self, { alpha = 0 }, 300, easing.easeOutCubic) end --- @type love.Canvas local characterPanelCanvas; function characterPanel:update(dt) - if self.animationNode then self.animationNode:update(dt) end + -- Tasks update automatically via task.update(dt) in main.lua self.skillRow:update(dt) self.bars.bounds = Rect { width = self.skillRow.bounds.width, @@ -82,14 +68,8 @@ function characterPanel:update(dt) end --- анимация появления - local alpha = 1 - if self.state == "show" then - alpha = self.animationNode:getValue() - elseif self.state == "hide" then - alpha = 1 - self.animationNode:getValue() - end local revealShader = Tree.assets.files.shaders.reveal - revealShader:send("t", alpha) + revealShader:send("t", self.alpha) end function characterPanel:draw() diff --git a/lib/simple_ui/level/end_turn.lua b/lib/simple_ui/level/end_turn.lua index ddc0c0b..53a072f 100644 --- a/lib/simple_ui/level/end_turn.lua +++ b/lib/simple_ui/level/end_turn.lua @@ -1,5 +1,5 @@ local Element = require "lib.simple_ui.element" -local AnimationNode = require "lib.animation_node" +local task = require "lib.utils.task" local easing = require "lib.utils.easing" --- @class EndTurnButton : UIElement @@ -51,14 +51,9 @@ function endTurnButton:onClick() local playing = Tree.level.characters[cid] if not playing:has(Tree.behaviors.positioned) then return end - AnimationNode { - function(node) - Tree.level.camera:animateTo(playing:has(Tree.behaviors.positioned).position, node) - end, - duration = 1500, - easing = easing.easeInOutCubic, - onEnd = function() if not playing:has(Tree.behaviors.ai) then Tree.level.selector:select(cid) end end - }:run() + Tree.level.camera:animateTo(playing:has(Tree.behaviors.positioned).position, 1500, easing.easeInOutCubic)( + function() if not playing:has(Tree.behaviors.ai) then Tree.level.selector:select(cid) end end + ) end return function(values) diff --git a/lib/utils/task.lua b/lib/utils/task.lua index ae71632..2593342 100644 --- a/lib/utils/task.lua +++ b/lib/utils/task.lua @@ -6,7 +6,12 @@ local easing_lib = require "lib.utils.easing" --- @alias Task fun(callback: fun(value: T): nil): nil local task = {} -local activeTweens = {} +local activeTweens = {} -- list of tweens +-- We also need a way to track tweens by target to support cancellation/replacement +-- Let's stick to a simple list for update, but maybe add a helper to find by target? +-- Or, just allow multiple tweens per target (which is valid for different properties). +-- But if we animate the SAME property, we have a conflict. +-- For now, let's just add task.cancel(target) which kills ALL tweens for that target. --- Внутренний хелпер для интерполяции (числа или Vec3) local function lerp(a, b, t) @@ -24,17 +29,22 @@ end function task.update(dt) for i = #activeTweens, 1, -1 do local t = activeTweens[i] - t.elapsed = t.elapsed + dt * 1000 - local progress = math.min(t.elapsed / t.duration, 1) - local value = t.easing(progress) - - for key, targetValue in pairs(t.properties) do - t.target[key] = lerp(t.initial[key], targetValue, value) - end - - if progress >= 1 then + if not t.cancelled then + t.elapsed = t.elapsed + dt * 1000 + local progress = math.min(t.elapsed / t.duration, 1) + local value = t.easing(progress) + + for key, targetValue in pairs(t.properties) do + t.target[key] = lerp(t.initial[key], targetValue, value) + end + + if progress >= 1 then + t.completed = true + table.remove(activeTweens, i) + if t.resolve then t.resolve() end + end + else table.remove(activeTweens, i) - if t.resolve then t.resolve() end end end end @@ -57,13 +67,26 @@ function task.completer() return c, future end +--- Отменяет все активные твины для указанного объекта. +--- @param target table +function task.cancel(target) + for _, t in ipairs(activeTweens) do + if t.target == target then + t.cancelled = true + end + end +end + --- Создает таску, которая плавно меняет свойства объекта. +--- Автоматически отменяет предыдущие твины для этого объекта (чтобы не было конфликтов). --- @param target table Объект, свойства которого меняем --- @param properties table Набор конечных значений { key = value } --- @param duration number Длительность в мс --- @param easing function? Функция смягчения (по умолчанию linear) --- @return Task function task.tween(target, properties, duration, easing) + task.cancel(target) -- Cancel previous animations on this target + local initial = {} for k, _ in pairs(properties) do initial[k] = target[k]