Compare commits

...

12 Commits

Author SHA1 Message Date
8fc4ca5483 Merge pull request 'feature/spell-constraints' (#32) from feature/spell-constraints into main
Reviewed-on: #32
2026-03-18 05:11:54 +03:00
2e96ec821d Add cooldown handling for spells and display in UI
- Implement cooldown tracking in SpellcasterBehavior
- Decrease cooldowns at end of each round in turn order
- Prevent casting spells on cooldown in spell.cast
- Show cooldown overlay and block click on skill buttons
- Adjust font sizes for better UI consistency
2026-03-18 05:09:35 +03:00
ec816eb666 Refactor spell cast and task returns for optional tasks 2026-03-18 04:05:04 +03:00
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
ecec540251 Improve target validation logic in spell casting function 2026-03-18 02:03:30 +03:00
a6578ec8dd Refactor target distance check using query intersection
Replace manual distance check with combined targetQuery and
distance query intersection for cleaner spell targeting logic

Fix query combinators to correctly reference self in closures
2026-03-18 02:01:59 +03:00
ef5ff5f847 tweak light effects in spells 2026-03-18 01:47:09 +03:00
0d2ed101d6 Refactor light animation in regenerateMana spell using easing 2026-03-18 00:56:19 +03:00
4431934e6b Refactor spell target selection to use composable queries
Introduce SpellTargetQuery abstraction for flexible target filtering.
Replace fixed target types with query-based system supporting union,
intersection, and exclusion of target conditions. Update spells
accordingly.
2026-03-17 23:09:36 +03:00
e0b08b07ec Remove residentsleeper behavior from light effect in regenerateMana
spell
2026-03-17 20:39:30 +03:00
2e6155aea4 Add target validation and refactor spells with new cast logic 2026-02-13 01:28:36 +03:00
95f2230302 Add simple spell framework and refactor dev_attack spell to use it 2026-02-13 00:43:54 +03:00
14 changed files with 392 additions and 172 deletions

View File

@ -39,17 +39,24 @@ function behavior:makeTurn()
self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти
end) end)
spellB.spellbook[1]:cast(self.owner, self.target)(function() local task1 = spellB.spellbook[1]:cast(self.owner, self.target)
if task1 then
task1(
function()
-- здесь мы оказываемся после того, как сходили в первый раз -- здесь мы оказываемся после того, как сходили в первый раз
print("[AI]: finished move 1")
local newTarget = Vec3 { 1, 1 } local newTarget = Vec3 { 1, 1 }
-- поэтому позиция персонажа для нового каста пересчитается динамически local task2 = spellB.spellbook[1]:cast(self.owner, newTarget)
spellB.spellbook[1]:cast(self.owner, newTarget)(function() if task2 then
print("[AI]: finished move 2")
-- дергаем функцию после завершения хода -- дергаем функцию после завершения хода
task2(callback)
else
callback() callback()
end) end
end) end
)
else
callback()
end
end) end)
end end
end end

View File

