From 95f2230302f4e59d0fa738a8e48dd5059340d8f5 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Fri, 13 Feb 2026 00:43:54 +0300 Subject: [PATCH] Add simple spell framework and refactor dev_attack spell to use it --- lib/spell/spell.lua | 67 ++++++++++++++++++++++++++++++++++ lib/spellbook.lua | 88 +++++++++++++++++---------------------------- 2 files changed, 100 insertions(+), 55 deletions(-) create mode 100644 lib/spell/spell.lua diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua new file mode 100644 index 0000000..82bcbb1 --- /dev/null +++ b/lib/spell/spell.lua @@ -0,0 +1,67 @@ +--- @alias SpellTarget "any" Любой тайл +--- | "caster" Сам кастующий +--- | "enemy" Противники +--- | "ally" Союзники +--- | "character" Любой персонаж + +--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell +--- @field tag string +--- @field baseCost integer Базовые затраты маны на каст +--- @field baseCooldown integer Базовый кулдаун в ходах +--- @field possibleTarget SpellTarget Возможная цель +--- @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 Вызывается в момент каста, изменяет мир. +local spell = {} +spell.__index = spell +spell.tag = "spell_base" +spell.baseCost = 1 +spell.baseCooldown = 1 +spell.possibleTarget = "any" + +function spell:update(caster, dt) end + +function spell:draw() end + +function spell:cast(caster, target) return end + +--- Конструктор [Spell] +--- @param data {tag: string, baseCost: integer, baseCooldown: integer, possibleTarget: SpellTarget, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task} +--- @return Spell +function spell.new(data) + local newSpell = { + tag = data.tag, + baseCost = data.baseCost, + baseCooldown = data.baseCooldown, + possibleTarget = data.possibleTarget, + distance = data.distance + } + + function newSpell:cast(caster, target) + -- проверка корректности цели + --- @TODO имплементировать все варианты SpellTarget + + -- проверка на расстояние до цели + if self.distance and caster:try(Tree.behaviors.positioned, function(p) + local dist = math.max(math.abs(p.position.x - target.x), math.abs(p.position.y - target.y)) + print("dist:", dist) + return dist > self.distance + end) then + return + end + + -- проверка на достаточное количество маны + if caster:try(Tree.behaviors.stats, function(stats) + return stats.mana < self.baseCost + end) then + return + end + + return data.onCast(caster, target) + end + + return setmetatable(newSpell, spell) +end + +return spell diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 7c95ea5..ee19bfe 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,28 +7,14 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async -local task = require 'lib.utils.task' +local task = require 'lib.utils.task' +local spell = require 'lib.spell.spell' ---- @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" - -function spell:update(caster, dt) end - -function spell:draw() end - -function spell:cast(caster, target) return end - -local walk = setmetatable({ +local walk = setmetatable({ --- @type Deque path = nil }, spell) -walk.tag = "dev_move" +walk.tag = "dev_move" function walk:cast(caster, target) if not caster:try(Tree.behaviors.stats, function(stats) @@ -114,47 +100,39 @@ function regenerateMana:cast(caster, target) } end -local attack = setmetatable({}, spell) -attack.tag = "dev_attack" +local attack = spell.new { + tag = "dev_attack", + baseCooldown = 1, + baseCost = 2, + possibleTarget = "enemy", + distance = 2, + onCast = function(caster, target) + --- @type Character + local targetCharacterId = Tree.level.characterGrid:get(target) + if not targetCharacterId or targetCharacterId == caster.id then return task.fromValue() end + local targetCharacter = Tree.level.characters[targetCharacterId] + targetCharacter:try(Tree.behaviors.stats, function(stats) + stats.hp = stats.hp - 4 + end) + + local sprite = caster:has(Tree.behaviors.sprite) + local targetSprite = targetCharacter:has(Tree.behaviors.sprite) + if not sprite or not targetSprite then return task.fromValue() end + + caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end) -function attack:cast(caster, target) - if caster:try(Tree.behaviors.positioned, function(p) - local dist = math.max(math.abs(p.position.x - target.x), math.abs(p.position.y - target.y)) - print("dist:", dist) - return dist > 2 - end) then return - end - - caster:try(Tree.behaviors.stats, function(stats) - stats.mana = stats.mana - 2 - end) - - --- @type Character - local targetCharacterId = Tree.level.characterGrid:get(target) - 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 - end) - - local sprite = caster:has(Tree.behaviors.sprite) - local targetSprite = targetCharacter:has(Tree.behaviors.sprite) - if not sprite or not targetSprite then return end - - caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) 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) + 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 +} ---------------------------------------- local spellbook = {