From 1b12b2c470a270e5a7a4b5ccc4b0ee5bea9a4cb5 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Tue, 16 Sep 2025 23:53:16 +0300 Subject: [PATCH 1/8] initial implementation of character behavior --- lib/character/animation.lua | 2 +- lib/character/behaviors/behavior.lua | 7 ++ lib/character/behaviors/map.lua | 72 ++++++++++++++++ .../{graphics.lua => behaviors/render.lua} | 27 +++--- lib/character/character.lua | 84 ++++++++++++++----- lib/character/logic.lua | 74 ---------------- lib/character/map_logic.lua | 24 ------ lib/level/grid/character_grid.lua | 14 +++- lib/level/level.lua | 2 +- lib/spellbook.lua | 2 +- lib/tree.lua | 12 ++- lib/ui/layout.lua | 1 + main.lua | 15 +--- 13 files changed, 181 insertions(+), 155 deletions(-) create mode 100644 lib/character/behaviors/behavior.lua create mode 100644 lib/character/behaviors/map.lua rename lib/character/{graphics.lua => behaviors/render.lua} (51%) delete mode 100644 lib/character/logic.lua delete mode 100644 lib/character/map_logic.lua diff --git a/lib/character/animation.lua b/lib/character/animation.lua index 94621d0..53a3628 100644 --- a/lib/character/animation.lua +++ b/lib/character/animation.lua @@ -14,7 +14,7 @@ RIGHT = 1 local animation = {} animation.__index = animation -local function new(id, spriteDir) +local function new(spriteDir) local anim = { animationTable = {}, animationGrid = {} diff --git a/lib/character/behaviors/behavior.lua b/lib/character/behaviors/behavior.lua new file mode 100644 index 0000000..1dd5ac0 --- /dev/null +++ b/lib/character/behaviors/behavior.lua @@ -0,0 +1,7 @@ +--- Поведение персонажа. Их можно комбинировать как угодно, добавлять и заменять на лету... +--- @class Behavior +--- @field id string +--- @field owner Character +--- @field dependencies Behavior[] +--- @field update fun(self, dt): nil +--- @field draw fun(self): nil diff --git a/lib/character/behaviors/map.lua b/lib/character/behaviors/map.lua new file mode 100644 index 0000000..07a2df8 --- /dev/null +++ b/lib/character/behaviors/map.lua @@ -0,0 +1,72 @@ +local utils = require "lib.utils.utils" + +--- Отвечает за размещение и перемещение по локации +--- @class MapBehavior : Behavior +--- @field position Vec3 +--- @field runTarget Vec3 точка, в которую в данный момент бежит персонаж +--- @field displayedPosition Vec3 точка, в которой персонаж отображается +--- @field t0 number время начала движения для анимациии +--- @field path Deque путь, по которому сейчас бежит персонаж +--- @field size Vec3 +local mapBehavior = {} +mapBehavior.__index = mapBehavior +mapBehavior.id = "map" + + +--- @param position? Vec3 +--- @param size? Vec3 +function mapBehavior.new(position, size) + return setmetatable({ + position = position or Vec3({}), + displayedPosition = position or Vec3({}), + size = size or Vec3({ 1, 1 }), + }, mapBehavior) +end + +--- @param path Deque +function mapBehavior:followPath(path) + if path:is_empty() then return end + self.position = self.displayedPosition + self.owner:setState("run") + self.path = path; + ---@type Vec3 + local nextCell = path:peek_front() + self:runTo(nextCell) + path:pop_front() +end + +--- @param target Vec3 +function mapBehavior:runTo(target) + self.t0 = love.timer.getTime() + self.runTarget = target + local charPos = self.position + local render = self.owner:has(Tree.behaviors.render) + if not render then return end + if target.x < charPos.x then + render.animation.side = LEFT + elseif target.x > charPos.x then + render.animation.side = RIGHT + end +end + +function mapBehavior:update(dt) + if self.owner.state == "run" and self.runTarget then + local delta = love.timer.getTime() - self.t0 or love.timer.getTime() + local fraction = delta / + (0.5 * self.runTarget:subtract(self.position):length()) -- бежим одну клетку за 500 мс, по диагонали больше + if fraction >= 1 then -- анимация перемещена завершена + self.position = self.runTarget + if not self.path:is_empty() then -- еще есть, куда бежать + self:runTo(self.path:peek_front()) + self.path:pop_front() + else -- мы добежали до финальной цели + self.owner:setState("idle") + self.runTarget = nil + end + else -- анимация перемещения не завершена + self.displayedPosition = utils.lerp(self.position, self.runTarget, fraction) -- линейный интерполятор + end + end +end + +return mapBehavior diff --git a/lib/character/graphics.lua b/lib/character/behaviors/render.lua similarity index 51% rename from lib/character/graphics.lua rename to lib/character/behaviors/render.lua index f966431..bc43b98 100644 --- a/lib/character/graphics.lua +++ b/lib/character/behaviors/render.lua @@ -1,25 +1,26 @@ ---- @class Graphics ---- @field owner Character +--- @class DrawBehavior : Behavior --- @field animation Animation -local graphics = {} -graphics.__index = graphics +local renderBehavior = {} +renderBehavior.__index = renderBehavior +renderBehavior.id = "render" +renderBehavior.dependencies = { Tree.behaviors.map } + ---- @param id Id --- @param spriteDir table -local function new(id, spriteDir) +function renderBehavior.new(spriteDir) return setmetatable({ - id = id, - animation = (require 'lib.character.animation').new(id, spriteDir) - }, graphics) + animation = (require 'lib.character.animation').new(spriteDir) + }, renderBehavior) end -function graphics:update(dt) +function renderBehavior:update(dt) self.animation.animationTable[self.owner:getState()]:update(dt) end -function graphics:draw() +function renderBehavior:draw() local ppm = Tree.level.camera.pixelsPerMeter - local position = self.owner.logic.mapLogic.displayedPosition + if not self.owner:has(Tree.behaviors.map) then return end + local position = self.owner:has(Tree.behaviors.map).displayedPosition local state = self.owner:getState() if Tree.level.selector.id == self.owner.id then love.graphics.setColor(0.5, 1, 0.5) end @@ -30,4 +31,4 @@ function graphics:draw() love.graphics.setColor(1, 1, 1) end -return { new = new } +return renderBehavior diff --git a/lib/character/character.lua b/lib/character/character.lua index 98d0a29..7ab99d0 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -1,5 +1,6 @@ require 'lib.utils.vec3' +--- @alias CharacterState "idle"|"run"|"attack"|"hurt" --- @alias Id integer --- @type Id @@ -8,9 +9,8 @@ local characterId = 1 --- @todo Композиция лучше наследования, но не до такой же степени! Надо отрефакторить и избавиться от сотни полей в таблице --- @class Character --- @field id Id ---- @field info Info ---- @field graphics Graphics ---- @field logic Logic +--- @field behaviors Behavior[] +--- @field _behaviorsIdx {string: integer} --- @field spellbook Spell[] собственный набор спеллов персонажа --- @field cast Spell | nil ссылка на активный спелл из спеллбука local character = {} @@ -26,50 +26,94 @@ character.__index = character local function spawn(name, template, spriteDir, position, size, level) local char = {} + char = setmetatable(char, character) char.id = characterId characterId = characterId + 1 - char = setmetatable(char, character) + char.behaviors = {} + char._behaviorsIdx = {} + + char:addBehavior { + Tree.behaviors.map.new(position, size), + Tree.behaviors.render.new(spriteDir), + } + char:setState("idle") --- @todo сделать это отдельным модулем + local spb = require "lib.spellbook" --- @todo это тоже + char.spellbook = spb.of { spb.walk } - char:addModules( - { - logic = (require 'lib.character.logic').new(char.id, position, size), - graphics = (require 'lib.character.graphics').new(char.id, spriteDir), - info = (require "lib/character/info").new(name, template, level) - } - ) Tree.level.characters[char.id] = char return char end +--- Проверяет, есть ли у персонажа поведение [behavior]. +--- @generic T : Behavior +--- @param behavior T +--- @return T | nil +function character:has(behavior) + --- @cast behavior Behavior + local idx = self._behaviorsIdx[behavior.id] + if not idx then return nil end + return self.behaviors[idx] or nil +end + --- usage: --- addModules( {logic = logic.new(), graphics = graphics.new(), ...} ) -function character:addModules(modules) - for key, module in pairs(modules) do +--- +--- or you may chain this if you are a wannabe haskell kiddo +function character:addBehavior(modules) + for _, module in ipairs(modules) do + if module.dependencies then + for _, dep in ipairs(module.dependencies) do + if not self:has(dep) then + return print("[Character]: cannot add \"" .. module.id .. + "\" for a character (Id = " .. self.id .. "): needs \"" .. dep.id .. "\"!") + end + end + end module.owner = self - self[key] = module + table.insert(self.behaviors, module) + self._behaviorsIdx[module.id] = #self.behaviors end + return self end --- геттеры и сеттеры для "внешних" данных +--- забей, это в поведения +--- @deprecated --- @return CharacterState function character:getState() - return self.logic.state or "idle" + return self.state or "idle" +end + +--- @param state CharacterState +--- @deprecated +function character:setState(state) --- @todo это вообще должно быть отдельное поведение + self.state = state + self:has(Tree.behaviors.render).animation:setState(state, (state ~= "idle" and state ~= "run") and function() + self:setState("idle") + end or nil) end --- @param path Deque function character:followPath(path) - self.logic:followPath(path) + self:has(Tree.behaviors.map):followPath(path) end function character:update(dt) - self.logic:update(dt) + --- @todo ну ты понел + -- for _, b in ipairs(self.behaviors) do + -- if b.update then b:update(dt) end + -- end + self:has(Tree.behaviors.map):update(dt) if self.cast then self.cast:update(self, dt) end - self.graphics:update(dt) + self:has(Tree.behaviors.render):update(dt) end function character:draw() - self.graphics:draw() - if self.cast then self.cast:draw() end + for _, b in ipairs(self.behaviors) do + if b.draw then b:draw() end + end + + if self.cast then self.cast:draw() end --- @todo 🤡 end return { spawn = spawn } diff --git a/lib/character/logic.lua b/lib/character/logic.lua deleted file mode 100644 index e5e0564..0000000 --- a/lib/character/logic.lua +++ /dev/null @@ -1,74 +0,0 @@ -local utils = require "lib.utils.utils" - ---- @alias CharacterState "idle"|"run"|"attack"|"hurt" - ---- @class Logic ---- @field owner Character ---- @field mapLogic MapLogic ---- @field state CharacterState -local logic = {} -logic.__index = logic - ---- @param id Id ---- @param position? Vec3 ---- @param size? Vec3 -local function new(id, position, size) - return setmetatable({ - id = id, - mapLogic = (require 'lib.character.map_logic').new(id, position, size), - state = "idle" - }, logic) -end - ---- @param state CharacterState -function logic:setState(state) - self.state = state - self.owner.graphics.animation:setState(state, (state ~= "idle" and state ~= "run") and function() - self:setState("idle") - end or nil) -end - ---- @param path Deque -function logic:followPath(path) - if path:is_empty() then return end - self:setState("run") - self.mapLogic.path = path; - ---@type Vec3 - local nextCell = path:peek_front() - self:runTo(nextCell) - path:pop_front() -end - ---- @param target Vec3 -function logic:runTo(target) - self.mapLogic.t0 = love.timer.getTime() - self.mapLogic.runTarget = target - local charPos = self.mapLogic.position - if target.x < charPos.x then - self.owner.graphics.animation.side = LEFT - elseif target.x > charPos.x then - self.owner.graphics.animation.side = RIGHT - end -end - -function logic:update(dt) - if self.state == "run" and self.mapLogic.runTarget then - local delta = love.timer.getTime() - self.mapLogic.t0 or love.timer.getTime() - local fraction = delta / - (0.5 * self.mapLogic.runTarget:subtract(self.mapLogic.position):length()) -- бежим одну клетку за 500 мс, по диагонали больше - if fraction >= 1 then -- анимация перемещена завершена - self.mapLogic.position = self.mapLogic.runTarget - if not self.mapLogic.path:is_empty() then -- еще есть, куда бежать - self:runTo(self.mapLogic.path:peek_front()) - self.mapLogic.path:pop_front() - else -- мы добежали до финальной цели - self:setState("idle") - self.mapLogic.runTarget = nil - end - else -- анимация перемещения не завершена - self.mapLogic.displayedPosition = utils.lerp(self.mapLogic.position, self.mapLogic.runTarget, fraction) -- линейный интерполятор - end - end -end - -return { new = new } diff --git a/lib/character/map_logic.lua b/lib/character/map_logic.lua deleted file mode 100644 index 805af01..0000000 --- a/lib/character/map_logic.lua +++ /dev/null @@ -1,24 +0,0 @@ ---- @class MapLogic ---- @field id Id ---- @field position Vec3 ---- @field runTarget Vec3 точка, в которую в данный момент бежит персонаж ---- @field displayedPosition Vec3 точка, в которой персонаж отображается ---- @field t0 number время начала движения для анимациии ---- @field path Deque путь, по которому сейчас бежит персонаж ---- @field size Vec3 -local mapLogic = {} - ---- @param id Id ---- @param position? Vec3 ---- @param size? Vec3 -local function new(id, position, size) - return setmetatable({ - id = id, - position = position or Vec3({}), - displayedPosition = position or Vec3({}), - size = size or Vec3({ 1, 1 }), - path = (require "lib.utils.deque").new() - }, mapLogic) -end - -return { new = new } diff --git a/lib/level/grid/character_grid.lua b/lib/level/grid/character_grid.lua index 0192f1e..b21772b 100644 --- a/lib/level/grid/character_grid.lua +++ b/lib/level/grid/character_grid.lua @@ -12,9 +12,12 @@ function grid:add(id) local character = Tree.level.characters[id] if not character then return end - local centerX, centerY = math.floor(character.logic.mapLogic.position.x + 0.5), - math.floor(character.logic.mapLogic.position.y + 0.5) - local sizeX, sizeY = character.logic.mapLogic.size.x, character.logic.mapLogic.size.y + local mapB = character:has(Tree.behaviors.map) + if not mapB then return end + + local centerX, centerY = math.floor(mapB.displayedPosition.x + 0.5), + math.floor(mapB.displayedPosition.y + 0.5) + local sizeX, sizeY = mapB.size.x, mapB.size.y for y = centerY, centerY + sizeY - 1 do for x = centerX, centerX + sizeX - 1 do @@ -26,7 +29,10 @@ end --- @param a Character --- @param b Character local function drawCmp(a, b) - return a.logic.mapLogic.displayedPosition.y < b.logic.mapLogic.displayedPosition.y + -- здесь персонажи гарантированно имеют нужное поведение + return a:has(Tree.behaviors.map).displayedPosition.y + < + b:has(Tree.behaviors.map).displayedPosition.y end --- fills the grid with the actual data diff --git a/lib/level/level.lua b/lib/level/level.lua index 6362402..eb182e9 100644 --- a/lib/level/level.lua +++ b/lib/level/level.lua @@ -39,7 +39,7 @@ end function level:draw() self.tileGrid:draw() - while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n, но линейное + while not self.characterGrid.yOrderQueue:is_empty() do -- по сути это сортировка кучей за n log n self.characterGrid.yOrderQueue:pop():draw() end end diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 67d7981..6ace2cc 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -26,7 +26,7 @@ function walk:cast(caster, target) end function walk:update(caster, dt) - local charPos = caster.logic.mapLogic.position:floor() + local charPos = caster:has(Tree.behaviors.map).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) diff --git a/lib/tree.lua b/lib/tree.lua index ff01584..7e8197f 100644 --- a/lib/tree.lua +++ b/lib/tree.lua @@ -3,9 +3,13 @@ --- В love.update обновлять, в love.draw читать -Tree = { +Tree = { assets = (require "lib.utils.asset_bundle"):load() } -Tree.panning = require "lib/panning" -Tree.controls = require "lib.controls" -Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен +Tree.panning = require "lib/panning" +Tree.controls = require "lib.controls" +Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен + +Tree.behaviors = {} --- @todo написать нормальную загрузку поведений +Tree.behaviors.map = require "lib.character.behaviors.map" +Tree.behaviors.render = require "lib.character.behaviors.render" diff --git a/lib/ui/layout.lua b/lib/ui/layout.lua index dcd5ea0..19f687d 100644 --- a/lib/ui/layout.lua +++ b/lib/ui/layout.lua @@ -13,6 +13,7 @@ function SkillButton:update(dt) ui.Rectangle.update(self, dt) self.color = self.owner.cast and { 0, 1, 0 } or { 1, 0, 0 } self:onTap(function() + print(self.owner.spellbook[self.spellId]) self.owner.cast = self.owner.cast and nil or self.owner.spellbook[self.spellId] end) end diff --git a/main.lua b/main.lua index e98f845..47e2dad 100644 --- a/main.lua +++ b/main.lua @@ -3,25 +3,14 @@ local character = require "lib/character/character" require "lib/tree" local layout = require "lib.ui.layout" -local spellbook = require "lib.spellbook" + function love.conf(t) t.console = true end function love.load() - for x = 0, 29, 1 do - for y = 0, 29, 1 do - if math.random() > 0.8 then - local c = character.spawn("Hero", "warrior", Tree.assets.files.sprites.character) - c.logic.mapLogic.position = Vec3 { x, y } - c.logic.mapLogic.displayedPosition = Vec3 { x, y } - c.spellbook = spellbook.of { spellbook.walk } - c.logic:setState("attack") - end - end - end - + character.spawn("Hero", "warrior", Tree.assets.files.sprites.character) love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true }) end From 947787ff44c8dc29c888599ab05d65bac44e7942 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Wed, 17 Sep 2025 13:21:50 +0300 Subject: [PATCH 2/8] implement character:try --- lib/character/behaviors/map.lua | 17 +++++++++-------- lib/character/behaviors/render.lua | 21 ++++++++++++--------- lib/character/character.lua | 22 +++++++++++++++++++++- 3 files changed, 42 insertions(+), 18 deletions(-) diff --git a/lib/character/behaviors/map.lua b/lib/character/behaviors/map.lua index 07a2df8..2bd8565 100644 --- a/lib/character/behaviors/map.lua +++ b/lib/character/behaviors/map.lua @@ -39,14 +39,15 @@ end function mapBehavior:runTo(target) self.t0 = love.timer.getTime() self.runTarget = target - local charPos = self.position - local render = self.owner:has(Tree.behaviors.render) - if not render then return end - if target.x < charPos.x then - render.animation.side = LEFT - elseif target.x > charPos.x then - render.animation.side = RIGHT - end + self.owner:try(Tree.behaviors.render, + function(render) + if target.x < self.position.x then + render.animation.side = LEFT + elseif target.x > self.position.x then + render.animation.side = RIGHT + end + end + ) end function mapBehavior:update(dt) diff --git a/lib/character/behaviors/render.lua b/lib/character/behaviors/render.lua index bc43b98..73066bb 100644 --- a/lib/character/behaviors/render.lua +++ b/lib/character/behaviors/render.lua @@ -18,17 +18,20 @@ function renderBehavior:update(dt) end function renderBehavior:draw() - local ppm = Tree.level.camera.pixelsPerMeter - if not self.owner:has(Tree.behaviors.map) then return end - local position = self.owner:has(Tree.behaviors.map).displayedPosition - local state = self.owner:getState() + self.owner:try(Tree.behaviors.map, + function(map) + local ppm = Tree.level.camera.pixelsPerMeter + local position = map.displayedPosition + local state = self.owner:getState() - if Tree.level.selector.id == self.owner.id then love.graphics.setColor(0.5, 1, 0.5) end + if Tree.level.selector.id == self.owner.id then love.graphics.setColor(0.5, 1, 0.5) end - self.animation.animationTable[state]:draw(Tree.assets.files.sprites.character[state], - position.x + 0.5, - position.y + 0.5, nil, 1 / ppm * self.animation.side, 1 / ppm, 38, 47) - love.graphics.setColor(1, 1, 1) + self.animation.animationTable[state]:draw(Tree.assets.files.sprites.character[state], + position.x + 0.5, + position.y + 0.5, nil, 1 / ppm * self.animation.side, 1 / ppm, 38, 47) + love.graphics.setColor(1, 1, 1) + end + ) end return renderBehavior diff --git a/lib/character/character.lua b/lib/character/character.lua index 7ab99d0..cf709d3 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -55,8 +55,28 @@ function character:has(behavior) return self.behaviors[idx] or nil end +--- Если у персонажа есть поведение [behavior], применяет к нему [fn] +--- +--- Дальше meme для интеллектуалов +--- +--- *Я: мам купи мне >>=* +--- +--- *Мама: у нас дома есть >>=* +--- +--- *Дома:* +--- @generic T : Behavior +--- @generic V +--- @param behavior T +--- @param fn fun(behavior: T) : V | nil +--- @return V | nil +function character:try(behavior, fn) + local b = self:has(behavior) + if not b then return end + return fn(b) +end + --- usage: ---- addModules( {logic = logic.new(), graphics = graphics.new(), ...} ) +--- addModules( {logic.new(), graphics.new(), ...} ) --- --- or you may chain this if you are a wannabe haskell kiddo function character:addBehavior(modules) From c4dfb5956d8b446c84932d5d8fb94370daeaf3af Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 28 Sep 2025 23:34:17 +0300 Subject: [PATCH 3/8] add spellcaster behavior --- lib/character/behaviors/spellcaster.lua | 25 +++++++++++++++++++++++++ lib/character/character.lua | 17 ++++++----------- lib/level/selector.lua | 9 +++++---- lib/spellbook.lua | 3 +-- lib/tree.lua | 15 ++++++++------- lib/ui/layout.lua | 9 +++++---- 6 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 lib/character/behaviors/spellcaster.lua diff --git a/lib/character/behaviors/spellcaster.lua b/lib/character/behaviors/spellcaster.lua new file mode 100644 index 0000000..2195b8f --- /dev/null +++ b/lib/character/behaviors/spellcaster.lua @@ -0,0 +1,25 @@ +--- @class SpellcasterBehavior : Behavior +--- @field spellbook Spell[] собственный набор спеллов персонажа +--- @field cast Spell | nil ссылка на активный спелл из спеллбука +local behavior = {} +behavior.__index = behavior +behavior.id = "spellcaster" + +---@param spellbook Spell[] | nil +---@return SpellcasterBehavior +function behavior.new(spellbook) + local spb = require "lib.spellbook" --- @todo временное добавление ходьбы всем персонажам + local t = {} + t.spellbook = spellbook or spb.of { spb.walk } + return setmetatable(t, behavior) +end + +function behavior:update(dt) + if self.cast then self.cast:update(self.owner, dt) end +end + +function behavior:draw() + if self.cast then self.cast:draw() end +end + +return behavior diff --git a/lib/character/character.lua b/lib/character/character.lua index cf709d3..57205c8 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -35,10 +35,10 @@ local function spawn(name, template, spriteDir, position, size, level) char:addBehavior { Tree.behaviors.map.new(position, size), Tree.behaviors.render.new(spriteDir), + Tree.behaviors.spellcaster.new() } - char:setState("idle") --- @todo сделать это отдельным модулем - local spb = require "lib.spellbook" --- @todo это тоже - char.spellbook = spb.of { spb.walk } + char:setState("idle") --- @todo сделать это отдельным модулем + Tree.level.characters[char.id] = char return char @@ -120,20 +120,15 @@ end function character:update(dt) --- @todo ну ты понел - -- for _, b in ipairs(self.behaviors) do - -- if b.update then b:update(dt) end - -- end - self:has(Tree.behaviors.map):update(dt) - if self.cast then self.cast:update(self, dt) end - self:has(Tree.behaviors.render):update(dt) + for _, b in ipairs(self.behaviors) do + if b.update then b:update(dt) end + end end function character:draw() for _, b in ipairs(self.behaviors) do if b.draw then b:draw() end end - - if self.cast then self.cast:draw() end --- @todo 🤡 end return { spawn = spawn } diff --git a/lib/level/selector.lua b/lib/level/selector.lua index 2814ea3..8305a3e 100644 --- a/lib/level/selector.lua +++ b/lib/level/selector.lua @@ -25,10 +25,11 @@ function selector:update(dt) if not characterId and self.id then -- Когда кликаем по тайлу за персонажа в режиме каста, кастуем спелл local char = Tree.level.characters[self.id] - if char.cast then - char.cast:cast(char, mousePosition) - char.cast = nil - end + char:try(Tree.behaviors.spellcaster, function(b) + if not b.cast then return end + b.cast:cast(char, mousePosition) + b.cast = nil + end) end self:select(characterId) diff --git a/lib/spellbook.lua b/lib/spellbook.lua index 6ace2cc..0854045 100644 --- a/lib/spellbook.lua +++ b/lib/spellbook.lua @@ -17,12 +17,11 @@ local walk = setmetatable({ }, spell) function walk:cast(caster, target) - caster.cast = nil local path = self.path path:pop_front() print("Following path: ") for p in path:values() do print(p) end - caster:followPath(path) + caster:has(Tree.behaviors.map):followPath(path) end function walk:update(caster, dt) diff --git a/lib/tree.lua b/lib/tree.lua index 7e8197f..3bd5858 100644 --- a/lib/tree.lua +++ b/lib/tree.lua @@ -3,13 +3,14 @@ --- В love.update обновлять, в love.draw читать -Tree = { +Tree = { assets = (require "lib.utils.asset_bundle"):load() } -Tree.panning = require "lib/panning" -Tree.controls = require "lib.controls" -Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен +Tree.panning = require "lib/panning" +Tree.controls = require "lib.controls" +Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен -Tree.behaviors = {} --- @todo написать нормальную загрузку поведений -Tree.behaviors.map = require "lib.character.behaviors.map" -Tree.behaviors.render = require "lib.character.behaviors.render" +Tree.behaviors = {} --- @todo написать нормальную загрузку поведений +Tree.behaviors.map = require "lib.character.behaviors.map" +Tree.behaviors.render = require "lib.character.behaviors.render" +Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster" diff --git a/lib/ui/layout.lua b/lib/ui/layout.lua index 19f687d..3f0e03f 100644 --- a/lib/ui/layout.lua +++ b/lib/ui/layout.lua @@ -11,10 +11,11 @@ local SkillButton = ui.Rectangle { } function SkillButton:update(dt) ui.Rectangle.update(self, dt) - self.color = self.owner.cast and { 0, 1, 0 } or { 1, 0, 0 } - self:onTap(function() - print(self.owner.spellbook[self.spellId]) - self.owner.cast = self.owner.cast and nil or self.owner.spellbook[self.spellId] + self.owner:try(Tree.behaviors.spellcaster, function(spellcaster) + self.color = spellcaster.cast and { 0, 1, 0 } or { 1, 0, 0 } + self:onTap(function() + spellcaster.cast = spellcaster.cast and nil or spellcaster.spellbook[self.spellId] + end) end) end From 9fe2276d0471e4e4aa121f9c09dba16f0107f196 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 28 Sep 2025 23:39:41 +0300 Subject: [PATCH 4/8] remove deprecated fields related to spellcasting in Character --- lib/character/character.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/character/character.lua b/lib/character/character.lua index 57205c8..d124a7b 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -11,8 +11,6 @@ local characterId = 1 --- @field id Id --- @field behaviors Behavior[] --- @field _behaviorsIdx {string: integer} ---- @field spellbook Spell[] собственный набор спеллов персонажа ---- @field cast Spell | nil ссылка на активный спелл из спеллбука local character = {} character.__index = character From dd84e157bddf658e8830ab4110a94e5268acb880 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Sun, 28 Sep 2025 23:47:50 +0300 Subject: [PATCH 5/8] refactor Character:addBehavior --- lib/character/character.lua | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/character/character.lua b/lib/character/character.lua index d124a7b..0c40abb 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -74,22 +74,24 @@ function character:try(behavior, fn) end --- usage: ---- addModules( {logic.new(), graphics.new(), ...} ) +--- addBehavior( {logic.new(), graphics.new(), ...} ) --- --- or you may chain this if you are a wannabe haskell kiddo -function character:addBehavior(modules) - for _, module in ipairs(modules) do - if module.dependencies then - for _, dep in ipairs(module.dependencies) do +--- @param behaviors Behavior[] +--- @return Character | nil +function character:addBehavior(behaviors) + for _, b in ipairs(behaviors) do + if b.dependencies then + for _, dep in ipairs(b.dependencies) do if not self:has(dep) then - return print("[Character]: cannot add \"" .. module.id .. + return print("[Character]: cannot add \"" .. b.id .. "\" for a character (Id = " .. self.id .. "): needs \"" .. dep.id .. "\"!") end end end - module.owner = self - table.insert(self.behaviors, module) - self._behaviorsIdx[module.id] = #self.behaviors + b.owner = self + table.insert(self.behaviors, b) + self._behaviorsIdx[b.id] = #self.behaviors end return self end From 99d523a761f5b030caa968ead5be1f97c9257ef8 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Mon, 29 Sep 2025 04:30:55 +0300 Subject: [PATCH 6/8] - reimplement animation as AnimatedBehavior - remove deprecated in Character - remove RenderBehavior --- lib/character/animation.lua | 46 ------------------ lib/character/behaviors/animated.lua | 70 ++++++++++++++++++++++++++++ lib/character/behaviors/map.lua | 18 ++++--- lib/character/behaviors/render.lua | 37 --------------- lib/character/character.lua | 29 +----------- lib/tree.lua | 4 +- 6 files changed, 84 insertions(+), 120 deletions(-) delete mode 100644 lib/character/animation.lua create mode 100644 lib/character/behaviors/animated.lua delete mode 100644 lib/character/behaviors/render.lua diff --git a/lib/character/animation.lua b/lib/character/animation.lua deleted file mode 100644 index 53a3628..0000000 --- a/lib/character/animation.lua +++ /dev/null @@ -1,46 +0,0 @@ -local anim8 = require "lib.utils.anim8" - ---- Скорость между кадрами в анимации -local ANIMATION_SPEED = 0.1 - -LEFT = -1 -RIGHT = 1 - ---- @class Animation ---- @field animationTable table ---- @field animationGrid table ---- @field state "idle"|"run"|"hurt"|"attack" ---- @field side 1|-1 -local animation = {} -animation.__index = animation - -local function new(spriteDir) - local anim = { - animationTable = {}, - animationGrid = {} - } - - -- n: name; i: image - for n, i in pairs(spriteDir) do - local aGrid = anim8.newGrid(96, 64, i:getWidth(), i:getHeight()) - local tiles = '1-' .. math.ceil(i:getWidth() / 96) - anim.animationGrid[n] = aGrid(tiles, 1) - end - - anim.state = "idle" - anim.side = RIGHT - - return setmetatable(anim, animation) -end - -function animation:getState() - return self.state -end - ---- @param state CharacterState -function animation:setState(state, onLoop) - self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], ANIMATION_SPEED, onLoop) - self.state = state -end - -return { new = new } diff --git a/lib/character/behaviors/animated.lua b/lib/character/behaviors/animated.lua new file mode 100644 index 0000000..d3784ea --- /dev/null +++ b/lib/character/behaviors/animated.lua @@ -0,0 +1,70 @@ +local anim8 = require "lib.utils.anim8" + +--- @class AnimatedBehavior : Behavior +--- @field animationTable table +--- @field animationGrid table +--- @field state "idle"|"run"|"hurt"|"attack" +--- @field side 1|-1 +local animated = {} +animated.__index = animated +animated.id = "animated" +animated.dependencies = { Tree.behaviors.map } +animated.LEFT = -1 +animated.RIGHT = 1 +--- Скорость между кадрами в анимации +animated.ANIMATION_SPEED = 0.1 + +function animated.new(spriteDir) + local anim = setmetatable({}, animated) + anim.animationTable = {} + anim.animationGrid = {} + + -- n: name; i: image + for n, i in pairs(spriteDir) do + local aGrid = anim8.newGrid(96, 64, i:getWidth(), i:getHeight()) + local tiles = '1-' .. math.ceil(i:getWidth() / 96) + anim.animationGrid[n] = aGrid(tiles, 1) + end + + anim.state = "idle" + anim.side = animated.RIGHT + anim:play("idle") + return anim +end + +function animated:update(dt) + local anim = self.animationTable[self.state] or self.animationTable["idle"] or nil + + if not anim then return end + anim:update(dt) +end + +function animated:draw() + if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end + + self.owner:try(Tree.behaviors.map, + function(map) + local ppm = Tree.level.camera.pixelsPerMeter + local position = map.displayedPosition + if Tree.level.selector.id == self.owner.id then love.graphics.setColor(0.5, 1, 0.5) end + self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], + position.x + 0.5, + position.y + 0.5, nil, 1 / ppm * self.side, 1 / ppm, 38, 47) + love.graphics.setColor(1, 1, 1) + end + ) +end + +--- @param state string +--- @param loop boolean | nil +function animated:play(state, loop) + if not self.animationGrid[state] then + return print("[AnimatedBehavior]: no animation for '" .. state .. "'") + end + self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED, function() + if not loop then self:play("idle", true) end + end) + self.state = state +end + +return animated diff --git a/lib/character/behaviors/map.lua b/lib/character/behaviors/map.lua index 2bd8565..c1573ca 100644 --- a/lib/character/behaviors/map.lua +++ b/lib/character/behaviors/map.lua @@ -27,7 +27,9 @@ end function mapBehavior:followPath(path) if path:is_empty() then return end self.position = self.displayedPosition - self.owner:setState("run") + self.owner:try(Tree.behaviors.animated, function(animated) + animated:play("run", true) + end) self.path = path; ---@type Vec3 local nextCell = path:peek_front() @@ -39,19 +41,19 @@ end function mapBehavior:runTo(target) self.t0 = love.timer.getTime() self.runTarget = target - self.owner:try(Tree.behaviors.render, - function(render) + self.owner:try(Tree.behaviors.animated, + function(animated) if target.x < self.position.x then - render.animation.side = LEFT + animated.side = Tree.behaviors.animated.LEFT elseif target.x > self.position.x then - render.animation.side = RIGHT + animated.side = Tree.behaviors.animated.RIGHT end end ) end function mapBehavior:update(dt) - if self.owner.state == "run" and self.runTarget then + if self.runTarget then local delta = love.timer.getTime() - self.t0 or love.timer.getTime() local fraction = delta / (0.5 * self.runTarget:subtract(self.position):length()) -- бежим одну клетку за 500 мс, по диагонали больше @@ -61,7 +63,9 @@ function mapBehavior:update(dt) self:runTo(self.path:peek_front()) self.path:pop_front() else -- мы добежали до финальной цели - self.owner:setState("idle") + self.owner:try(Tree.behaviors.animated, function(animated) + animated:play("idle", true) + end) self.runTarget = nil end else -- анимация перемещения не завершена diff --git a/lib/character/behaviors/render.lua b/lib/character/behaviors/render.lua deleted file mode 100644 index 73066bb..0000000 --- a/lib/character/behaviors/render.lua +++ /dev/null @@ -1,37 +0,0 @@ ---- @class DrawBehavior : Behavior ---- @field animation Animation -local renderBehavior = {} -renderBehavior.__index = renderBehavior -renderBehavior.id = "render" -renderBehavior.dependencies = { Tree.behaviors.map } - - ---- @param spriteDir table -function renderBehavior.new(spriteDir) - return setmetatable({ - animation = (require 'lib.character.animation').new(spriteDir) - }, renderBehavior) -end - -function renderBehavior:update(dt) - self.animation.animationTable[self.owner:getState()]:update(dt) -end - -function renderBehavior:draw() - self.owner:try(Tree.behaviors.map, - function(map) - local ppm = Tree.level.camera.pixelsPerMeter - local position = map.displayedPosition - local state = self.owner:getState() - - if Tree.level.selector.id == self.owner.id then love.graphics.setColor(0.5, 1, 0.5) end - - self.animation.animationTable[state]:draw(Tree.assets.files.sprites.character[state], - position.x + 0.5, - position.y + 0.5, nil, 1 / ppm * self.animation.side, 1 / ppm, 38, 47) - love.graphics.setColor(1, 1, 1) - end - ) -end - -return renderBehavior diff --git a/lib/character/character.lua b/lib/character/character.lua index 0c40abb..9968731 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -1,12 +1,10 @@ require 'lib.utils.vec3' ---- @alias CharacterState "idle"|"run"|"attack"|"hurt" --- @alias Id integer --- @type Id local characterId = 1 ---- @todo Композиция лучше наследования, но не до такой же степени! Надо отрефакторить и избавиться от сотни полей в таблице --- @class Character --- @field id Id --- @field behaviors Behavior[] @@ -32,11 +30,9 @@ local function spawn(name, template, spriteDir, position, size, level) char:addBehavior { Tree.behaviors.map.new(position, size), - Tree.behaviors.render.new(spriteDir), + Tree.behaviors.animated.new(spriteDir), Tree.behaviors.spellcaster.new() } - char:setState("idle") --- @todo сделать это отдельным модулем - Tree.level.characters[char.id] = char return char @@ -96,30 +92,7 @@ function character:addBehavior(behaviors) return self end ---- геттеры и сеттеры для "внешних" данных ---- забей, это в поведения ---- @deprecated ---- @return CharacterState -function character:getState() - return self.state or "idle" -end - ---- @param state CharacterState ---- @deprecated -function character:setState(state) --- @todo это вообще должно быть отдельное поведение - self.state = state - self:has(Tree.behaviors.render).animation:setState(state, (state ~= "idle" and state ~= "run") and function() - self:setState("idle") - end or nil) -end - ---- @param path Deque -function character:followPath(path) - self:has(Tree.behaviors.map):followPath(path) -end - function character:update(dt) - --- @todo ну ты понел for _, b in ipairs(self.behaviors) do if b.update then b:update(dt) end end diff --git a/lib/tree.lua b/lib/tree.lua index 3bd5858..5c634cd 100644 --- a/lib/tree.lua +++ b/lib/tree.lua @@ -10,7 +10,7 @@ Tree.panning = require "lib/panning" Tree.controls = require "lib.controls" Tree.level = (require "lib.level.level").new("procedural", "flower_plains") -- для теста у нас только один уровень, который сразу же загружен -Tree.behaviors = {} --- @todo написать нормальную загрузку поведений +Tree.behaviors = {} --- @todo написать нормальную загрузку поведений Tree.behaviors.map = require "lib.character.behaviors.map" -Tree.behaviors.render = require "lib.character.behaviors.render" Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster" +Tree.behaviors.animated = require "lib.character.behaviors.animated" From 95b94bb701c04b5cb3872bcddcd23c1447987208 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Mon, 6 Oct 2025 01:30:17 +0300 Subject: [PATCH 7/8] rename AnimatedBehavior -> SpriteBehavior --- lib/character/behaviors/map.lua | 16 +++++----- .../behaviors/{animated.lua => sprite.lua} | 32 +++++++++---------- lib/character/character.lua | 2 +- lib/tree.lua | 2 +- main.lua | 1 + 5 files changed, 27 insertions(+), 26 deletions(-) rename lib/character/behaviors/{animated.lua => sprite.lua} (76%) diff --git a/lib/character/behaviors/map.lua b/lib/character/behaviors/map.lua index c1573ca..535087e 100644 --- a/lib/character/behaviors/map.lua +++ b/lib/character/behaviors/map.lua @@ -27,8 +27,8 @@ end function mapBehavior:followPath(path) if path:is_empty() then return end self.position = self.displayedPosition - self.owner:try(Tree.behaviors.animated, function(animated) - animated:play("run", true) + self.owner:try(Tree.behaviors.sprite, function(sprite) + sprite:play("run", true) end) self.path = path; ---@type Vec3 @@ -41,12 +41,12 @@ end function mapBehavior:runTo(target) self.t0 = love.timer.getTime() self.runTarget = target - self.owner:try(Tree.behaviors.animated, - function(animated) + self.owner:try(Tree.behaviors.sprite, + function(sprite) if target.x < self.position.x then - animated.side = Tree.behaviors.animated.LEFT + sprite.side = Tree.behaviors.sprite.LEFT elseif target.x > self.position.x then - animated.side = Tree.behaviors.animated.RIGHT + sprite.side = Tree.behaviors.sprite.RIGHT end end ) @@ -63,8 +63,8 @@ function mapBehavior:update(dt) self:runTo(self.path:peek_front()) self.path:pop_front() else -- мы добежали до финальной цели - self.owner:try(Tree.behaviors.animated, function(animated) - animated:play("idle", true) + self.owner:try(Tree.behaviors.sprite, function(sprite) + sprite:play("idle", true) end) self.runTarget = nil end diff --git a/lib/character/behaviors/animated.lua b/lib/character/behaviors/sprite.lua similarity index 76% rename from lib/character/behaviors/animated.lua rename to lib/character/behaviors/sprite.lua index d3784ea..89eecb7 100644 --- a/lib/character/behaviors/animated.lua +++ b/lib/character/behaviors/sprite.lua @@ -1,21 +1,21 @@ local anim8 = require "lib.utils.anim8" ---- @class AnimatedBehavior : Behavior +--- @class SpriteBehavior : Behavior --- @field animationTable table --- @field animationGrid table --- @field state "idle"|"run"|"hurt"|"attack" --- @field side 1|-1 -local animated = {} -animated.__index = animated -animated.id = "animated" -animated.dependencies = { Tree.behaviors.map } -animated.LEFT = -1 -animated.RIGHT = 1 +local sprite = {} +sprite.__index = sprite +sprite.id = "sprite" +sprite.dependencies = { Tree.behaviors.map } +sprite.LEFT = -1 +sprite.RIGHT = 1 --- Скорость между кадрами в анимации -animated.ANIMATION_SPEED = 0.1 +sprite.ANIMATION_SPEED = 0.1 -function animated.new(spriteDir) - local anim = setmetatable({}, animated) +function sprite.new(spriteDir) + local anim = setmetatable({}, sprite) anim.animationTable = {} anim.animationGrid = {} @@ -27,19 +27,19 @@ function animated.new(spriteDir) end anim.state = "idle" - anim.side = animated.RIGHT + anim.side = sprite.RIGHT anim:play("idle") return anim end -function animated:update(dt) +function sprite:update(dt) local anim = self.animationTable[self.state] or self.animationTable["idle"] or nil if not anim then return end anim:update(dt) end -function animated:draw() +function sprite:draw() if not self.animationTable[self.state] or not Tree.assets.files.sprites.character[self.state] then return end self.owner:try(Tree.behaviors.map, @@ -57,9 +57,9 @@ end --- @param state string --- @param loop boolean | nil -function animated:play(state, loop) +function sprite:play(state, loop) if not self.animationGrid[state] then - return print("[AnimatedBehavior]: no animation for '" .. state .. "'") + return print("[SpriteBehavior]: no animation for '" .. state .. "'") end self.animationTable[state] = anim8.newAnimation(self.animationGrid[state], self.ANIMATION_SPEED, function() if not loop then self:play("idle", true) end @@ -67,4 +67,4 @@ function animated:play(state, loop) self.state = state end -return animated +return sprite diff --git a/lib/character/character.lua b/lib/character/character.lua index 9968731..fd14504 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -30,7 +30,7 @@ local function spawn(name, template, spriteDir, position, size, level) char:addBehavior { Tree.behaviors.map.new(position, size), - Tree.behaviors.animated.new(spriteDir), + Tree.behaviors.sprite.new(spriteDir), Tree.behaviors.spellcaster.new() } diff --git a/lib/tree.lua b/lib/tree.lua index 5c634cd..07a06b4 100644 --- a/lib/tree.lua +++ b/lib/tree.lua @@ -13,4 +13,4 @@ Tree.level = (require "lib.level.level").new("procedural", "flow Tree.behaviors = {} --- @todo написать нормальную загрузку поведений Tree.behaviors.map = require "lib.character.behaviors.map" Tree.behaviors.spellcaster = require "lib.character.behaviors.spellcaster" -Tree.behaviors.animated = require "lib.character.behaviors.animated" +Tree.behaviors.sprite = require "lib.character.behaviors.sprite" diff --git a/main.lua b/main.lua index 47e2dad..89f65f5 100644 --- a/main.lua +++ b/main.lua @@ -11,6 +11,7 @@ end function love.load() character.spawn("Hero", "warrior", Tree.assets.files.sprites.character) + character.spawn("Hero", "warrior", Tree.assets.files.sprites.character, Vec3 { 3, 3 }) love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true }) end From 210faaf6a322ed4607981fcc9ae229713f9daacd Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Mon, 6 Oct 2025 01:36:37 +0300 Subject: [PATCH 8/8] implement base behavior --- lib/character/behaviors/behavior.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/character/behaviors/behavior.lua b/lib/character/behaviors/behavior.lua index 1dd5ac0..2282389 100644 --- a/lib/character/behaviors/behavior.lua +++ b/lib/character/behaviors/behavior.lua @@ -3,5 +3,18 @@ --- @field id string --- @field owner Character --- @field dependencies Behavior[] +--- @field new fun(...) : self --- @field update fun(self, dt): nil --- @field draw fun(self): nil +local behavior = {} +behavior.__index = behavior +behavior.id = "behavior" +behavior.dependencies = {} + +function behavior.new() return setmetatable({}, behavior) end + +function behavior:update(dt) end + +function behavior:draw() end + +return behavior