refactor character & grid again

Co-authored-by: Ivan Yuriev <ivanyr44@gmail.com>
This commit is contained in:
Neckrat 2025-08-09 23:22:34 +03:00
parent 1a2a7ab60f
commit af792bd2d5
10 changed files with 265 additions and 122 deletions

View File

@ -0,0 +1,46 @@
local anim8 = require "lib/anim8"
--- Скорость между кадрами в анимации
local ANIMATION_SPEED = 0.1
--- @class Animation
--- @field animationTable table<string, table>
local animation = {}
local function new(id, spriteDir)
local anim = {
animationTable = {}
}
local 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)
animationGrid[n] = aGrid(tiles, 1)
end
anim.state = "idle"
anim.animationTable.idle = anim8.newAnimation(animationGrid["idle"], ANIMATION_SPEED)
anim.animationTable.run = anim8.newAnimation(animationGrid["run"], ANIMATION_SPEED)
anim.animationTable.attack = anim8.newAnimation(animationGrid["attack"], ANIMATION_SPEED, function()
anim.state = "idle"
end)
anim.animationTable.hurt = anim8.newAnimation(animationGrid["hurt"], ANIMATION_SPEED, function()
anim.state = "idle"
end)
return setmetatable(anim, animation)
end
function animation:getState()
return self.state
end
--- @param state State
function animation:setState(state)
self.state = state
end
return { new = new }

View File

@ -1,22 +1,17 @@
local anim8 = require "lib/anim8"
require 'lib/vec3'
--- Скорость между кадрами в анимации
local ANIMATION_SPEED = 0.1
--- @alias Id integer
--- @type Id
local characterId = 1
--- @todo Композиция лучше наследования, но не до такой же степени! Надо отрефакторить и избавиться от сотни полей в таблице
--- @class Character
--- @field id integer
--- @field animationTable table<string, table>
--- @field state "idle"|"run"|"attack"|"hurt"
--- @field id Id
--- @field info Info
--- @field player table
--- @field position Vec3
--- @field latestPosition Vec3 позиция, где character был один тик назад
--- @field runTarget Vec3 точка, в которую в данный момент бежит персонаж
--- @field size Vec3
--- @field graphics Graphics
--- @field logic Logic
local character = {}
character.__index = character
@ -24,88 +19,37 @@ character.__index = character
--- @param name string
--- @param template ClassTemplate
--- @param spriteDir table
--- @param position? Vec3
--- @param size? Vec3
--- @param level? integer
local function spawn(name, template, spriteDir, level)
local 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)
animationGrid[n] = aGrid(tiles, 1)
end
local char = {
animationTable = {}
}
local function spawn(name, template, spriteDir, position, size, level)
local char = {}
char.id = characterId
characterId = characterId + 1
char.position = Vec3({})
char.size = Vec3({ 1, 1 })
char.state = "idle"
char.animationTable.idle = anim8.newAnimation(animationGrid["idle"], ANIMATION_SPEED)
char.animationTable.run = anim8.newAnimation(animationGrid["run"], ANIMATION_SPEED)
char.animationTable.attack = anim8.newAnimation(animationGrid["attack"], ANIMATION_SPEED, function()
char.state = "idle"
end)
char.animationTable.hurt = anim8.newAnimation(animationGrid["hurt"], ANIMATION_SPEED, function()
char.state = "idle"
end)
char.info = (require "lib/character/info").new(name, template)
char.logic = (require 'lib.character.logic').new(char.id, position, size)
char.graphics = (require 'lib.character.graphics').new(char.id, spriteDir)
char.info = (require "lib/character/info").new(name, template, level)
char = setmetatable(char, character)
Tree.level.characters[char.id] = char
Tree.level.positionGrid:add(char)
Tree.level.characterGrid:add(char)
return char
end
--- @param target Vec3
function character:runTo(target)
self.state = "run"
self.runTarget = target
self.logic:runTo(target)
end
function character:update(dt)
if self.state == "run" and self.runTarget then
if self.position:floor() == self.runTarget:floor() then -- мы добежали до цели и сейчас в целевой клетке
self.state = "idle"
self.runTarget = nil
else -- мы не добежали до цели
local vel = (self.runTarget:subtract(self.position):normalize() --[[@as Vec3]]
):scale(2 * dt) -- бежим 2 условных метра в секунду
self.position = self.position:add(vel)
end
end
if self.position ~= self.latestPosition then
-- типа уведомление о том, что положение (на уровне клеток) изменилось
Tree.level.positionGrid:remove(self)
Tree.level.positionGrid:add(self)
end
if love.keyboard.isDown("r") then
self.state = "run"
end
if love.keyboard.isDown("i") then
self.state = "idle"
end
if love.keyboard.isDown("u") then
self.state = "attack"
end
if love.keyboard.isDown("h") then
self.state = "hurt"
end
self.animationTable[self.state]:update(dt)
self.logic:update(dt)
self.graphics:update(dt)
end
function character:draw()
local ppm = Tree.level.camera.pixelsPerMeter
self.animationTable[self.state]:draw(Tree.assets.files.sprites.character[self.state], self.position.x,
self.position.y, nil, 1 / ppm, 1 / ppm, 38, 47)
self.graphics:draw()
end
return { spawn = spawn }

