heroes-of-nerevelon/lib/spellbook.lua
PeaAshMeter d84fc4a7c2 Add path preview and refactor spells with new Spell API
- Add path preview support to Spell with update and draw methods
- Refactor spell:cast to always return a Task
- Simplify spell.new constructor and apply distance constraint uniformly
- Replace walk spell with new Spell-based implementation supporting path
  preview
- Remove debug print from target_test.lua
2026-03-18 03:53:27 +03:00

143 lines
6.5 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

--- Алгоритм обработки заклинания (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 task = require 'lib.utils.task'
local spell = require 'lib.spell.spell'
local targetTest = require 'lib.spell.target_test'
local Query = require "lib.spell.target_query"
local easing = require "lib.utils.easing"
local walk = spell.new {
tag = "dev_move",
previewType = "path",
baseCooldown = 1,
baseCost = 2,
targetQuery = Query(targetTest.any):exclude(Query(targetTest.character)),
distance = 3,
onCast = function(caster, target)
local initialPos = caster:has(Tree.behaviors.positioned).position:floor()
local path = require "lib.pathfinder" (initialPos, target)
path:pop_front()
if path:is_empty() then
print("[Walk]: the path is empty", initialPos, target)
return task.fromValue()
end
local sprite = caster:has(Tree.behaviors.sprite)
assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE")
if not sprite then
return task.fromValue()
end
return caster:has(Tree.behaviors.tiled):followPath(path)
end
}
local regenerateMana = spell.new {
tag = "dev_mana",
baseCooldown = 2,
baseCost = 0,
targetQuery = Query(targetTest.caster),
distance = 0,
onCast = function(caster, target)
caster:try(Tree.behaviors.stats, function(stats)
stats.mana = 10
stats.initiative = stats.initiative + 10
end)
local sprite = caster:has(Tree.behaviors.sprite)
if not sprite then return task.fromValue() end
print(caster.id, "has regenerated mana and gained initiative")
local light = require "lib/character/character".spawn("Light Effect")
light:addBehavior {
Tree.behaviors.light.new { color = Vec3 { 0.3, 0.3, 0.6 }, intensity = 4 },
Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
}
return task.wait {
task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]],
{ intensity = 1, color = Vec3 {} }, 800, easing.easeInCubic), function()
light:die()
return task.fromValue()
end),
sprite:animate("hurt")
}
end
}
local attack = spell.new {
tag = "dev_attack",
baseCooldown = 1,
baseCost = 2,
targetQuery = Query(targetTest.character):exclude(Query(targetTest.caster)),
distance = 1,
onCast = function(caster, target)
--- @type Character
local targetCharacterId = Tree.level.characterGrid:get(target)
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)
return
task.wait {
sprite:animate("attack"),
task.wait {
task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(500),
function()
local light = require "lib/character/character".spawn("Light Effect")
light:addBehavior {
Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 },
Tree.behaviors.positioned.new(targetCharacter:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
}
return
task.wait {
task.chain(task.tween(light:has(Tree.behaviors.light) --[[@as LightBehavior]],
{ intensity = 1, color = Vec3 {} }, 1000, easing.easeInCubic), function()
light:die()
return task.fromValue()
end),
targetSprite:animate("hurt")
}
end
),
Tree.audio:play(Tree.assets.files.audio.sounds.hurt)
}
}
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
print(i)
spb[i] = setmetatable({}, { __index = sp })
end
return spb
end
return spellbook