From d84fc4a7c2462c141b592080d51feb446c579ee9 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 03:53:27 +0300 Subject: [PATCH] Add path preview and refactor spells with new Spell API - Add path preview support to Spell with update and draw methods - Refactor spell:cast to always return a Task - Simplify spell.new constructor and apply distance constraint uniformly - Replace walk spell with new Spell-based implementation supporting path preview - Remove debug print from target_test.lua --- lib/spell/spell.lua | 55 ++++++++++++++++++------ lib/spell/target_test.lua | 1 - lib/spellbook.lua | 89 +++++++++++++-------------------------- 3 files changed, 72 insertions(+), 73 deletions(-) diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 143a456..e387a2d 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -1,5 +1,6 @@ local Query = require "lib.spell.target_query" local targetTest = require "lib.spell.target_test" +local task = require "lib.utils.task" --- @alias SpellPreview "default" Подсветка возможных целей --- | "path" Подсветка пути до цели @@ -13,7 +14,7 @@ local targetTest = require "lib.spell.target_test" --- @field distance? integer Сторона квадрата с центром в позиции кастера, в пределах которого должна находиться цель, либо отсутствие ограничения --- @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 Вызывается в момент каста, изменяет мир. +--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task Вызывается в момент каста, изменяет мир. local spell = {} spell.__index = spell spell.tag = "spell_base" @@ -22,35 +23,63 @@ spell.baseCooldown = 1 spell.targetQuery = Query(targetTest.any) spell.previewType = "default" -function spell:update(caster, dt) end +function spell:update(caster, dt) + if self.previewType == "path" then + local charPos = caster:has(Tree.behaviors.positioned).position:floor() + --- @type Vec3 + local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor() + if self.targetQuery.test(caster, mpos) then + self.path = require "lib.pathfinder" (charPos, mpos) + else + self.path = nil + end + end +end -function spell:draw() end +function spell:draw() + if self.previewType == "path" then + local path = self.path --[[@as Deque?]] + if not path then return end + --- Это отрисовка пути персонажа к мышке + Tree.level.camera:attach() + love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) + love.graphics.setColor(0.6, 0.75, 0.5) + for p in path:values() do + love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1) + end + love.graphics.setCanvas() + Tree.level.camera:detach() + love.graphics.setColor(1, 1, 1) + end +end -function spell:cast(caster, target) return end +function spell:cast(caster, target) return task.fromValue() end --- Конструктор [Spell] ---- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task} +--- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery?, previewType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task} --- @return Spell function spell.new(data) - local newSpell = { + local newSpell = setmetatable({ tag = data.tag, baseCost = data.baseCost, baseCooldown = data.baseCooldown, targetQuery = data.targetQuery, + previewType = data.previewType, distance = data.distance - } + }, spell) + + newSpell.targetQuery = newSpell.distance + and newSpell.targetQuery:intersect(Query(targetTest.distance(newSpell.distance))) + or newSpell.targetQuery function newSpell:cast(caster, target) - local targetQuery = self.distance - and self.targetQuery:intersect(Query(targetTest.distance(self.distance))) - or self.targetQuery - if not targetQuery.test(caster, target) then return end -- проверка корректности цели + if not self.targetQuery.test(caster, target) then return task.fromValue() end -- проверка корректности цели -- проверка на достаточное количество маны if caster:try(Tree.behaviors.stats, function(stats) return stats.mana < self.baseCost end) then - return + return task.fromValue() end caster:try(Tree.behaviors.stats, function(stats) @@ -60,7 +89,7 @@ function spell.new(data) return data.onCast(caster, target) end - return setmetatable(newSpell, spell) + return newSpell end return spell diff --git a/lib/spell/target_test.lua b/lib/spell/target_test.lua index 0f99146..ee7a3ed 100644 --- a/lib/spell/target_test.lua +++ b/lib/spell/target_test.lua @@ -20,7 +20,6 @@ return { return caster:try(Tree.behaviors.positioned, function(p) local dist = math.max(math.abs(p.position.x - targetPosition.x), math.abs(p.position.y - targetPosition.y)) - print("dist:", dist) return dist <= radius end) end diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 30e62a6..0a1cc0b 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,66 +7,37 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async -local task = require 'lib.utils.task' -local spell = require 'lib.spell.spell' -local targetTest = require 'lib.spell.target_test' -local Query = require "lib.spell.target_query" -local easing = require "lib.utils.easing" +local task = require 'lib.utils.task' +local spell = require 'lib.spell.spell' +local targetTest = require 'lib.spell.target_test' +local Query = require "lib.spell.target_query" +local easing = require "lib.utils.easing" -local walk = setmetatable({ - --- @type Deque - path = nil -}, spell) -walk.tag = "dev_move" +local walk = spell.new { + tag = "dev_move", + previewType = "path", + baseCooldown = 1, + baseCost = 2, + targetQuery = Query(targetTest.any):exclude(Query(targetTest.character)), + distance = 3, + onCast = function(caster, 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 + print("[Walk]: the path is empty", initialPos, target) + return task.fromValue() + end -function walk:cast(caster, target) - if not caster:try(Tree.behaviors.stats, function(stats) - return stats.mana >= 2 - end) then - return + local sprite = caster:has(Tree.behaviors.sprite) + assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE") + if not sprite then + return task.fromValue() + end + + return caster:has(Tree.behaviors.tiled):followPath(path) end - - local initialPos = caster:has(Tree.behaviors.positioned).position:floor() - local path = require "lib.pathfinder" (initialPos, target) - path:pop_front() - if path:is_empty() then - print("[Walk]: the path is empty", initialPos, target) - return - end - - caster:try(Tree.behaviors.stats, function(stats) - stats.mana = stats.mana - 2 - end) - - local sprite = caster:has(Tree.behaviors.sprite) - assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE") - if not sprite then - return - end - - return caster:has(Tree.behaviors.tiled):followPath(path) -end - -function walk:update(caster, dt) - local charPos = caster:has(Tree.behaviors.positioned).position:floor() - --- @type Vec3 - local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor() - self.path = require "lib.pathfinder" (charPos, mpos) -end - -function walk:draw() - if not self.path then return end - --- Это отрисовка пути персонажа к мышке - Tree.level.camera:attach() - love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) - love.graphics.setColor(0.6, 0.75, 0.5) - for p in self.path:values() do - love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1) - end - love.graphics.setCanvas() - Tree.level.camera:detach() - love.graphics.setColor(1, 1, 1) -end +} local regenerateMana = spell.new { tag = "dev_mana", @@ -101,7 +72,7 @@ local regenerateMana = spell.new { end } -local attack = spell.new { +local attack = spell.new { tag = "dev_attack", baseCooldown = 1, baseCost = 2, @@ -151,7 +122,7 @@ local attack = spell.new { } ---------------------------------------- -local spellbook = { +local spellbook = { walk = walk, regenerateMana = regenerateMana, attack = attack