View File

@ -0,0 +1,31 @@
--- @class Graphics
--- @field id Id
--- @field animation Animation
local graphics = {}
graphics.__index = graphics
--- @param id Id
--- @param spriteDir table
local function new(id, spriteDir)
return setmetatable({
id = id,
animation = (require 'lib.character.animation').new(id, spriteDir)
}, graphics)
end
function graphics:update(dt)
local state = Tree.level.characters[self.id].logic.state
self.animation.animationTable[state]:update(dt)
end
function graphics:draw()
local ppm = Tree.level.camera.pixelsPerMeter
local position = Tree.level.characters[self.id].logic.mapLogic.position
local state = Tree.level.characters[self.id].logic.state
self.animation.animationTable[state]:draw(Tree.assets.files.sprites.character[state],
position.x,
position.y, nil, 1 / ppm, 1 / ppm, 38, 47)
end
return { new = new }

45
lib/character/logic.lua Normal file
View File

@ -0,0 +1,45 @@
--- @alias State "idle"|"run"|"attack"|"hurt"
--- @class Logic
--- @field id Id
--- @field mapLogic MapLogic
--- @field state State
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)
}, logic)
end
--- @param target Vec3
function logic:runTo(target)
self.state = "run"
self.mapLogic.runTarget = target
end
function logic:update(dt)
if self.state == "run" and self.mapLogic.runTarget then
if self.mapLogic.position:floor() == self.mapLogic.runTarget:floor() then -- мы добежали до цели и сейчас в целевой клетке
self.state = "idle"
self.mapLogic.runTarget = nil
else -- мы не добежали до цели
local vel = (self.mapLogic.runTarget:subtract(self.mapLogic.position):normalize() --[[@as Vec3]]
):scale(2 * dt) -- бежим 2 условных метра в секунду
self.mapLogic.position = self.mapLogic.position:add(vel)
end
end
if self.mapLogic.position ~= self.mapLogic.latestPosition then
-- типа уведомление о том, что положение (на уровне клеток) изменилось
Tree.level.characterGrid:remove(Tree.level.characters[self.id])
Tree.level.characterGrid:add(Tree.level.characters[self.id])
end
end
return { new = new }

View File

@ -0,0 +1,20 @@
--- @class MapLogic
--- @field id Id
--- @field position Vec3
--- @field latestPosition Vec3 позиция, где character был один тик назад
--- @field runTarget Vec3 точка, в которую в данный момент бежит персонаж
--- @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({}),
size = size or Vec3({ 1, 1 })
}, mapLogic)
end
return { new = new }

View File

@ -1,47 +0,0 @@
local utils = require "lib/utils"
--- @class Grid
local grid = {}
grid.__index = grid
--- Adds a character id to the grid
--- @param character Character
function grid:add(character)
local centerX, centerY = math.floor(character.position.x), math.floor(character.position.y)
local sizeX, sizeY = character.size.x, character.size.y
for y = centerY, centerY + sizeY - 1 do
for x = centerX, centerX + sizeX - 1 do
self[x][y] = character.id
end
end
end
--- Removes a character id from the grid
--- @param character Character
function grid:remove(character)
local centerX, centerY = math.floor(character.position.x), math.floor(character.position.y)
local sizeX, sizeY = character.size.x, character.size.y
for y = centerY, centerY + sizeY - 1 do
for x = centerX, centerX + sizeX - 1 do
self[x][y] = nil
end
end
end
--- Generates an empty grid
--- @param width number
--- @param height number
--- @return Grid
local function generateGrid(width, height)
local g = utils.generateList(width, function(_)
return utils.generateList(height, function(_)
return {}
end)
end)
return setmetatable(g, grid)
end
return { new = generateGrid }