@ -1,11 +1,13 @@
--- @class SpellcasterBehavior : Behavior --- @class SpellcasterBehavior : Behavior
--- @field spellbook Spell[] собственный набор спеллов персонажа --- @field spellbook Spell[] собственный набор спеллов персонажа
--- @field cast Spell | nil ссылка на активный спелл из спеллбука --- @field cast Spell | nil ссылка на активный спелл из спеллбука
--- @field cooldowns {[string]: integer} текущий кулдаун спеллов по тегам
--- @field state "idle" | "casting" | "running" --- @field state "idle" | "casting" | "running"
local behavior = {} local behavior = {}
behavior.__index = behavior behavior.__index = behavior
behavior.id = "spellcaster" behavior.id = "spellcaster"
behavior.state = "idle" behavior.state = "idle"
behavior.cooldowns = {}
---@param spellbook Spell[] | nil ---@param spellbook Spell[] | nil
---@return SpellcasterBehavior ---@return SpellcasterBehavior
@ -13,6 +15,7 @@ function behavior.new(spellbook)
local spb = require "lib.spellbook" --- @todo временное добавление ходьбы (и читов) всем персонажам local spb = require "lib.spellbook" --- @todo временное добавление ходьбы (и читов) всем персонажам
local t = {} local t = {}
t.spellbook = spellbook or spb.of { spb.walk, spb.regenerateMana, spb.attack } t.spellbook = spellbook or spb.of { spb.walk, spb.regenerateMana, spb.attack }
t.cooldowns = {}
return setmetatable(t, behavior) return setmetatable(t, behavior)
end end
@ -23,6 +26,14 @@ function behavior:endCast()
Tree.level.selector:unlock() Tree.level.selector:unlock()
end end
function behavior:processCooldowns()
local cds = {}
for tag, cd in pairs(self.cooldowns) do
cds[tag] = (cd - 1) >= 0 and cd - 1 or 0
end
self.cooldowns = cds
end
function behavior:update(dt) function behavior:update(dt)
if Tree.level.selector:deselected() then if Tree.level.selector:deselected() then
self.state = "idle" self.state = "idle"

View File

@ -38,16 +38,17 @@ function selector:update(dt)
return return
end end
local task = b.cast:cast(char, mousePosition) -- в task функция, которая запускает анимацию спелла local task = b.cast:cast(char, mousePosition) -- в task функция, которая запускает анимацию спелла
if task then if not task then return end -- не получилось скастовать
self:lock() self:lock()
b.state = "running" b.state = "running"
task( task(
function(_) -- это коллбэк, который сработает по окончании анимации спелла function(_) -- это коллбэк, который сработает по окончании анимации спелла
b:endCast() b:endCast()
if not char:has(Tree.behaviors.ai) then self:select(char.id) end -- выделяем персонажа обратно после того, как посмотрели на каст
end end
) )
end
end) end)
end end
end end

View File

@ -57,10 +57,20 @@ function turnOrder:next()
end) end)
end end
--- Производим действия в конце раунда
---
--- Меняем местами очередь сходивших и не сходивших (пустую) --- Меняем местами очередь сходивших и не сходивших (пустую)
function turnOrder:endRound() function turnOrder:endRound()
assert(self.pendingQueue:size() == 0, "[TurnOrder]: tried to end the round before everyone had a turn") assert(self.pendingQueue:size() == 0, "[TurnOrder]: tried to end the round before everyone had a turn")
print("[TurnOrder]: end of the round") print("[TurnOrder]: end of the round")
for _, id in ipairs(self.actedQueue.data) do
local char = Tree.level.characters[id]
char:try(Tree.behaviors.spellcaster, function(spellcaster)
spellcaster:processCooldowns()
end)
end
self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue self.actedQueue, self.pendingQueue = self.pendingQueue, self.actedQueue
self.current = self.pendingQueue:pop() self.current = self.pendingQueue:pop()
end end

View File

@ -57,7 +57,7 @@ function barElement:draw()
love.graphics.setColor(1, 1, 1) love.graphics.setColor(1, 1, 1)
--- текст поверх --- текст поверх
if self.drawText then if self.drawText then
local font = Tree.fonts:getDefaultTheme():getVariant("medium") local font = Tree.fonts:getDefaultTheme():getVariant("small")
local t = love.graphics.newText(font, tostring(self.value) .. "/" .. tostring(self.maxValue)) local t = love.graphics.newText(font, tostring(self.value) .. "/" .. tostring(self.maxValue))
love.graphics.draw(t, math.floor(self.bounds.x + self.bounds.width / 2 - t:getWidth() / 2), love.graphics.draw(t, math.floor(self.bounds.x + self.bounds.width / 2 - t:getWidth() / 2),
math.floor(self.bounds.y + self.bounds.height / 2 - t:getHeight() / 2)) math.floor(self.bounds.y + self.bounds.height / 2 - t:getHeight() / 2))

View File

