diff --git a/lib/character/animation.lua b/lib/character/animation.lua new file mode 100644 index 0000000..5871e48 --- /dev/null +++ b/lib/character/animation.lua @@ -0,0 +1,46 @@ +local anim8 = require "lib/anim8" + +--- Скорость между кадрами в анимации +local ANIMATION_SPEED = 0.1 + +--- @class Animation +--- @field animationTable 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 } diff --git a/lib/character/character.lua b/lib/character/character.lua index 18864b3..d29cd1f 100644 --- a/lib/character/character.lua +++ b/lib/character/character.lua @@ -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 ---- @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 } diff --git a/lib/character/graphics.lua b/lib/character/graphics.lua new file mode 100644 index 0000000..c116699 --- /dev/null +++ b/lib/character/graphics.lua @@ -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 } diff --git a/lib/character/logic.lua b/lib/character/logic.lua new file mode 100644 index 0000000..c5bdc16 --- /dev/null +++ b/lib/character/logic.lua @@ -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 } diff --git a/lib/character/map_logic.lua b/lib/character/map_logic.lua new file mode 100644 index 0000000..8717bff --- /dev/null +++ b/lib/character/map_logic.lua @@ -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 } diff --git a/lib/grid.lua b/lib/grid.lua deleted file mode 100644 index 6a61d25..0000000 --- a/lib/grid.lua +++ /dev/null @@ -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 } diff --git a/lib/grid/character.lua b/lib/grid/character.lua new file mode 100644 index 0000000..1425065 --- /dev/null +++ b/lib/grid/character.lua @@ -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 } diff --git a/lib/grid/grid.lua b/lib/grid/grid.lua new file mode 100644 index 0000000..ccdf5fd --- /dev/null +++ b/lib/grid/grid.lua @@ -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 diff --git a/lib/grid/tile.lua b/lib/grid/tile.lua new file mode 100644 index 0000000..bfd5eb3 --- /dev/null +++ b/lib/grid/tile.lua @@ -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 } diff --git a/lib/level.lua b/lib/level.lua index 255fe28..b5811e4 100644 --- a/lib/level.lua +++ b/lib/level.lua @@ -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)