27
lib/grid/character.lua Normal file
View File

@ -0,0 +1,27 @@
local Grid = require "lib.grid.grid"
--- @class CharacterGrid: Grid
local CharacterGrid = setmetatable({}, { __index = Grid })
CharacterGrid.__index = CharacterGrid
function CharacterGrid.new(width, height)
return setmetatable(Grid.new(width, height, nil), CharacterGrid)
end
--- Adds a character id to the grid
--- @param character Character
function CharacterGrid:add(character)
local cx, cy = math.floor(character.logic.mapLogic.position.x), math.floor(character.logic.mapLogic.position.y)
local sx, sy = character.logic.mapLogic.size.x, character.logic.mapLogic.size.y
self:fillRect(cx, cy, sx, sy, character.id)
end
--- Removes a character id from the grid
--- @param character Character
function CharacterGrid:remove(character)
local cx, cy = math.floor(character.logic.mapLogic.position.x), math.floor(character.logic.mapLogic.position.y)
local sx, sy = character.logic.mapLogic.size.x, character.logic.mapLogic.size.y
self:fillRect(cx, cy, sx, sy, nil)
end
return { new = CharacterGrid.new }

58
lib/grid/grid.lua Normal file
View File

@ -0,0 +1,58 @@
local utils = require "lib/utils"
--- @class Grid
local Grid = {}
Grid.__index = Grid
--- Создать пустую сетку width x height, заполненную initial (по умолчанию nil)
function Grid.new(width, height, initial)
local g = utils.generateList(width, function()
return utils.generateList(height, function()
return initial
end)
end)
return setmetatable(g, Grid)
end
--- @param x integer
--- @param y integer
function Grid:get(x, y)
local col = self[x]
return col and col[y] or nil
end
--- @param x integer
--- @param y integer
function Grid:set(x, y, value)
self[x][y] = value
end
--- @param x integer
--- @param y integer
function Grid:clear(x, y)
self[x][y] = nil
end
-- нормализуем прямоугольник (поддержка отрицательных размеров)
local function normalizeRect(x, y, w, h)
if w < 0 then
x = x + w + 1; w = -w
end
if h < 0 then
y = y + h + 1; h = -h
end
return x, y, w, h
end
--- Заполнить прямоугольник значением value
function Grid:fillRect(x, y, w, h, value)
x, y, w, h = normalizeRect(x, y, w, h)
local x2, y2 = x + w - 1, y + h - 1
for yy = y, y2 do
for xx = x, x2 do
self[xx][yy] = value
end
end
end
return Grid

18
lib/grid/tile.lua Normal file
View File

@ -0,0 +1,18 @@
local Grid = require "lib.grid.grid"
--- @class TileGrid: Grid
local TileGrid = setmetatable({}, { __index = Grid })
TileGrid.__index = TileGrid
function TileGrid.new(width, height)
return setmetatable(Grid.new(width, height, nil), TileGrid)
end
--- @param x integer
--- @param y integer
--- @param tile Tile | nil
function TileGrid:set(x, y, tile)
Grid.set(self, x, y, tile)
end
return { new = TileGrid.new }

View File

@ -2,7 +2,7 @@ local utils = require "lib/utils"
--- @class Level
--- @field characters Character[]
--- @field positionGrid Grid
--- @field characterGrid CharacterGrid
--- @field selector Selector
--- @field camera Camera
local level = {}
@ -11,7 +11,8 @@ level.__index = level
local function new()
return setmetatable({
characters = {},
positionGrid = (require "lib/grid").new(30, 30), -- magic numbers for testing purposes only
characterGrid = (require "lib/grid/character").new(30, 30), -- magic numbers for testing purposes only
tileGrid = (require "lib/grid/tile").new(30, 30), -- magic numbers for testing purposes only
selector = (require "lib/selector").new(),
camera = (require "lib/camera").new()
}, level)