From 1b12b2c470a270e5a7a4b5ccc4b0ee5bea9a4cb5 Mon Sep 17 00:00:00 2001 From: PeaAshMeter Date: Tue, 16 Sep 2025 23:53:16 +0300 Subject: [PATCH] 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