local Query = require "lib.spell.target_query" local targetTest = require "lib.spell.target_test" local task = require "lib.utils.task" local easing = require "lib.utils.easing" local pf = require "lib.pathfinder" --- @alias SpellPreview "default" Подсветка возможных целей --- | "path" Подсветка пути до цели --- @class Spell --- @field tag string --- @field baseCost integer Базовые затраты маны на каст --- @field baseCooldown integer Базовый кулдаун в ходах --- @field targetQuery SpellTargetQuery Селектор возможных целей --- @field targetType SpellPreview Вид превью во время каста --- @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? Вызывается в момент каста, изменяет мир. local spell = {} spell.__index = spell spell.tag = "spell_base" spell.baseCost = 1 spell.baseCooldown = 1 spell.targetQuery = Query(targetTest.any) spell.targetType = "default" --- Вызывается, когда игрок выбирает спелл на панели заклинаний --- @param caster Character function spell:onSelected(caster) self.targets = self.targetQuery:asSet(caster) self.tSize = 0.67 -- анимация появления таргетов task.tween(self, { tSize = 1 }, 200, easing.easeOutQuad) end function spell:update(caster, dt) if self.targetType == "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 local icons = require("lib.utils.sprite_atlas").load(Tree.assets.files.overlay_icons) function spell:draw() if self.targetType == "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) local i = 0 path:pop_front() for p in path:values() do i = i + 1 local s = 1 / Tree.level.camera.pixelsPerMeter local quad = i > self.distance and icons:pickQuad('dev_path_closed') or icons:pickQuad('dev_path') love.graphics.draw(icons.atlas, quad, p.x, p.y, 0, s, s) end love.graphics.setCanvas() Tree.level.camera:detach() love.graphics.setColor(1, 1, 1) else Tree.level.camera:attach() love.graphics.setCanvas(Tree.level.render.textures.overlayLayer) love.graphics.setColor(1, 1, 1, 0.5) for _, p in pairs(self.targets) do local s = self.tSize / Tree.level.camera.pixelsPerMeter local quad = icons:pickQuad('dev_target') love.graphics.draw(icons.atlas, quad, p.x + 0.5 - self.tSize / 2, p.y + 0.5 - self.tSize / 2, 0, s, s) end love.graphics.setShader() love.graphics.setCanvas() Tree.level.camera:detach() love.graphics.setColor(1, 1, 1) end end function spell:cast(caster, target) return task.fromValue() end --- Конструктор [Spell] --- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery?, targetType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task?} --- @return Spell function spell.new(data) local newSpell = setmetatable({ tag = data.tag, baseCost = data.baseCost, baseCooldown = data.baseCooldown, targetQuery = data.targetQuery, targetType = data.targetType, distance = data.distance }, spell) newSpell.targetQuery = (newSpell.distance and newSpell.targetType ~= "path") and newSpell.targetQuery:intersect(Query(targetTest.distance(newSpell.distance))) or newSpell.targetQuery function newSpell:cast(caster, target) if caster:try(Tree.behaviors.spellcaster, function(spellcaster) -- проверка на кулдаун return (spellcaster.cooldowns[self.tag] or 0) > 0 end) then return end if not self.targetQuery.test(caster, target) then return end -- проверка корректности цели if self.targetType == "path" then -- дополнительное условие для спеллов с путями (количество шагов) if not caster:has(Tree.behaviors.tiled) or not caster:has(Tree.behaviors.positioned) then return end local i = 1 for _ in pf(caster:has(Tree.behaviors.positioned).position, target):values() do if i > self.distance + 1 then return end -- учитывается начальная точка, где находится кастер i = i + 1 end end -- проверка на достаточное количество маны if caster:try(Tree.behaviors.stats, function(stats) return stats.mana < self.baseCost end) then return end caster:try(Tree.behaviors.stats, function(stats) stats.mana = stats.mana - self.baseCost end) caster:try(Tree.behaviors.spellcaster, function(spellcaster) spellcaster.cooldowns[self.tag] = self.baseCooldown end) caster:try(Tree.behaviors.effects, function(effects) effects:beforeCast() end) return data.onCast(caster, target) end return newSpell end return spell