--- Алгоритм обработки заклинания (for dummies): --- 1) ПОКА выделен персонаж И он находится в режиме каста, вызывать spell:update() и spell:draw() каждый кадр (это отвечает за обработку и отрисовку превьюшки каста, например, превью пути или зоны поражения; реализуется через установку spellcaster.cast, см. код в кнопке) --- ЕСЛИ выбран тайл, ТО вызвать spell:cast() (это запрос на обработку последствий применения заклинания, например, старт анимации ходьбы, выпуск снаряда и т.д.; реализовано в selector) --- ЕСЛИ spell:cast() ИСТИНА, ТО вызвать selector:lock() (отключить обработку выделения всего на уровне; реализовано в selector) --- --- 2) ПОКА анимация каста НЕ завершена, ничего не делать, ИНАЧЕ вызвать behaviors.spellcaster:endCast() (вот это сейчас нужно вызывать самостоятельно, т.к. нет возможности обобщенно отследить завершение анимаций) --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async local AnimationNode = require "lib.animation_node" --- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell --- @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 function spell:update(caster, dt) end function spell:draw() end function spell:cast(caster, target) return true end local walk = setmetatable({ --- @type Deque path = nil }, spell) function walk:cast(caster, target) if not caster:try(Tree.behaviors.stats, function(stats) return stats.mana >= 2 end) then return false end local path = self.path path:pop_front() if path:is_empty() then return false end for p in path:values() do print(p) 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.map):followPath(path, node) end, onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end, }:run() return true end function walk:update(caster, dt) local charPos = caster:has(Tree.behaviors.map).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 --- Это отрисовка пути персонажа к мышке 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.setColor(1, 1, 1) end local regenerateMana = setmetatable({}, spell) function regenerateMana:cast(caster, target) caster:try(Tree.behaviors.stats, function(stats) stats.mana = 10 end) print(caster.id, "has regenerated mana") local sprite = caster:has(Tree.behaviors.sprite) if not sprite then return true end AnimationNode { function(node) sprite:animate("hurt", node) end, onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end }:run() return true end local attack = setmetatable({}, spell) function attack:cast(caster, target) if caster:try(Tree.behaviors.map, function(map) local dist = math.max(math.abs(map.position.x - target.x), math.abs(map.position.y - target.y)) print("dist:", dist) return dist >= 2 end) then return false end --- @type Character local targetCharacterId = Tree.level.characterGrid:get(target) if not targetCharacterId then return false 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 true 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(200, node) end, children = { AnimationNode { function(node) targetSprite:animate("hurt", node) end } } } } }:run() return true end ---------------------------------------- local spellbook = { walk = walk, regenerateMana = regenerateMana, attack = attack } --- Создает новый спеллбук с уникальными спеллами (а не ссылками на шаблоны) --- @param list Spell[] function spellbook.of(list) local spb = {} for i, sp in ipairs(list) do spb[i] = setmetatable({}, { __index = sp }) end return spb end return spellbook