@ -22,7 +22,7 @@ function endTurnButton:update(dt)
end end
function endTurnButton:layout() function endTurnButton:layout()
local font = Tree.fonts:getDefaultTheme():getVariant("headline") local font = Tree.fonts:getDefaultTheme():getVariant("large")
self.text = love.graphics.newText(font, "Завершить ход") self.text = love.graphics.newText(font, "Завершить ход")
self.bounds.width = self.text:getWidth() + 32 self.bounds.width = self.text:getWidth() + 32
self.bounds.height = self.text:getHeight() + 16 self.bounds.height = self.text:getHeight() + 16

View File

@ -8,6 +8,7 @@ local UI_SCALE = require "lib.simple_ui.level.scale"
--- @field hovered boolean --- @field hovered boolean
--- @field selected boolean --- @field selected boolean
--- @field onClick function? --- @field onClick function?
--- @field getCooldown function?
--- @field icon? string --- @field icon? string
local skillButton = setmetatable({}, Element) local skillButton = setmetatable({}, Element)
skillButton.__index = skillButton skillButton.__index = skillButton
@ -18,7 +19,11 @@ function skillButton:update(dt)
if self:hitTest(mx, my) then if self:hitTest(mx, my) then
self.hovered = true self.hovered = true
if Tree.controls:isJustPressed("select") then if Tree.controls:isJustPressed("select") then
local cd = self.getCooldown and self.getCooldown() or 0
if cd == 0 then
if self.onClick then self.onClick() end if self.onClick then self.onClick() end
end
Tree.controls:consume("select") Tree.controls:consume("select")
end end
else else
@ -29,6 +34,7 @@ end
function skillButton:draw() function skillButton:draw()
love.graphics.setLineWidth(2) love.graphics.setLineWidth(2)
local cd = self.getCooldown and self.getCooldown() or 0
if not self.icon then if not self.icon then
love.graphics.setColor(0.05, 0.05, 0.05) love.graphics.setColor(0.05, 0.05, 0.05)
@ -53,6 +59,25 @@ function skillButton:draw()
love.graphics.setColor(0.7, 1, 0.7, 0.5) love.graphics.setColor(0.7, 1, 0.7, 0.5)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
end end
if cd > 0 then
love.graphics.setColor(0, 0, 0, 0.5)
love.graphics.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height)
local font = Tree.fonts:getDefaultTheme():getVariant("headline")
love.graphics.setColor(0, 0, 0)
local t = love.graphics.newText(font, tostring(cd))
love.graphics.draw(t, math.floor(self.bounds.x + 2 + self.bounds.width / 2 - t:getWidth() / 2),
math.floor(self.bounds.y + 2 + self.bounds.height / 2 - t:getHeight() / 2))
love.graphics.setColor(1, 1, 1)
love.graphics.draw(t, math.floor(self.bounds.x + self.bounds.width / 2 - t:getWidth() / 2),
math.floor(self.bounds.y + self.bounds.height / 2 - t:getHeight() / 2))
else
end
love.graphics.setColor(1, 1, 1) love.graphics.setColor(1, 1, 1)
end end
@ -92,6 +117,9 @@ function skillRow.new(characterId)
behavior.cast = nil behavior.cast = nil
end end
end end
skb.getCooldown = function()
return behavior.cooldowns[spell.tag] or 0
end
t.children[i] = skb t.children[i] = skb
end end
end) end)

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

