From 95f2230302f4e59d0fa738a8e48dd5059340d8f5 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Fri, 13 Feb 2026 00:43:54 +0300 Subject: [PATCH 01/11] Add simple spell framework and refactor dev_attack spell to use it --- lib/spell/spell.lua | 67 ++++++++++++++++++++++++++++++++++ lib/spellbook.lua | 88 +++++++++++++++++---------------------------- 2 files changed, 100 insertions(+), 55 deletions(-) create mode 100644 lib/spell/spell.lua diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua new file mode 100644 index 0000000..82bcbb1 --- /dev/null +++ b/lib/spell/spell.lua @@ -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 Вызывается в момент каста, изменяет мир. +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 diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 7c95ea5..ee19bfe 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,28 +7,14 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это 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 ---- @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 Вызывается в момент каста, изменяет мир. -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({ +local walk = setmetatable({ --- @type Deque path = nil }, spell) -walk.tag = "dev_move" +walk.tag = "dev_move" function walk:cast(caster, target) if not caster:try(Tree.behaviors.stats, function(stats) @@ -114,47 +100,39 @@ function regenerateMana:cast(caster, target) } end -local attack = setmetatable({}, spell) -attack.tag = "dev_attack" +local attack = spell.new { + 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 - 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.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200), - function() return targetSprite:animate("hurt") end - ), - Tree.audio:play(Tree.assets.files.audio.sounds.hurt) + sprite:animate("attack"), + task.wait { + task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200), + function() return targetSprite:animate("hurt") end + ), + Tree.audio:play(Tree.assets.files.audio.sounds.hurt) + } } - } -end + end +} ---------------------------------------- local spellbook = { -- 2.47.2 From 2e6155aea4c59ac302c76ff59d50f718bf3f452d Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Fri, 13 Feb 2026 01:28:36 +0300 Subject: [PATCH 02/11] Add target validation and refactor spells with new cast logic --- lib/spell/spell.lua | 29 +++++++++++++++++--- lib/spellbook.lua | 65 ++++++++++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 82bcbb1..ec162a9 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -26,6 +26,25 @@ function spell:draw() 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] --- @param data {tag: string, baseCost: integer, baseCooldown: integer, possibleTarget: SpellTarget, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task} --- @return Spell @@ -39,9 +58,6 @@ function spell.new(data) } 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)) @@ -51,6 +67,9 @@ function spell.new(data) return end + -- проверка корректности цели + if not checkTarget(caster, self.possibleTarget, target) then return end + -- проверка на достаточное количество маны if caster:try(Tree.behaviors.stats, function(stats) return stats.mana < self.baseCost @@ -58,6 +77,10 @@ function spell.new(data) return end + caster:try(Tree.behaviors.stats, function(stats) + stats.mana = stats.mana - self.baseCost + end) + return data.onCast(caster, target) end diff --git a/lib/spellbook.lua b/lib/spellbook.lua index ee19bfe..bfca196 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -65,51 +65,54 @@ function walk:draw() love.graphics.setColor(1, 1, 1) end -local regenerateMana = setmetatable({}, spell) -regenerateMana.tag = "dev_mana" +local regenerateMana = spell.new { + tag = "dev_mana", + baseCooldown = 2, + baseCost = 0, + possibleTarget = "caster", + distance = 0, + onCast = function(caster, target) + caster:try(Tree.behaviors.stats, function(stats) + stats.mana = 10 + stats.initiative = stats.initiative + 10 + end) -function regenerateMana:cast(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 sprite = caster:has(Tree.behaviors.sprite) - if not sprite then return nil 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.6, 0.3, 0.3 }, intensity = 4 }, + Tree.behaviors.residentsleeper.new(), + Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), + } - 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.residentsleeper.new(), - 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 - local flash = function(callback) - light:has(Tree.behaviors.light):animateColor(Vec3 {})( - function() - light:die() - callback() - end - ) + return task.wait { + flash, + sprite:animate("hurt") + } end - - return task.wait { - flash, - sprite:animate("hurt") - } -end +} local attack = spell.new { tag = "dev_attack", baseCooldown = 1, baseCost = 2, possibleTarget = "enemy", - distance = 2, + distance = 1, 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 -- 2.47.2 From e0b08b07ec64205a3db5fc53e749c2fe33cd4cbe Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Tue, 17 Mar 2026 20:39:30 +0300 Subject: [PATCH 03/11] Remove residentsleeper behavior from light effect in regenerateMana spell --- lib/spellbook.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index bfca196..3ce45f1 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -84,7 +84,6 @@ local regenerateMana = spell.new { 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.residentsleeper.new(), Tree.behaviors.positioned.new(caster:has(Tree.behaviors.positioned).position + Vec3 { 0.5, 0.5 }), } -- 2.47.2 From 4431934e6b0ec71e54dc352b84a3e9f0d9501b47 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Tue, 17 Mar 2026 23:09:36 +0300 Subject: [PATCH 04/11] 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. --- lib/spell/spell.lua | 54 ++++++++++-------------------- lib/spell/target_query.lua | 67 ++++++++++++++++++++++++++++++++++++++ lib/spell/target_test.lua | 13 ++++++++ lib/spellbook.lua | 14 ++++---- 4 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 lib/spell/target_query.lua create mode 100644 lib/spell/target_test.lua diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index ec162a9..43f8a65 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -1,24 +1,26 @@ ---- @alias SpellTarget "any" Любой тайл ---- | "caster" Сам кастующий ---- | "enemy" Противники ---- | "ally" Союзники ---- | "character" Любой персонаж +local Query = require "lib.spell.target_query" +local targetTest = require "lib.spell.target_test" ---- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell +--- @alias SpellPreview "default" Подсветка возможных целей +--- | "path" Подсветка пути до цели + +--- @class Spell --- @field tag string --- @field baseCost integer Базовые затраты маны на каст --- @field baseCooldown integer Базовый кулдаун в ходах ---- @field possibleTarget SpellTarget Возможная цель +--- @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.possibleTarget = "any" +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) end @@ -26,34 +28,15 @@ function spell:draw() 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] ---- @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 function spell.new(data) local newSpell = { tag = data.tag, baseCost = data.baseCost, baseCooldown = data.baseCooldown, - possibleTarget = data.possibleTarget, + targetQuery = data.targetQuery, distance = data.distance } @@ -67,8 +50,7 @@ function spell.new(data) return end - -- проверка корректности цели - if not checkTarget(caster, self.possibleTarget, target) then return end + if not self.targetQuery.test(caster, target) then return end -- проверка корректности цели -- проверка на достаточное количество маны if caster:try(Tree.behaviors.stats, function(stats) diff --git a/lib/spell/target_query.lua b/lib/spell/target_query.lua new file mode 100644 index 0000000..6460c47 --- /dev/null +++ b/lib/spell/target_query.lua @@ -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 diff --git a/lib/spell/target_test.lua b/lib/spell/target_test.lua new file mode 100644 index 0000000..f7646a7 --- /dev/null +++ b/lib/spell/target_test.lua @@ -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 +} diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 3ce45f1..302338e 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,14 +7,16 @@ --- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла --- Да, это Future/Promise/await/async -local task = require 'lib.utils.task' -local spell = require 'lib.spell.spell' +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 walk = setmetatable({ +local walk = setmetatable({ --- @type Deque path = nil }, spell) -walk.tag = "dev_move" +walk.tag = "dev_move" function walk:cast(caster, target) if not caster:try(Tree.behaviors.stats, function(stats) @@ -69,7 +71,7 @@ local regenerateMana = spell.new { tag = "dev_mana", baseCooldown = 2, baseCost = 0, - possibleTarget = "caster", + targetQuery = Query(targetTest.caster), distance = 0, onCast = function(caster, target) caster:try(Tree.behaviors.stats, function(stats) @@ -107,7 +109,7 @@ local attack = spell.new { tag = "dev_attack", baseCooldown = 1, baseCost = 2, - possibleTarget = "enemy", + targetQuery = Query(targetTest.character):exclude(Query(targetTest.caster)), distance = 1, onCast = function(caster, target) --- @type Character -- 2.47.2 From 0d2ed101d6d754eea528c12f7f485565978907e8 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 00:56:19 +0300 Subject: [PATCH 05/11] Refactor light animation in regenerateMana spell using easing --- lib/spellbook.lua | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 302338e..8e7834e 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -11,6 +11,7 @@ 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 = setmetatable({ --- @type Deque @@ -89,17 +90,12 @@ local regenerateMana = spell.new { 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 { - flash, + 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), sprite:animate("hurt") } end -- 2.47.2 From ef5ff5f847dccf047ce6e21d594d593372c28d58 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 01:47:09 +0300 Subject: [PATCH 06/11] tweak light effects in spells --- lib/spellbook.lua | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 8e7834e..30e62a6 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -86,13 +86,13 @@ local regenerateMana = spell.new { 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.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 {} }, 1000, easing.easeInCubic), function() + { intensity = 1, color = Vec3 {} }, 800, easing.easeInCubic), function() light:die() return task.fromValue() end), @@ -125,9 +125,25 @@ local attack = spell.new { task.wait { sprite:animate("attack"), task.wait { - task.chain(targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200), - function() return targetSprite:animate("hurt") end + 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) } } -- 2.47.2 From a6578ec8dd978acc5c8236111299132b3ffc31b6 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 02:01:59 +0300 Subject: [PATCH 07/11] 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 --- lib/spell/spell.lua | 11 +---------- lib/spell/target_query.lua | 6 +++--- lib/spell/target_test.lua | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 43f8a65..099ec85 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -41,16 +41,7 @@ function spell.new(data) } function newSpell:cast(caster, target) - -- проверка на расстояние до цели - 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 not self.targetQuery.test(caster, target) then return end -- проверка корректности цели + if not self.targetQuery:intersect(Query(targetTest.distance(self.distance))).test(caster, target) then return end -- проверка корректности цели -- проверка на достаточное количество маны if caster:try(Tree.behaviors.stats, function(stats) diff --git a/lib/spell/target_query.lua b/lib/spell/target_query.lua index 6460c47..d84de28 100644 --- a/lib/spell/target_query.lua +++ b/lib/spell/target_query.lua @@ -19,7 +19,7 @@ function query:join(q) test = function(caster, pos) return self.test(caster, pos) or q.test(caster, pos) end - }, q) + }, query) end --- Пересечение @@ -29,7 +29,7 @@ function query:intersect(q) test = function(caster, pos) return self.test(caster, pos) and q.test(caster, pos) end - }, q) + }, query) end --- Исключение (не коммутативное, "те, что есть в query, но нет в q") @@ -39,7 +39,7 @@ function query:exclude(q) test = function(caster, pos) return self.test(caster, pos) and not q.test(caster, pos) end - }, q) + }, query) end --- Находит все соответствующие условиям координаты тайлов и возвращает их в виде списка diff --git a/lib/spell/target_test.lua b/lib/spell/target_test.lua index f7646a7..0f99146 100644 --- a/lib/spell/target_test.lua +++ b/lib/spell/target_test.lua @@ -1,13 +1,28 @@ --- @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)) + print("dist:", dist) + return dist <= radius + end) + end end } -- 2.47.2 From ecec540251738d27493721005da2908a2bd323f1 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 02:03:30 +0300 Subject: [PATCH 08/11] Improve target validation logic in spell casting function --- lib/spell/spell.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 099ec85..143a456 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -41,7 +41,10 @@ function spell.new(data) } function newSpell:cast(caster, target) - if not self.targetQuery:intersect(Query(targetTest.distance(self.distance))).test(caster, target) then return end -- проверка корректности цели + local targetQuery = self.distance + and self.targetQuery:intersect(Query(targetTest.distance(self.distance))) + or self.targetQuery + if not targetQuery.test(caster, target) then return end -- проверка корректности цели -- проверка на достаточное количество маны if caster:try(Tree.behaviors.stats, function(stats) -- 2.47.2 From d84fc4a7c2462c141b592080d51feb446c579ee9 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 03:53:27 +0300 Subject: [PATCH 09/11] 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 --- lib/spell/spell.lua | 55 ++++++++++++++++++------ lib/spell/target_test.lua | 1 - lib/spellbook.lua | 89 +++++++++++++-------------------------- 3 files changed, 72 insertions(+), 73 deletions(-) diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 143a456..e387a2d 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -1,5 +1,6 @@ local Query = require "lib.spell.target_query" local targetTest = require "lib.spell.target_test" +local task = require "lib.utils.task" --- @alias SpellPreview "default" Подсветка возможных целей --- | "path" Подсветка пути до цели @@ -13,7 +14,7 @@ local targetTest = require "lib.spell.target_test" --- @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 Вызывается в момент каста, изменяет мир. +--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task Вызывается в момент каста, изменяет мир. local spell = {} spell.__index = spell spell.tag = "spell_base" @@ -22,35 +23,63 @@ spell.baseCooldown = 1 spell.targetQuery = Query(targetTest.any) spell.previewType = "default" -function spell:update(caster, dt) end +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() 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 end +function spell:cast(caster, target) return task.fromValue() end --- Конструктор [Spell] ---- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task} +--- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery?, previewType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task} --- @return Spell function spell.new(data) - local newSpell = { + 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) - local targetQuery = self.distance - and self.targetQuery:intersect(Query(targetTest.distance(self.distance))) - or self.targetQuery - if not targetQuery.test(caster, target) then return end -- проверка корректности цели + if not self.targetQuery.test(caster, target) then return task.fromValue() end -- проверка корректности цели -- проверка на достаточное количество маны if caster:try(Tree.behaviors.stats, function(stats) return stats.mana < self.baseCost end) then - return + return task.fromValue() end caster:try(Tree.behaviors.stats, function(stats) @@ -60,7 +89,7 @@ function spell.new(data) return data.onCast(caster, target) end - return setmetatable(newSpell, spell) + return newSpell end return spell diff --git a/lib/spell/target_test.lua b/lib/spell/target_test.lua index 0f99146..ee7a3ed 100644 --- a/lib/spell/target_test.lua +++ b/lib/spell/target_test.lua @@ -20,7 +20,6 @@ return { 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)) - print("dist:", dist) return dist <= radius end) end diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 30e62a6..0a1cc0b 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -7,66 +7,37 @@ --- --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 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 = setmetatable({ - --- @type Deque - path = nil -}, spell) -walk.tag = "dev_move" +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 -function walk:cast(caster, target) - if not caster:try(Tree.behaviors.stats, function(stats) - return stats.mana >= 2 - end) then - return + 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 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 - end - - caster:try(Tree.behaviors.stats, function(stats) - stats.mana = stats.mana - 2 - end) - - local sprite = caster:has(Tree.behaviors.sprite) - assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE") - if not sprite then - return - end - - 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 - love.graphics.setCanvas() - Tree.level.camera:detach() - love.graphics.setColor(1, 1, 1) -end +} local regenerateMana = spell.new { tag = "dev_mana", @@ -101,7 +72,7 @@ local regenerateMana = spell.new { end } -local attack = spell.new { +local attack = spell.new { tag = "dev_attack", baseCooldown = 1, baseCost = 2, @@ -151,7 +122,7 @@ local attack = spell.new { } ---------------------------------------- -local spellbook = { +local spellbook = { walk = walk, regenerateMana = regenerateMana, attack = attack -- 2.47.2 From ec816eb6664d9e74621946f7212146c659f07e21 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 04:05:04 +0300 Subject: [PATCH 10/11] Refactor spell cast and task returns for optional tasks --- lib/level/selector.lua | 18 +++++++++--------- lib/spell/spell.lua | 8 ++++---- lib/spellbook.lua | 8 ++++---- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/level/selector.lua b/lib/level/selector.lua index 3c60a6f..2d100bf 100644 --- a/lib/level/selector.lua +++ b/lib/level/selector.lua @@ -38,16 +38,16 @@ function selector:update(dt) return end local task = b.cast:cast(char, mousePosition) -- в task функция, которая запускает анимацию спелла - if task then - self:lock() - b.state = "running" + if not task then return end -- не получилось скастовать - task( - function(_) -- это коллбэк, который сработает по окончании анимации спелла - b:endCast() - end - ) - end + self:lock() + b.state = "running" + + task( + function(_) -- это коллбэк, который сработает по окончании анимации спелла + b:endCast() + end + ) end) end end diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index e387a2d..337a02c 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -14,7 +14,7 @@ local task = require "lib.utils.task" --- @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 Вызывается в момент каста, изменяет мир. +--- @field cast fun(self: Spell, caster: Character, target: Vec3): Task? Вызывается в момент каста, изменяет мир. local spell = {} spell.__index = spell spell.tag = "spell_base" @@ -56,7 +56,7 @@ 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} +--- @param data {tag: string, baseCost: integer, baseCooldown: integer, targetQuery: SpellTargetQuery?, previewType: SpellPreview?, distance: integer?, onCast: fun(caster: Character, target: Vec3): Task?} --- @return Spell function spell.new(data) local newSpell = setmetatable({ @@ -73,13 +73,13 @@ function spell.new(data) or newSpell.targetQuery function newSpell:cast(caster, target) - if not self.targetQuery.test(caster, target) then return task.fromValue() 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 task.fromValue() + return end caster:try(Tree.behaviors.stats, function(stats) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 0a1cc0b..b9c0960 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -26,13 +26,13 @@ local walk = spell.new { path:pop_front() if path:is_empty() then print("[Walk]: the path is empty", initialPos, target) - return task.fromValue() + return end local sprite = caster:has(Tree.behaviors.sprite) assert(sprite, "[Walk]", "WTF DUDE WHERE'S YOUR SPRITE") if not sprite then - return task.fromValue() + return end return caster:has(Tree.behaviors.tiled):followPath(path) @@ -52,7 +52,7 @@ local regenerateMana = spell.new { end) local sprite = caster:has(Tree.behaviors.sprite) - if not sprite then return task.fromValue() end + if not sprite then return end print(caster.id, "has regenerated mana and gained initiative") local light = require "lib/character/character".spawn("Light Effect") @@ -88,7 +88,7 @@ local attack = spell.new { 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 + if not sprite or not targetSprite then return end caster:try(Tree.behaviors.positioned, function(b) b:lookAt(target) end) -- 2.47.2 From 2e96ec821df4c110c2c7d6fce97bce8cae5ab747 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 18 Mar 2026 05:09:35 +0300 Subject: [PATCH 11/11] 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 --- lib/character/behaviors/ai.lua | 31 +++++++++++++++---------- lib/character/behaviors/spellcaster.lua | 11 +++++++++ lib/level/selector.lua | 3 ++- lib/level/turn_order.lua | 10 ++++++++ lib/simple_ui/level/bar.lua | 2 +- lib/simple_ui/level/end_turn.lua | 2 +- lib/simple_ui/level/skill_row.lua | 30 +++++++++++++++++++++++- lib/spell/spell.lua | 9 +++++++ lib/utils/font_manager.lua | 10 ++++---- lib/utils/priority_queue.lua | 2 +- main.lua | 2 +- 11 files changed, 89 insertions(+), 23 deletions(-) diff --git a/lib/character/behaviors/ai.lua b/lib/character/behaviors/ai.lua index 5aea6d3..b5e7b82 100644 --- a/lib/character/behaviors/ai.lua +++ b/lib/character/behaviors/ai.lua @@ -1,4 +1,4 @@ -local easing = require "lib.utils.easing" +local easing = require "lib.utils.easing" local function closestCharacter(char) local caster = Vec3 {} @@ -39,17 +39,24 @@ function behavior:makeTurn() self.target = Vec3 { b.position.x, b.position.y + 1 } --- @todo тут захардкожено + 1, но мы должны как-то хитро определять с какой стороны обойти end) - spellB.spellbook[1]:cast(self.owner, self.target)(function() - -- здесь мы оказываемся после того, как сходили в первый раз - print("[AI]: finished move 1") - local newTarget = Vec3 { 1, 1 } - -- поэтому позиция персонажа для нового каста пересчитается динамически - spellB.spellbook[1]:cast(self.owner, newTarget)(function() - print("[AI]: finished move 2") - -- дергаем функцию после завершения хода - callback() - end) - end) + local task1 = spellB.spellbook[1]:cast(self.owner, self.target) + if task1 then + task1( + function() + -- здесь мы оказываемся после того, как сходили в первый раз + local newTarget = Vec3 { 1, 1 } + local task2 = spellB.spellbook[1]:cast(self.owner, newTarget) + if task2 then + -- дергаем функцию после завершения хода + task2(callback) + else + callback() + end + end + ) + else + callback() + end end) end end diff --git a/lib/character/behaviors/spellcaster.lua b/lib/character/behaviors/spellcaster.lua index 37c128b..aa0ad60 100644 --- a/lib/character/behaviors/spellcaster.lua +++ b/lib/character/behaviors/spellcaster.lua @@ -1,11 +1,13 @@ --- @class SpellcasterBehavior : Behavior --- @field spellbook Spell[] собственный набор спеллов персонажа --- @field cast Spell | nil ссылка на активный спелл из спеллбука +--- @field cooldowns {[string]: integer} текущий кулдаун спеллов по тегам --- @field state "idle" | "casting" | "running" local behavior = {} behavior.__index = behavior behavior.id = "spellcaster" behavior.state = "idle" +behavior.cooldowns = {} ---@param spellbook Spell[] | nil ---@return SpellcasterBehavior @@ -13,6 +15,7 @@ function behavior.new(spellbook) local spb = require "lib.spellbook" --- @todo временное добавление ходьбы (и читов) всем персонажам local t = {} t.spellbook = spellbook or spb.of { spb.walk, spb.regenerateMana, spb.attack } + t.cooldowns = {} return setmetatable(t, behavior) end @@ -23,6 +26,14 @@ function behavior:endCast() Tree.level.selector:unlock() 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) if Tree.level.selector:deselected() then self.state = "idle" diff --git a/lib/level/selector.lua b/lib/level/selector.lua index 2d100bf..06ec4a9 100644 --- a/lib/level/selector.lua +++ b/lib/level/selector.lua @@ -44,8 +44,9 @@ function selector:update(dt) b.state = "running" task( - function(_) -- это коллбэк, который сработает по окончании анимации спелла + function(_) -- это коллбэк, который сработает по окончании анимации спелла b:endCast() + if not char:has(Tree.behaviors.ai) then self:select(char.id) end -- выделяем персонажа обратно после того, как посмотрели на каст end ) end) diff --git a/lib/level/turn_order.lua b/lib/level/turn_order.lua index a059197..08e9eec 100644 --- a/lib/level/turn_order.lua +++ b/lib/level/turn_order.lua @@ -57,10 +57,20 @@ function turnOrder:next() end) end +--- Производим действия в конце раунда +--- --- Меняем местами очередь сходивших и не сходивших (пустую) function turnOrder:endRound() assert(self.pendingQueue:size() == 0, "[TurnOrder]: tried to end the round before everyone had a turn") 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.current = self.pendingQueue:pop() end diff --git a/lib/simple_ui/level/bar.lua b/lib/simple_ui/level/bar.lua index 9bacb90..35601af 100644 --- a/lib/simple_ui/level/bar.lua +++ b/lib/simple_ui/level/bar.lua @@ -57,7 +57,7 @@ function barElement:draw() love.graphics.setColor(1, 1, 1) --- текст поверх 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)) 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)) diff --git a/lib/simple_ui/level/end_turn.lua b/lib/simple_ui/level/end_turn.lua index 5621311..21b95d7 100644 --- a/lib/simple_ui/level/end_turn.lua +++ b/lib/simple_ui/level/end_turn.lua @@ -22,7 +22,7 @@ function endTurnButton:update(dt) end function endTurnButton:layout() - local font = Tree.fonts:getDefaultTheme():getVariant("headline") + local font = Tree.fonts:getDefaultTheme():getVariant("large") self.text = love.graphics.newText(font, "Завершить ход") self.bounds.width = self.text:getWidth() + 32 self.bounds.height = self.text:getHeight() + 16 diff --git a/lib/simple_ui/level/skill_row.lua b/lib/simple_ui/level/skill_row.lua index 6f7ddc7..b02e5c2 100644 --- a/lib/simple_ui/level/skill_row.lua +++ b/lib/simple_ui/level/skill_row.lua @@ -8,6 +8,7 @@ local UI_SCALE = require "lib.simple_ui.level.scale" --- @field hovered boolean --- @field selected boolean --- @field onClick function? +--- @field getCooldown function? --- @field icon? string local skillButton = setmetatable({}, Element) skillButton.__index = skillButton @@ -18,7 +19,11 @@ function skillButton:update(dt) if self:hitTest(mx, my) then self.hovered = true if Tree.controls:isJustPressed("select") then - if self.onClick then self.onClick() end + local cd = self.getCooldown and self.getCooldown() or 0 + if cd == 0 then + if self.onClick then self.onClick() end + end + Tree.controls:consume("select") end else @@ -29,6 +34,7 @@ end function skillButton:draw() love.graphics.setLineWidth(2) + local cd = self.getCooldown and self.getCooldown() or 0 if not self.icon then 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.rectangle("fill", self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height) 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) end @@ -92,6 +117,9 @@ function skillRow.new(characterId) behavior.cast = nil end end + skb.getCooldown = function() + return behavior.cooldowns[spell.tag] or 0 + end t.children[i] = skb end end) diff --git a/lib/spell/spell.lua b/lib/spell/spell.lua index 337a02c..577b01b 100644 --- a/lib/spell/spell.lua +++ b/lib/spell/spell.lua @@ -73,6 +73,12 @@ function spell.new(data) 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 -- проверка корректности цели -- проверка на достаточное количество маны @@ -86,6 +92,9 @@ function spell.new(data) 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 diff --git a/lib/utils/font_manager.lua b/lib/utils/font_manager.lua index 0bec070..f9c48df 100644 --- a/lib/utils/font_manager.lua +++ b/lib/utils/font_manager.lua @@ -4,11 +4,11 @@ --- @field private _sizes {[FontVariant]: integer} local theme = { _sizes = { - smallest = 10, - small = 12, - medium = 14, - large = 16, - headline = 20, + smallest = 12, + small = 14, + medium = 16, + large = 22, + headline = 32, } } theme.__index = theme diff --git a/lib/utils/priority_queue.lua b/lib/utils/priority_queue.lua index 0097604..4b09c2e 100644 --- a/lib/utils/priority_queue.lua +++ b/lib/utils/priority_queue.lua @@ -1,5 +1,5 @@ ---@class PriorityQueue ----@field private data any[] внутренний массив-куча (индексация с 1) +---@field data any[] внутренний массив-куча (индексация с 1) ---@field private cmp fun(a:any, b:any):boolean компаратор: true, если a выше по приоритету, чем b local PriorityQueue = {} PriorityQueue.__index = PriorityQueue diff --git a/main.lua b/main.lua index a0127ce..25db8f6 100644 --- a/main.lua +++ b/main.lua @@ -111,7 +111,7 @@ function love.draw() testLayout:draw() 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: " .. love.timer.getFPS() .. " lt: " .. lt .. " dt: " .. dt .. " mem: " .. string.format("%.2f MB", collectgarbage("count") / 1000) -- 2.47.2