diff --git a/assets/sprites/test.png b/assets/sprites/test.png new file mode 100644 index 0000000..0685450 Binary files /dev/null and b/assets/sprites/test.png differ diff --git a/character.lua b/character.lua index 3e5b75a..3c0289f 100644 --- a/character.lua +++ b/character.lua @@ -1,4 +1,5 @@ -local anim8 = require "anim8" +local anim8 = require "lib/anim8" +require 'lib/vec3' local CHARACTER_SIZE = 64 @@ -11,6 +12,7 @@ local IDLE_ROW = 1 local RUN_ROW = 2 local ATTACK_ROW = 3 +--- @class Character local Character = {} Character.name = "" @@ -44,18 +46,19 @@ Character.stats.hp = 30 Character.player = {} --- TODO: мнимая надежда на спеллмейкинг ---- +--- --- правда я абсолютно хз, как он будет смотреться --- в контексте рогалика, но посмотрим --- --- мб это будет метаспеллмейкинг на овощах Character.skills = {} ---- а надо ли оно? Character.class = "" +Character.position = Vec3({}) + --- Обёртка над Character:Create -CreateCharacter = Character.create +-- CreateCharacter = Character.create -- ты клоун же -- какого черта у тебя конструктор объекта принимает ссылку на объект @@ -67,7 +70,7 @@ CreateCharacter = Character.create --- @param name string --- @param imagePath string --- @param level? integer -function Character:create(name, imagePath, level) +function CreateCharacter(name, imagePath, level) -- aka Character.create(self, name, imagePath, level) -- TODO: добавить asset_loader, где все необходимые ассеты будут грузиться в одном месте, -- а здесь мы добавляем ассет на загрузку в очередь local image = love.graphics.newImage(imagePath) @@ -78,13 +81,10 @@ function Character:create(name, imagePath, level) idle = anim8.newAnimation(animationGrid(ANIMATION_SIZE, IDLE_ROW), ANIMATION_SPEED), run = anim8.newAnimation(animationGrid(ANIMATION_SIZE, RUN_ROW), ANIMATION_SPEED), attack = anim8.newAnimation(animationGrid(ANIMATION_SIZE, ATTACK_ROW), ANIMATION_SPEED) - }, - animation = self.animationTable.idle + } } end function Character:update(dt) self.animation:update(dt) end - -local f = Character.create diff --git a/faction.lua b/faction.lua index 8c06fcd..aaf619f 100644 --- a/faction.lua +++ b/faction.lua @@ -1,13 +1,43 @@ +--- @class Faction local Faction = {} Faction.name = "" Faction.characters = {} Faction.style = {} --- some sort of global variable :clown: +--- @type table FactionList = {} -function addFaction(name) - FactionList[name] = {} - +--- @param name string +function AddFaction(name) + FactionList[name] = Faction { name = name } end +--- @param name string +--- @param character table +function AddCharacter(name, character) + if FactionList[name] == nil then + print("Cannot add character to faction ", name, "\nFaction does not exist!") + else + FactionList[name]:append(character) + end +end + +--- @param name string +--- @param character Character +function RemoveCharacter(name, character) + if FactionList[name].characters[character.name] == nil then + print("I cant remove character in Faction ", name, ", because this character doesnt exist!") + end + FactionList[name]:remove(character) +end + +--- @param character Character +function Faction:append(character) + self.characters[character.name] = character +end + +--- @param character Character +function Faction:remove(character) + self.characters[character.name] = nil +end diff --git a/lib/asset_bundle.lua b/lib/asset_bundle.lua new file mode 100644 index 0000000..7ff31b6 --- /dev/null +++ b/lib/asset_bundle.lua @@ -0,0 +1,53 @@ +local AssetBundle = { + root = "/assets", + files = {} +} + +function AssetBundle:load(onFileLoading) + local callback = onFileLoading or function(path) + print("[AssetBundle]: loading " .. path) + end + + local function enumerate(path) + local tree = {} + + local contents = love.filesystem.getDirectoryItems(path) + for _, v in pairs(contents) do + local newPath = path .. "/" .. v + local type = love.filesystem.getInfo(newPath).type + if type == "file" then + callback(newPath) + local data = self.loadFile(newPath) + tree[self.cutExtension(v)] = data + end + if type == "directory" then + tree[v] = enumerate(newPath) + end + end + + return tree + end + + self.files = enumerate(self.root) +end + +function AssetBundle.loadFile(path) + local filedata = love.filesystem.newFileData(path) + local ext = filedata:getExtension() + if (ext == "png") then + local img = love.graphics.newImage(path) + img:setFilter("nearest", "nearest") + return + img + elseif (ext == "glsl") then + return love.graphics.newShader(path); + end + + return nil +end + +function AssetBundle.cutExtension(filename) + return string.match(filename, '(.+)%.(.+)') +end + +return AssetBundle diff --git a/lib/camera.lua b/lib/camera.lua new file mode 100644 index 0000000..55a7e96 --- /dev/null +++ b/lib/camera.lua @@ -0,0 +1,215 @@ +--[[ +Copyright (c) 2010-2015 Matthias Richter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +Except as contained in this notice, the name(s) of the above copyright holders +shall not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +]] -- + +local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or '' +local cos, sin = math.cos, math.sin + +local camera = {} +camera.__index = camera + +-- Movement interpolators (for camera locking/windowing) +camera.smooth = {} + +function camera.smooth.none() + return function(dx, dy) return dx, dy end +end + +function camera.smooth.linear(speed) + assert(type(speed) == "number", "Invalid parameter: speed = " .. tostring(speed)) + return function(dx, dy, s) + -- normalize direction + local d = math.sqrt(dx * dx + dy * dy) + local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal + if d > 0 then + dx, dy = dx / d, dy / d + end + + return dx * dts, dy * dts + end +end + +function camera.smooth.damped(stiffness) + assert(type(stiffness) == "number", "Invalid parameter: stiffness = " .. tostring(stiffness)) + return function(dx, dy, s) + local dts = love.timer.getDelta() * (s or stiffness) + return dx * dts, dy * dts + end +end + +local function new(x, y, zoom, rot, smoother) + x, y = x or love.graphics.getWidth() / 2, y or love.graphics.getHeight() / 2 + zoom = zoom or 1 + rot = rot or 0 + smoother = smoother or camera.smooth.none() -- for locking, see below + return setmetatable({ x = x, y = y, scale = zoom, rot = rot, smoother = smoother }, camera) +end + +function camera:lookAt(x, y) + self.x, self.y = x, y + return self +end + +function camera:move(dx, dy) + self.x, self.y = self.x + dx, self.y + dy + return self +end + +function camera:position() + return self.x, self.y +end + +function camera:rotate(phi) + self.rot = self.rot + phi + return self +end + +function camera:rotateTo(phi) + self.rot = phi + return self +end + +function camera:zoom(mul) + self.scale = self.scale * mul + return self +end + +function camera:zoomTo(zoom) + self.scale = zoom + return self +end + +function camera:attach(x, y, w, h, noclip) + x, y = x or 0, y or 0 + w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight() + + self._sx, self._sy, self._sw, self._sh = love.graphics.getScissor() + if not noclip then + love.graphics.setScissor(x, y, w, h) + end + + local cx, cy = x + w / 2, y + h / 2 + love.graphics.push() + love.graphics.translate(cx, cy) + love.graphics.scale(self.scale) + love.graphics.rotate(self.rot) + love.graphics.translate(-self.x, -self.y) +end + +function camera:detach() + love.graphics.pop() + love.graphics.setScissor(self._sx, self._sy, self._sw, self._sh) +end + +function camera:draw(...) + local x, y, w, h, noclip, func + local nargs = select("#", ...) + if nargs == 1 then + func = ... + elseif nargs == 5 then + x, y, w, h, func = ... + elseif nargs == 6 then + x, y, w, h, noclip, func = ... + else + error("Invalid arguments to camera:draw()") + end + + self:attach(x, y, w, h, noclip) + func() + self:detach() +end + +-- world coordinates to camera coordinates +function camera:cameraCoords(x, y, ox, oy, w, h) + ox, oy = ox or 0, oy or 0 + w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight() + + -- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center + local c, s = cos(self.rot), sin(self.rot) + x, y = x - self.x, y - self.y + x, y = c * x - s * y, s * x + c * y + return x * self.scale + w / 2 + ox, y * self.scale + h / 2 + oy +end + +-- camera coordinates to world coordinates +function camera:worldCoords(x, y, ox, oy, w, h) + ox, oy = ox or 0, oy or 0 + w, h = w or love.graphics.getWidth(), h or love.graphics.getHeight() + + -- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y) + local c, s = cos(-self.rot), sin(-self.rot) + x, y = (x - w / 2 - ox) / self.scale, (y - h / 2 - oy) / self.scale + x, y = c * x - s * y, s * x + c * y + return x + self.x, y + self.y +end + +function camera:mousePosition(ox, oy, w, h) + local mx, my = love.mouse.getPosition() + return self:worldCoords(mx, my, ox, oy, w, h) +end + +-- camera scrolling utilities +function camera:lockX(x, smoother, ...) + local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...) + self.x = self.x + dx + return self +end + +function camera:lockY(y, smoother, ...) + local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...) + self.y = self.y + dy + return self +end + +function camera:lockPosition(x, y, smoother, ...) + return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...)) +end + +function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...) + -- figure out displacement in camera coordinates + x, y = self:cameraCoords(x, y) + local dx, dy = 0, 0 + if x < x_min then + dx = x - x_min + elseif x > x_max then + dx = x - x_max + end + if y < y_min then + dy = y - y_min + elseif y > y_max then + dy = y - y_max + end + + -- transform displacement to movement in world coordinates + local c, s = cos(-self.rot), sin(-self.rot) + dx, dy = (c * dx - s * dy) / self.scale, (s * dx + c * dy) / self.scale + + -- move + self:move((smoother or self.smoother)(dx, dy, ...)) +end + +-- the module +return setmetatable({ new = new, smooth = camera.smooth }, + { __call = function(_, ...) return new(...) end }) diff --git a/grid.lua b/lib/grid.lua similarity index 100% rename from grid.lua rename to lib/grid.lua diff --git a/lib/level.lua b/lib/level.lua new file mode 100644 index 0000000..3addb26 --- /dev/null +++ b/lib/level.lua @@ -0,0 +1,5 @@ +local function new() + return { + characters = {} + } +end diff --git a/lib/tree.lua b/lib/tree.lua new file mode 100644 index 0000000..95edc48 --- /dev/null +++ b/lib/tree.lua @@ -0,0 +1,12 @@ +--- Дерево (таблица таблиц) для хранения всего состояния игры +--- Типа при запуске загружаем в него сейв и потом оно работает +--- В love.update обновлять, в love.draw читать + +local tree + +local function instance() + tree = tree or { + level = nil -- current level | nil + } + return tree +end diff --git a/lib/vec3.lua b/lib/vec3.lua new file mode 100644 index 0000000..79a6342 --- /dev/null +++ b/lib/vec3.lua @@ -0,0 +1,82 @@ +--- @class (exact) Vec3 +--- @field x number +--- @field y number +--- @field z number +local __Vec3 = { + x = 0, + y = 0, + z = 0, +} + +---Vec3 constructor +---@param vec number[] +---@return Vec3 +function Vec3(vec) + return setmetatable({ + x = vec[1] or 0, + y = vec[2] or 0, + z = vec[3] or 0, + }, { + __index = __Vec3, + __tostring = __Vec3.__tostring, + __add = __Vec3.add, + __mul = __Vec3.scale, + __unm = function(self) + return __Vec3.scale(self, -1) + end, + __sub = function(self, other) + return self + -other + end, + __eq = function(self, other) + return + self.x == other.x + and self.y == other.y + and self.z == other.z + end, + }) +end + +--- @param other Vec3 +--- @return Vec3 +function __Vec3:add(other) + return Vec3 { self.x + other.x, self.y + other.y, self.z + other.z } +end + +--- @param factor number +function __Vec3:scale(factor) + return Vec3 { self.x * factor, self.y * factor, self.z * factor } +end + +function __Vec3:length() + return math.sqrt(self.x ^ 2 + self.y ^ 2 + self.z ^ 2) +end + +function __Vec3:normalize() + local length = self:length() + if length == 0 then return nil end + return Vec3 { + self.x / length, + self.y / length, + self.z / length + } +end + +function __Vec3:direction() + return math.atan2(self.y, self.x) +end + +--- @param other Vec3 +function __Vec3:dot(other) + return self.x * other.x + self.y * other.y + self.z * other.z +end + +function __Vec3:__tostring() + return "Vec3{" .. self.x .. ", " .. self.y .. ", " .. self.z .. "}" +end + +--- @param other Vec3 +function __Vec3:angle_to(other) + return (other - self):direction() +end + +return Vec3 diff --git a/main.lua b/main.lua index 352716e..1b1cc26 100644 --- a/main.lua +++ b/main.lua @@ -1,26 +1,99 @@ +-- CameraLoader = require 'lib/camera' + require "character" +local Vec3 = require "lib/vec3" + +function love.conf(t) + t.console = true +end + +Camera = { + target = Vec3 {}, + position = Vec3 {}, + lerp_speed = 5.0, + max_offset = 1, -- на сколько метров камера может отдалиться от целевой позиции + max_offset_distance = 5 -- на сколько метров надо отвести мышь для максимального отдаления камеры +} +PIXELS_PER_METER = 100 + +function Camera:update(dt) + if not self.target then return end + + local mouse_offset = Vec3 { 0, 0, 0 } + local offset_distance = 0 + local adjusted_target = self.target * (self.max_offset * offset_distance / self.max_offset_distance) + + local to_target = adjusted_target - self.position + self.position = self.position + to_target * (dt * self.lerp_speed) +end + +-- Преобразует вектор мировых координат (в метрах) в вектоор экранных координат (в пикселях) +local function worldToScreen(worldPos) + return (worldPos - Camera.position) * PIXELS_PER_METER +end + function love.load() + Camera.target = Vec3({}) -- PlayerFaction = Faction -- Hero1 = Character:create("Petya", 10) -- Hero2 = Character:create("Andrysha", 12) -- PlayerFaction.characters = { Hero1, Hero2 } + love.window.setMode(1080, 720, { resizable = true, msaa = 4, vsync = true }) end -function love.update(dt) +---@todo Вынести это в свое поле в дереве глобального состояния уровня +-- local cameraPos = Vec3({}) +-- Camera:lockPosition(cameraPos.x, cameraPos.y, Camera.smooth.damped(10)) +function love.update(dt) + if love.keyboard.isDown("w") then Camera.target = Camera.target + Vec3({ 0, -dt }) end + if love.keyboard.isDown("a") then Camera.target = Camera.target + Vec3({ -dt }) end + if love.keyboard.isDown("s") then Camera.target = Camera.target + Vec3({ 0, dt }) end + if love.keyboard.isDown("d") then Camera.target = Camera.target + Vec3({ dt }) end + + Camera:update(dt) end function love.draw() + -- Camera:attach() + local uiSize = 0.2 + + local windowWidth = love.graphics.getWidth() + local windowHeight = love.graphics.getHeight() * (1 - uiSize) + local width = 20 + local height = 12 + + love.graphics.setColor(139 / 255, 195 / 255, 74 / 255, 1) + + love.graphics.rectangle('fill', worldToScreen(Vec3 {}).x, worldToScreen(Vec3 {}).y, width * PIXELS_PER_METER, + height * PIXELS_PER_METER) + love.graphics.setColor(244 / 255, 67 / 255, 54 / 255, 1) + love.graphics.rectangle('fill', worldToScreen(Vec3 {}).x, worldToScreen(Vec3 {}).y, 1 * PIXELS_PER_METER, + 1 * PIXELS_PER_METER) + + -- if windowWidth / windowHeight > width / height then + -- local fieldWidth = windowHeight * width / height + -- love.graphics.rectangle('fill', 0.5 * (windowWidth - fieldWidth), 0, + -- fieldWidth, windowHeight) + -- else + -- local fieldHeight = windowWidth * height / width + -- love.graphics.rectangle('fill', 0, 0.5 * (windowHeight - fieldHeight), + -- windowWidth, fieldHeight) + -- end + + -- Camera:detach() end local Level = {} Level.kakaya_ta_hren = 10 Level.map = {} +local v = Vec3({ 0, 1, 2 }) -- Faction -> Character -- calculate_order() +-- calculate_order()