@ -0,0 +1,104 @@
local Query = require "lib.spell.target_query"
local targetTest = require "lib.spell.target_test"
local task = require "lib.utils.task"
--- @alias SpellPreview "default" Подсветка возможных целей
--- | "path" Подсветка пути до цели
--- @class Spell
--- @field tag string
--- @field baseCost integer Базовые затраты маны на каст
--- @field baseCooldown integer Базовый кулдаун в ходах
--- @field targetQuery SpellTargetQuery Селектор возможных целей
--- @field previewType 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<nil>? Вызывается в момент каста, изменяет мир.
local spell = {}
spell.__index = spell
spell.tag = "spell_base"
spell.baseCost = 1
spell.baseCooldown = 1
spell.targetQuery = Query(targetTest.any)
spell.previewType = "default"
function spell:update(caster, dt)
if self.previewType == "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
function spell:draw()
if self.previewType == "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)
love.graphics.setColor(0.6, 0.75, 0.5)
for p in path:values() do
love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1)
end
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?, previewType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task<nil>?}
--- @return Spell
function spell.new(data)
local newSpell = setmetatable({
tag = data.tag,
baseCost = data.baseCost,
baseCooldown = data.baseCooldown,
targetQuery = data.targetQuery,
previewType = data.previewType,
distance = data.distance
}, spell)
newSpell.targetQuery = newSpell.distance
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 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)
return data.onCast(caster, target)
end
return newSpell
end
return spell

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
}, query)
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
}, query)
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
}, query)
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

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

@ -0,0 +1,27 @@
--- @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,
-- тайл в пределах окружности в нашей кривой метрике
--- @param radius number
distance = function(radius)
return function(caster, targetPosition)
return caster:try(Tree.behaviors.positioned, function(p)
local dist = math.max(math.abs(p.position.x - targetPosition.x),
math.abs(p.position.y - targetPosition.y))
return dist <= radius
end)
end
end
}

View File

