feature/spell-constraints #32

Merged
PeaAshMeter merged 11 commits from feature/spell-constraints into main 2026-03-18 05:11:55 +03:00
2 changed files with 100 additions and 55 deletions
Showing only changes of commit 95f2230302 - Show all commits

67
lib/spell/spell.lua Normal file
View File

@ -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> | 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

View File

@ -7,28 +7,14 @@
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
--- Да, это Future/Promise/await/async --- Да, это 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 local walk = setmetatable({
--- @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> | 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({
--- @type Deque --- @type Deque
path = nil path = nil
}, spell) }, spell)
walk.tag = "dev_move" walk.tag = "dev_move"
function walk:cast(caster, target) function walk:cast(caster, target)
if not caster:try(Tree.behaviors.stats, function(stats) if not caster:try(Tree.behaviors.stats, function(stats)
@ -114,47 +100,39 @@ function regenerateMana:cast(caster, target)
} }
end end
local attack = setmetatable({}, spell) local attack = spell.new {
attack.tag = "dev_attack" 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 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.wait {
task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200), sprite:animate("attack"),
function() return targetSprite:animate("hurt") end task.wait {
), task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200),
Tree.audio:play(Tree.assets.files.audio.sounds.hurt) function() return targetSprite:animate("hurt") end
),
Tree.audio:play(Tree.assets.files.audio.sounds.hurt)
}
} }
} end
end }
---------------------------------------- ----------------------------------------
local spellbook = { local spellbook = {