feature/spell-constraints #32

Merged
PeaAshMeter merged 11 commits from feature/spell-constraints into main 2026-03-18 05:11:55 +03:00
4 changed files with 106 additions and 42 deletions
Showing only changes of commit 4431934e6b - Show all commits

View File

@ -1,24 +1,26 @@
--- @alias SpellTarget "any" Любой тайл local Query = require "lib.spell.target_query"
--- | "caster" Сам кастующий local targetTest = require "lib.spell.target_test"
--- | "enemy" Противники
--- | "ally" Союзники
--- | "character" Любой персонаж
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell --- @alias SpellPreview "default" Подсветка возможных целей
--- | "path" Подсветка пути до цели
--- @class Spell
--- @field tag string --- @field tag string
--- @field baseCost integer Базовые затраты маны на каст --- @field baseCost integer Базовые затраты маны на каст
--- @field baseCooldown integer Базовый кулдаун в ходах --- @field baseCooldown integer Базовый кулдаун в ходах
--- @field possibleTarget SpellTarget Возможная цель --- @field targetQuery SpellTargetQuery Селектор возможных целей
--- @field previewType SpellPreview Вид превью во время каста
--- @field distance? integer Сторона квадрата с центром в позиции кастера, в пределах которого должна находиться цель, либо отсутствие ограничения --- @field distance? integer Сторона квадрата с центром в позиции кастера, в пределах которого должна находиться цель, либо отсутствие ограничения
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла --- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире --- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task<nil> | nil Вызывается в момент каста, изменяет мир. --- @field cast fun(self: Spell, caster: Character, target: Vec3): Task<nil> | nil Вызывается в момент каста, изменяет мир.
local spell = {} local spell = {}
spell.__index = spell spell.__index = spell
spell.tag = "spell_base" spell.tag = "spell_base"
spell.baseCost = 1 spell.baseCost = 1
spell.baseCooldown = 1 spell.baseCooldown = 1
spell.possibleTarget = "any" spell.targetQuery = Query(targetTest.any)
spell.previewType = "default"
function spell:update(caster, dt) end function spell:update(caster, dt) end
@ -26,34 +28,15 @@ function spell:draw() end
function spell:cast(caster, target) return end function spell:cast(caster, target) return end
--- @param caster Character
--- @param spellTarget SpellTarget
--- @param targetPosition Vec3
local function checkTarget(caster, spellTarget, targetPosition)
--- @TODO имплементировать все варианты SpellTarget
local targetCharacterId = Tree.level.characterGrid:get(targetPosition)
if spellTarget == "caster" then
return targetCharacterId == caster.id
end
if spellTarget == "character" then
return not not targetCharacterId
end
if spellTarget == "enemy" then
return targetCharacterId and targetCharacterId ~= caster.id
end
return true
end
--- Конструктор [Spell] --- Конструктор [Spell]
--- @param data {tag: string, baseCost: integer, baseCooldown: integer, possibleTarget: SpellTarget, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task} --- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task}
--- @return Spell --- @return Spell
function spell.new(data) function spell.new(data)
local newSpell = { local newSpell = {
tag = data.tag, tag = data.tag,
baseCost = data.baseCost, baseCost = data.baseCost,
baseCooldown = data.baseCooldown, baseCooldown = data.baseCooldown,
possibleTarget = data.possibleTarget, targetQuery = data.targetQuery,
distance = data.distance distance = data.distance
} }
@ -67,8 +50,7 @@ function spell.new(data)
return return
end end
-- проверка корректности цели if not self.targetQuery.test(caster, target) then return end -- проверка корректности цели
if not checkTarget(caster, self.possibleTarget, target) then return end
-- проверка на достаточное количество маны -- проверка на достаточное количество маны
if caster:try(Tree.behaviors.stats, function(stats) if caster:try(Tree.behaviors.stats, function(stats)

View File

@ -0,0 +1,67 @@
--- Тип, отвечающий за выбор и фильтрацию подходящих тайлов как цели спелла
--- теория множеств my beloved?
--- @class SpellTargetQuery
local query = {}
query.__index = query
--- Проверяет координаты на соответствие внутреннему условию
--- @param caster Character
--- @param position Vec3
--- @return boolean
function query.test(caster, position)
return true
end
--- Объединение
--- @param q SpellTargetQuery
function query:join(q)
return setmetatable({
test = function(caster, pos)
return self.test(caster, pos) or q.test(caster, pos)
end
}, q)
end
--- Пересечение
--- @param q SpellTargetQuery
function query:intersect(q)
return setmetatable({
test = function(caster, pos)
return self.test(caster, pos) and q.test(caster, pos)
end
}, q)
end
--- Исключение (не коммутативное, "те, что есть в query, но нет в q")
--- @param q SpellTargetQuery
function query:exclude(q)
return setmetatable({
test = function(caster, pos)
return self.test(caster, pos) and not q.test(caster, pos)
end
}, q)
end
--- Находит все соответствующие условиям координаты тайлов и возвращает их в виде списка
--- @param caster Character
--- @return Vec3[]
function query:asSet(caster)
--- @TODO: оптимизировать и брать не всю карту для выборки
local res = {}
for _, tile in pairs(Tree.level.tileGrid) do
if self.test(caster, tile.position) then
table.insert(res, tile.position)
end
end
return res
end
--- @param test SpellTargetTest
local function new(test)
return setmetatable({
test = test
}, query)
end
return new

13
lib/spell/target_test.lua Normal file
View File

@ -0,0 +1,13 @@
--- @alias SpellTargetTest fun(caster: Character, targetPosition: Vec3) : boolean
return {
any = function() return true end,
caster = function(caster, targetPosition)
local targetCharacterId = Tree.level.characterGrid:get(targetPosition)
return caster.id == targetCharacterId
end,
character = function(caster, targetPosition)
local targetCharacterId = Tree.level.characterGrid:get(targetPosition)
return not not targetCharacterId
end
}

View File

@ -7,14 +7,16 @@
--- --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' local spell = require 'lib.spell.spell'
local targetTest = require 'lib.spell.target_test'
local Query = require "lib.spell.target_query"
local walk = setmetatable({ 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)
@ -69,7 +71,7 @@ local regenerateMana = spell.new {
tag = "dev_mana", tag = "dev_mana",
baseCooldown = 2, baseCooldown = 2,
baseCost = 0, baseCost = 0,
possibleTarget = "caster", targetQuery = Query(targetTest.caster),
distance = 0, distance = 0,
onCast = function(caster, target) onCast = function(caster, target)
caster:try(Tree.behaviors.stats, function(stats) caster:try(Tree.behaviors.stats, function(stats)
@ -107,7 +109,7 @@ local attack = spell.new {
tag = "dev_attack", tag = "dev_attack",
baseCooldown = 1, baseCooldown = 1,
baseCost = 2, baseCost = 2,
possibleTarget = "enemy", targetQuery = Query(targetTest.character):exclude(Query(targetTest.caster)),
distance = 1, distance = 1,
onCast = function(caster, target) onCast = function(caster, target)
--- @type Character --- @type Character