@ -8,35 +8,19 @@
--- Да, это 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 targetTest = require 'lib.spell.target_test'
local Query = require "lib.spell.target_query"
local easing = require "lib.utils.easing"
--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell local walk = spell.new {
--- @field tag string tag = "dev_move",
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла previewType = "path",
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире baseCooldown = 1,
--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task<nil> | nil Вызывается в момент каста, изменяет мир. baseCost = 2,
local spell = {} targetQuery = Query(targetTest.any):exclude(Query(targetTest.character)),
spell.__index = spell distance = 3,
spell.tag = "base" onCast = function(caster, target)
function spell:update(caster, dt) end
function spell:draw() end
function spell:cast(caster, target) return end
local walk = setmetatable({
--- @type Deque
path = nil
}, spell)
walk.tag = "dev_move"
function walk:cast(caster, target)
if not caster:try(Tree.behaviors.stats, function(stats)
return stats.mana >= 2
end) then
return
end
local initialPos = caster:has(Tree.behaviors.positioned).position:floor() local initialPos = caster:has(Tree.behaviors.positioned).position:floor()
local path = require "lib.pathfinder" (initialPos, target) local path = require "lib.pathfinder" (initialPos, target)
path:pop_front() path:pop_front()
@ -45,10 +29,6 @@ function walk:cast(caster, target)
return return
end end
caster:try(Tree.behaviors.stats, function(stats)
stats.mana = stats.mana - 2
end)
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE") assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE")
if not sprite then if not sprite then
@ -56,83 +36,51 @@ function walk:cast(caster, target)
end end
return caster:has(Tree.behaviors.tiled):followPath(path) return caster:has(Tree.behaviors.tiled):followPath(path)
end
function walk:update(caster, dt)
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()
self.path = require "lib.pathfinder" (charPos, mpos)
end
function walk:draw()
if not self.path then return end
--- Это отрисовка пути персонажа к мышке
Tree.level.camera:attach()
love.graphics.setCanvas(Tree.level.render.textures.overlayLayer)
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 end
love.graphics.setCanvas() }
Tree.level.camera:detach()
love.graphics.setColor(1, 1, 1)
end
local regenerateMana = setmetatable({}, spell) local regenerateMana = spell.new {
regenerateMana.tag = "dev_mana" tag = "dev_mana",
baseCooldown = 2,
function regenerateMana:cast(caster, target) baseCost = 0,
targetQuery = Query(targetTest.caster),
distance = 0,
onCast = function(caster, target)
caster:try(Tree.behaviors.stats, function(stats) caster:try(Tree.behaviors.stats, function(stats)
stats.mana = 10 stats.mana = 10
stats.initiative = stats.initiative + 10 stats.initiative = stats.initiative + 10
end) end)
local sprite = caster:has(Tree.behaviors.sprite) local sprite = caster:has(Tree.behaviors.sprite)
if not sprite then return nil end if not sprite then return end
print(caster.id, "has regenerated mana and gained initiative") print(caster.id, "has regenerated mana and gained initiative")
local light = require "lib/character/character".spawn("Light Effect") local light = require "lib/character/character".spawn("Light Effect")
light:addBehavior { light:addBehavior {
Tree.behaviors.light.new { color = Vec3 { 0.6, 0.3, 0.3 }, intensity = 4 }, Tree.behaviors.light.new { color = Vec3 { 0.3, 0.3, 0.6 }, intensity = 4 },
Tree.behaviors.residentsleeper.new(),
Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }),
} }
local flash = function(callback)
light:has(Tree.behaviors.light):animateColor(Vec3 {})(
function()
light:die()
callback()
end
)
end
return task.wait { return task.wait {
flash, 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") sprite:animate("hurt")
} }
end
local attack = setmetatable({}, spell)
attack.tag = "dev_attack"
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
end end
}
caster:try(Tree.behaviors.stats, function(stats) local attack = spell.new {
stats.mana = stats.mana - 2 tag = "dev_attack",
end) baseCooldown = 1,
baseCost = 2,
targetQuery = Query(targetTest.character):exclude(Query(targetTest.caster)),
distance = 1,
onCast = function(caster, target)
--- @type Character --- @type Character
local targetCharacterId = Tree.level.characterGrid:get(target) local targetCharacterId = Tree.level.characterGrid:get(target)
if not targetCharacterId or targetCharacterId == caster.id then return end
local targetCharacter = Tree.level.characters[targetCharacterId] local targetCharacter = Tree.level.characters[targetCharacterId]
targetCharacter:try(Tree.behaviors.stats, function(stats) targetCharacter:try(Tree.behaviors.stats, function(stats)
stats.hp = stats.hp - 4 stats.hp = stats.hp - 4
@ -148,13 +96,30 @@ function attack:cast(caster, target)
task.wait { task.wait {
sprite:animate("attack"), sprite:animate("attack"),
task.wait { task.wait {
task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200), task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(500),
function() return targetSprite:animate("hurt") end 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) Tree.audio:play(Tree.assets.files.audio.sounds.hurt)
} }
} }
end end
}
---------------------------------------- ----------------------------------------
local spellbook = { local spellbook = {

View File

@ -4,11 +4,11 @@
--- @field private _sizes {[FontVariant]: integer} --- @field private _sizes {[FontVariant]: integer}
local theme = { local theme = {
_sizes = { _sizes = {
smallest = 10, smallest = 12,
small = 12, small = 14,
medium = 14, medium = 16,
large = 16, large = 22,
headline = 20, headline = 32,
} }
} }
theme.__index = theme theme.__index = theme

View File

@ -1,5 +1,5 @@
---@class PriorityQueue ---@class PriorityQueue
---@field private data any[] внутренний массив-куча (индексация с 1) ---@field data any[] внутренний массив-куча (индексация с 1)
---@field private cmp fun(a:any, b:any):boolean компаратор: true, если a выше по приоритету, чем b ---@field private cmp fun(a:any, b:any):boolean компаратор: true, если a выше по приоритету, чем b
local PriorityQueue = {} local PriorityQueue = {}
PriorityQueue.__index = PriorityQueue PriorityQueue.__index = PriorityQueue

View File

@ -111,7 +111,7 @@ function love.draw()
testLayout:draw() testLayout:draw()
love.graphics.setColor(1, 1, 1) love.graphics.setColor(1, 1, 1)
love.graphics.setFont(Tree.fonts:getTheme("Roboto_Mono"):getVariant("medium")) love.graphics.setFont(Tree.fonts:getTheme("Roboto_Mono"):getVariant("small"))
local stats = "fps: " .. local stats = "fps: " ..
love.timer.getFPS() .. love.timer.getFPS() ..
" lt: " .. lt .. " dt: " .. dt .. " mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000) " lt: " .. lt .. " dt: " .. dt .. " mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000)