diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b35845c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "game/lib/Concord"] + path = game/lib/Concord + url = https://github.com/Keyslam-Group/Concord +[submodule "game/lib/classic"] + path = game/lib/classic + url = https://github.com/rxi/classic/ diff --git a/game/asset/audio/bgm/Ensemble.mp3 b/game/asset/audio/bgm/Ensemble.mp3 new file mode 100644 index 0000000..8fd8ec0 Binary files /dev/null and b/game/asset/audio/bgm/Ensemble.mp3 differ diff --git a/game/lib/reap/init.lua b/game/lib/reap/init.lua new file mode 100644 index 0000000..74ed535 --- /dev/null +++ b/game/lib/reap/init.lua @@ -0,0 +1,33 @@ +local reap = {} + +--- given lua require file path, return the base in lua path notation +--- usage example : +--- ``local BASE_PATH = reap.base_path(...)`` +---@param path string ex: lib.reap.test +---@return string base_path ex: lib.reap +---@return number replacement_count ex: 1 +function reap.base_path(path) + return path:gsub('%.[^%.]+$', '') +end + +--- given lua require file path, return the base in directory notation +--- usage example : +--- ``local BASE_DIR = reap.base_dir(...)`` +---@param path string ex: lib.reap.test +---@return string base_dir ex: lib/reap +---@return number replacement_count ex: 1 +function reap.base_dir(path) + return reap.base_path(path):gsub("[.]", "/") +end + +--- given lua require file path, return in directory notation +--- usage example : +--- ``local DIR = reap.dir(...)`` +---@param path string ex: lib.reap.test +---@return string lua_dir ex: lib/reap/test +---@return number replacement_count ex: 1 +function reap.dir(path) + return path:gsub("[.]", "/") +end + +return reap diff --git a/game/lib/reoof/cache.lua b/game/lib/reoof/cache.lua new file mode 100644 index 0000000..880bc3d --- /dev/null +++ b/game/lib/reoof/cache.lua @@ -0,0 +1,112 @@ +local format = string.format + +local classic = require("lib.classic.classic") + +---@class Cache : lib.classic.class +---@field private fn function +---@field cache table +---@field count integer +---@field name string +local cache = classic:extend() +cache._VERSION = "0.0.0-alpha" +cache._DESCRIPTION = "A simple and straightforward cache made for LÖVE." +cache._URL = "https://github.com/FNicon/reoof" +cache._LICENSE = [[ + MIT License + + Copyright (c) 2025 FNicon + + 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. + + 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 function default_empty() +end + +--- create entity +---@param fn? function lambda to load resource +---@param name? string for debug purposes +-- ---@return Cache self +function cache:new(fn, name) + -- local self = setmetatable({}, cache) + self.fn = fn or default_empty + self.cache = {} + self.count = 0 + self.name = name or "cache" +end + +--- load resource to cache +---@param self Cache +---@param key string assigned_key +---@param ... any +---@return any resource +function cache:load_to(key, ...) + if (self.cache[key] == nil) then + self.cache[key] = self.fn(...) + self.count = self.count + 1 + return self.cache[key] + else + return self:load_from(key) + end +end + + +--- load resource from cache +---@param self Cache +---@param key string assigned_key +---@return any resource +function cache:load_from(key) + if (self.cache[key] == nil) then + print(format("WARNING : key %s not found"), key) + else + return self.cache[key] + end +end + +--- release object and set it up to nil +---@param self Cache +function cache:release() + for k, _ in pairs(self.cache) do + self:release(k) + end + self.cache = nil + self.count = nil + self.name = nil + self.fn = nil + setmetatable(self, nil) + self = nil +end + +--- release object and set it up to nil +---@param self Cache +---@param key string +function cache:release_from(key) + if (self.cache[key]) then + if (self.cache[key].release) then + self.cache[key]:release() + self.cache[key] = nil + self.count = self.count - 1 + else + print(format("WARNING : during release, %s don't have release function"), key) + end + else + error(format("ERROR : during release, %s not found", key)) + end +end + +return cache diff --git a/game/lib/reoof/pool.lua b/game/lib/reoof/pool.lua new file mode 100644 index 0000000..85dab21 --- /dev/null +++ b/game/lib/reoof/pool.lua @@ -0,0 +1,194 @@ +local insert = table.insert +local remove = table.remove + +local classic = require("lib.classic.classic") + + +---@class Pool : lib.classic.class +---@field private fn function +---@field private rfn function +---@field private efn function +---@field active any[] +---@field hidden any[] +---@field max number | nil +---@field name string +local pool = classic:extend() +pool._VERSION = "0.0.0-alpha" +pool._DESCRIPTION = "A simple and straightforward pool made for LÖVE." +pool._URL = "https://github.com/FNicon/reoof" +pool._LICENSE = [[ + MIT License + + Copyright (c) 2025 FNicon + + 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. + + 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. +]] + +--- default equal function +---@param v1 any +---@param v2 any +local function defaultEqual(v1, v2) + return v1 == v2 +end + +--- Return the first index with the given value (or nil if not found). +---@param array any[] +---@param value any +---@param efn fun(...):boolean +---@return number? +local function indexOf(array, value, efn) + for i, v in pairs(array) do + if efn(v, value) then + return i + end + end + return nil +end + +--- initialize pool +---@param fn fun(...):any generate Function +---@param rfn fun(...):any reset Function +---@param name? string for debug purposes +---@param max? number pool max size +---@param efn? fun(...):boolean equal Function +---@return Pool self +function pool:new(fn, rfn, name, max, efn) + -- local self = setmetatable({}, pool) + self.active = {} + self.hidden = {} + self.fn = fn + self.rfn = rfn + self.efn = efn or defaultEqual + self.max = max > 0 and max or nil + self.name = name or "pool" + return self +end + +--- put entity to pool +---@param self Pool +---@param entity any +---@return Pool +---@return number hidden_idx index from hidden pool +function pool:put(entity) + local idx = indexOf(self.active, entity, self.efn) + if (idx) then + self:put_to(entity, idx) + return self, #self.hidden + else + print("WARNING : trying to insert entity not from generate function") + return self, -1 + end +end + +--- put entity to pool +---@param self Pool +---@param entity any +---@param idx number +---@return Pool +---@return number hidden_idx index from hidden pool +function pool:put_to(entity, idx) + if (self.max) then + if (#self.hidden < self.max) then + insert(self.hidden, entity) + else + self:release(entity) + end + else + insert(self.hidden, entity) + end + remove(self.active, idx) + return self, #self.hidden +end + +--- get entity from pool +---@param self Pool +---@param ... any +---@return any entity from pool +---@return number active_idx +function pool:get(...) + local entity = self.hidden[#self.hidden] + if (entity) then + insert(self.active, entity) + remove(self.hidden, #self.hidden) + self.rfn(entity, ...) + return entity, #self.active + else + entity = self.fn(...) + insert(self.active, entity) + return entity, #self.active + end +end + +--- release function +---@param self Pool +---@param entity? any +function pool:release(entity) + if (entity == nil) then + for _, v in pairs(self.active) do + self:release(v) + end + for _, v in pairs(self.hidden) do + self:release(v) + end + self.active = nil + self.hidden = nil + self.fn = nil + self.rfn = nil + self.efn = nil + self.name = nil + setmetatable(self, nil) + self = nil + else + local idx = indexOf(self.active, entity, self.efn) + if (idx) then + self:release_from("active", idx) + else + idx = indexOf(self.hidden, entity, self.efn) + if (idx) then + self:release_from("hidden", idx) + else + print("WARNING : trying to release object not from generate function") + end + end + end +end + +--- release function +---@param self Pool +---@param from_pool "hidden" | "active" +---@param idx number +function pool:release_from(from_pool, idx) + if (from_pool == "hidden") then + if (self.hidden[idx].release) then + self.hidden[idx]:release() + remove(self.hidden, idx) + else + print("WARNING : during release hidden, %d don't have release function", idx) + end + else + if (self.active[idx].release) then + self.active[idx]:release() + remove(self.active, idx) + else + print("WARNING : during release active, %d don't have release function", idx) + end + end +end + +return pool diff --git a/game/main.lua b/game/main.lua index a1a5d59..77330d2 100644 --- a/game/main.lua +++ b/game/main.lua @@ -6,9 +6,14 @@ local mode = { local actor = require("src.entities.shared.actor") local player = actor.load("player") +local source_cls = require("wrapper.lappy.new.source") +local source = source_cls.obj + local mode_i = 1 function love.load() + source:load_to("asset/audio/bgm/Ensemble.mp3", "asset/audio/bgm/Ensemble.mp3", "stream") + source:load_from("asset/audio/bgm/Ensemble.mp3"):play() mode[mode_i].load(player) end diff --git a/game/wrapper/Concord/world.lua b/game/wrapper/Concord/world.lua new file mode 100644 index 0000000..e70231d --- /dev/null +++ b/game/wrapper/Concord/world.lua @@ -0,0 +1,142 @@ +local format = string.format + +local reap = require("libs.reap") + +local BASE = reap.base_path(...) +local component_builder = require(format("%s.component", BASE)) +local entity_builder = require(format("%s.entity", BASE)) + +local Concord = require("libs.Concord") + +-- Modules +local Entity = Concord.entity +local Component = Concord.component +local System = Concord.system +local World = Concord.world + +local main_wrapper = require "wrapper.lappy.world" +---@class wrappers.Concord.world : lappy.world +local wrapper = main_wrapper:extend() + +function wrapper:new(path, name) + wrapper.super.new(self, path, name) + self.world = World() +end + +local function load_systems(world, system_paths) + for _, p in pairs(system_paths) do + local Systems = {} + Concord.utils.loadNamespace(p, Systems) + for k, s in pairs(Systems) do + for c, l in pairs(s.components) do + if (type(c) == "number") then + component_builder.state(l) + else + component_builder.component(c, l) + end + end + local new_s = s.new() + if (new_s) then + world:addSystem( + new_s + ) + end + end + end +end + +local function load_entities(world, data) + for _, d in pairs(data) do + local entity = entity_builder.new() + entity:assemble(d.assemblage, d.data) + world:addEntity(entity) + end +end + +function wrapper:load(system_paths, entities_data) + load_systems(self.world, system_paths) + load_entities(self.world, entities_data) + self.world:emit("load") +end + +function wrapper:draw() + self.world:emit("draw") +end + +function wrapper:update(dt) + self.world:emit("update", dt) +end + +-- mouse + +function wrapper:mousemoved( x, y, dx, dy, istouch) + self.world:emit("mousemoved", x, y, dx, dy, istouch) +end + +function wrapper:mousepressed(x, y, button, istouch, presses) + self.world:emit("mousepressed", x, y, button, istouch, presses) +end + +function wrapper:mousereleased(x, y, button, istouch, presses) + self.world:emit("mousereleased", x, y, button, istouch, presses) +end + +function wrapper:wheelmoved(x, y) + self.world:emit("wheelmoved", x, y) +end + +-- keyboard + +function wrapper:keypressed(key, scancode, isrepeat) + self.world:emit("keypressed", key, scancode, isrepeat) +end + +function wrapper:keyreleased(key, scancode) + self.world:emit("keyreleased", key, scancode) +end + +-- touchscreen + +function wrapper:touchpressed(id, x, y, dx, dy, pressure) + self.world:emit("touchpressed", id, x, y, dx, dy, pressure) +end + +function wrapper:textinput(t) + self.world:emit("textinput", t) +end + +function wrapper:resize(w,h) + self.world:emit("rezise", w, h) +end + +function wrapper:focus(f) + self.world:emit("focus", f) +end + +function wrapper:quit() + self.world:emit("quit") +end + +-- for gamepad support also add the following: + +function wrapper:joystickadded(joystick) + self.world:emit("joystickadded", joystick) +end + +function wrapper:joystickremoved(joystick) + self.world:emit("joystickremoved", joystick) +end + +function wrapper:gamepadpressed(joystick, button) + self.world:emit("gamepadpressed", joystick, button) +end + +function wrapper:gamepadreleased(joystick, button) + self.world:emit("gamepadreleased", joystick, button) +end + +function wrapper:gamepadaxis(joystick, axis, value) + self.world:emit("gamepadaxis", joystick, axis, value) +end + +return wrapper diff --git a/game/wrapper/lappy/new/image.lua b/game/wrapper/lappy/new/image.lua new file mode 100644 index 0000000..ef94c68 --- /dev/null +++ b/game/wrapper/lappy/new/image.lua @@ -0,0 +1,17 @@ +local cache = require("lib.reoof.cache") +---@class lappy.image : Cache +---@field cache any +local _cache = cache:extend() + +--- new function +function _cache:new() + _cache.super.new(self, love.graphics.newImage, ".image") + self.cache = {} +end + +local obj = _cache() + +return { + class = _cache, + obj = obj +} diff --git a/game/wrapper/lappy/new/image_data.lua b/game/wrapper/lappy/new/image_data.lua new file mode 100644 index 0000000..cb8c8bb --- /dev/null +++ b/game/wrapper/lappy/new/image_data.lua @@ -0,0 +1,17 @@ +local cache = require("lib.reoof.cache") +---@class lappy.image_data : Cache +---@field cache any +local _cache = cache:extend() + +--- new function +function _cache:new() + _cache.super.new(self, love.image.newImageData, ".image_data") + self.cache = {} +end + +local obj = _cache() + +return { + class = _cache, + obj = obj +} diff --git a/game/wrapper/lappy/new/image_font.lua b/game/wrapper/lappy/new/image_font.lua new file mode 100644 index 0000000..fa116f1 --- /dev/null +++ b/game/wrapper/lappy/new/image_font.lua @@ -0,0 +1,17 @@ +local cache = require("lib.reoof.cache") +---@class lappy.image_font : Cache +---@field cache any +local _cache = cache:extend() + +--- new function +function _cache:new() + _cache.super.new(self, love.graphics.newImageFont, ".image_font") + self.cache = {} +end + +local obj = _cache() + +return { + class = _cache, + obj = obj +} diff --git a/game/wrapper/lappy/new/quad.lua b/game/wrapper/lappy/new/quad.lua new file mode 100644 index 0000000..41809c6 --- /dev/null +++ b/game/wrapper/lappy/new/quad.lua @@ -0,0 +1,17 @@ +local cache = require("lib.reoof.cache") +---@class lappy.quad : Cache +---@field cache any +local _cache = cache:extend() + +--- new function +function _cache:new() + _cache.super.new(self, love.image.newQuad, ".quad") + self.cache = {} +end + +local obj = _cache() + +return { + class = _cache, + obj = obj +} diff --git a/game/wrapper/lappy/new/source.lua b/game/wrapper/lappy/new/source.lua new file mode 100644 index 0000000..b2457ee --- /dev/null +++ b/game/wrapper/lappy/new/source.lua @@ -0,0 +1,17 @@ +local cache = require("lib.reoof.cache") +---@class lappy.source : Cache +---@field cache any +local _cache = cache:extend() + +--- new function +function _cache:new() + _cache.super.new(self, love.audio.newSource, ".source") + self.cache = {} +end + +local obj = _cache() + +return { + class = _cache, + obj = obj +} diff --git a/game/wrapper/lappy/new/sprite_batch.lua b/game/wrapper/lappy/new/sprite_batch.lua new file mode 100644 index 0000000..373de42 --- /dev/null +++ b/game/wrapper/lappy/new/sprite_batch.lua @@ -0,0 +1,17 @@ +local cache = require("lib.reoof.cache") +---@class lappy.sprite_batch : Cache +---@field cache any +local _cache = cache:extend() + +--- new function +function _cache:new() + _cache.super.new(self, love.graphics.newSpriteBatch, ".batch") + self.cache = {} +end + +local obj = _cache() + +return { + class = _cache, + obj = obj +} diff --git a/game/wrapper/lappy/world.lua b/game/wrapper/lappy/world.lua new file mode 100644 index 0000000..9958eb1 --- /dev/null +++ b/game/wrapper/lappy/world.lua @@ -0,0 +1,79 @@ +local classic = require "lib.classic" +---@class lappy.world : lib.classic.class +local wrapper = classic:extend() + +function wrapper:new(path, name) + self.path = path + self.name = name +end + +function wrapper:load(_args) +end + +function wrapper:draw() +end + +function wrapper:update(dt) +end + +-- mouse + +function wrapper:mousemoved( x, y, dx, dy, istouch) +end + +function wrapper:mousepressed(x, y, button, istouch, presses) +end + +function wrapper:mousereleased(x, y, button, istouch, presses) +end + +function wrapper:wheelmoved(x, y) +end + +-- keyboard + +function wrapper:keypressed(key, scancode, isrepeat) +end + +function wrapper:keyreleased(key, scancode) +end + +-- touchscreen + +function wrapper:touchpressed(id, x, y, dx, dy, pressure) +end + +function wrapper:textinput(t) +end + +function wrapper:resize(w,h) +end + +function wrapper:focus(f) +end + +function wrapper:quit() +end + +-- for gamepad support also add the following: + +function wrapper:joystickadded(joystick) +end + +function wrapper:joystickremoved(joystick) +end + +function wrapper:gamepadpressed(joystick, button) +end + +function wrapper:gamepadreleased(joystick, button) +end + +function wrapper:gamepadaxis(joystick, axis, value) +end + +function wrapper:__tostring() + return string.format("%s.%s", self.path, self.name) +end + +return wrapper