aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorne_mene <[email protected]>2026-03-08 22:21:42 +0100
committerne_mene <[email protected]>2026-03-08 22:21:42 +0100
commitb744faa2e42ed37459fe3edb69c1149146233e5b (patch)
tree25f238d3e467c7cf43e619b579e0df89762b1cca
parent95d50b15634bf4799a2005e381d82c110fbff39b (diff)
let there be light
-rw-r--r--.luarc.json8
-rw-r--r--TODO.txt10
-rw-r--r--main.lua49
-rw-r--r--src/ecs.lua169
-rw-r--r--src/events.lua19
-rw-r--r--src/init.lua47
-rw-r--r--src/input.lua75
-rw-r--r--src/objs/player.lua32
-rw-r--r--src/textures.lua26
-rw-r--r--src/utils.lua39
10 files changed, 474 insertions, 0 deletions
diff --git a/.luarc.json b/.luarc.json
new file mode 100644
index 0000000..7a1b616
--- /dev/null
+++ b/.luarc.json
@@ -0,0 +1,8 @@
+{
+ "diagnostics": {
+ "globals": ["love"],
+ "disable": [
+ "lowercase-global"
+ ]
+ }
+}
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..a6dd4cb
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,10 @@
+HIGH PRIORITY:
+- Event system
+- Player movement
+
+LOW PRIORITY:
+- Fix mouse position
+
+DONE:
+- ECS
+- Input system
diff --git a/main.lua b/main.lua
new file mode 100644
index 0000000..36294c2
--- /dev/null
+++ b/main.lua
@@ -0,0 +1,49 @@
+require "src.init"
+
+local lg = love.graphics
+
+function love.load()
+ main_init()
+ local scn = new_scene()
+ event_bind(scn.on_update, "Body", body_sys)
+ event_bind(scn.on_update, "Player", player_movement_sys)
+
+ event_bind(scn.on_draw, "Body", draw_sys)
+ set_scene(scn)
+
+ new_player(100, 100)
+end
+
+function love.update(dt)
+ local scn = get_current_scene()
+ assert(scn, "No scene set.")
+
+ fire_event(scn.on_update, dt)
+ flush_scene()
+end
+
+function love.draw(dt)
+ lg.origin()
+ lg.setCanvas(Viewport)
+ lg.clear()
+
+ local scn = get_current_scene()
+ assert(scn, "No scene set.")
+
+ fire_event(scn.on_draw, dt)
+
+ -- TODO: Take care of weird displays
+ lg.setCanvas()
+ local scr_width, scr_height = lg.getDimensions()
+ WindowScale = min(scr_width/SCR_WIDTH, scr_height/SCR_HEIGHT)
+
+ lg.draw(
+ Viewport,
+ scr_width/2, scr_height/2, 0,
+ WindowScale, WindowScale,
+ SCR_WIDTH/2, SCR_HEIGHT/2)
+
+ lg.print(tostring(love.timer.getFPS()))
+ input_step()
+end
+
diff --git a/src/ecs.lua b/src/ecs.lua
new file mode 100644
index 0000000..1df5421
--- /dev/null
+++ b/src/ecs.lua
@@ -0,0 +1,169 @@
+
+local current_scene = nil
+local compfuncs = {}
+
+function TAGCOMP(_) end
+
+function new_scene()
+ local newscene = {
+ compmask = {},
+ entities = {},
+ entities_size = 0,
+
+ free_entids = {},
+ free_entids_size = 0,
+
+ killset = {},
+ killq = {},
+ killq_size = 0,
+
+ -- Events
+ on_draw = new_event(),
+ on_update = new_event(),
+
+ comp_removeq = {},
+ comp_entq = {},
+ comp_removeq_size = 0,
+ }
+ for comp, _ in pairs(compfuncs) do
+ newscene.compmask[comp] = {
+ sparse = {},
+ dense = {},
+ size = 0,
+ }
+ end
+ return newscene
+end
+
+function set_scene(newscene)
+ current_scene = newscene
+end
+
+function get_current_scene()
+ return current_scene
+end
+
+function register_comp(name, func)
+ assert(compfuncs[name] == nil, "Component '"..name.."' is already registered.")
+
+ compfuncs[name] = func
+end
+
+function add_comp(ent, comp, ...)
+ assert(current_scene, "No scene up.")
+ assert(compfuncs[comp], "Unknown component '"..tostring(comp).."'")
+ assert(current_scene.entities[ent.id], "Entity "..tostring(ent.id).." doesn't exist.")
+
+ local mask = current_scene.compmask[comp]
+ assert(not mask.sparse[ent.id], "Entity "..tostring(ent.id).." already has component of type '"..comp.."'.")
+
+ mask.size = mask.size + 1
+ mask.sparse[ent.id] = mask.size
+ mask.dense[mask.size] = ent.id
+
+ compfuncs[comp](current_scene.entities[ent.id], ...)
+end
+
+local function remove_comp(ent, comp)
+ assert(current_scene, "No scene up.")
+ assert(compfuncs[comp], "Unknown component '"..tostring(comp).."'")
+ assert(current_scene.entities[ent.id], "Entity "..tostring(ent.id).." doesn't exist.")
+
+ local mask = current_scene.compmask[comp]
+ assert(mask.sparse[ent.id], "Entity "..tostring(ent.id).." does not have component of type '"..comp.."'.")
+
+ local index = mask.sparse[ent.id]
+ local lastid = mask.dense[mask.size]
+
+ mask.sparse[ent.id] = nil
+ mask.sparse[lastid] = index
+
+ mask.dense[index] = mask.dense[mask.size]
+ mask.dense[mask.size] = nil
+ mask.size = mask.size - 1
+end
+
+function run_system(comp, func, ...)
+ assert(current_scene, "No scene up.")
+ assert(compfuncs[comp], "Unknown component '"..tostring(comp).."'")
+
+ for _, entid in ipairs(current_scene.compmask[comp].dense) do
+ func(current_scene.entities[entid], ...)
+ end
+end
+
+function queue_entity_kill(ent)
+ assert(current_scene, "No scene up.")
+ assert(current_scene.entities[ent.id], "Entity "..tostring(ent.id).." doesn't exist.")
+
+ if current_scene.killset[ent.id] then
+ return
+ end
+ current_scene.killset[ent.id] = true
+ current_scene.killq_size = current_scene.killq_size + 1
+ current_scene.killq[current_scene.killq_size] = ent.id
+end
+
+function has_comp(ent, comp)
+ assert(current_scene, "No scene up.")
+ assert(compfuncs[comp], "Unknown component '"..tostring(comp).."'")
+ assert(current_scene.entities[ent.id], "Entity "..tostring(ent.id).." doesn't exist.")
+
+ return current_scene.compmask[comp].sparse[ent.id] ~= nil
+end
+
+function queue_remove_comp(ent, comp)
+ assert(current_scene, "No scene up.")
+ assert(compfuncs[comp], "Unknown component '"..tostring(comp).."'")
+
+ if not has_comp(ent, comp) then return end
+
+ current_scene.comp_removeq_size = current_scene.comp_removeq_size + 1
+ current_scene.comp_removeq[current_scene.comp_removeq_size] = comp
+ current_scene.comp_entq[current_scene.comp_removeq_size] = ent
+end
+
+function flush_scene()
+ assert(current_scene, "No scene up.")
+
+ for i=1, current_scene.comp_removeq_size do
+ remove_comp(current_scene.comp_entq[i], current_scene.comp_removeq[i])
+ end
+ current_scene.comp_removeq_size = 0
+
+ for i=current_scene.killq_size, 1, -1 do
+ local ent = current_scene.entities[current_scene.killq[i]]
+ for comp, _ in pairs(current_scene.compmask) do
+ if has_comp(ent, comp) then
+ remove_comp(ent, comp)
+ end
+ end
+
+ current_scene.killset[ent.id] = nil
+
+ current_scene.free_entids_size = current_scene.free_entids_size + 1
+ current_scene.free_entids[current_scene.free_entids_size] = ent.id
+ end
+ current_scene.killq_size = 0
+end
+
+function new_entity()
+ assert(current_scene, "No scene up.")
+
+ if current_scene.free_entids_size > 0 then
+ local entid = current_scene.free_entids[current_scene.free_entids_size]
+ current_scene.free_entids_size = current_scene.free_entids_size - 1
+
+ local ent = current_scene.entities[entid]
+ for key in pairs(ent) do
+ ent[key] = nil
+ end
+ ent.id = entid
+ return ent
+ end
+ local newid = current_scene.entities_size + 1
+ current_scene.entities[newid] = {id = newid}
+ current_scene.entities_size = newid
+
+ return current_scene.entities[newid]
+end
diff --git a/src/events.lua b/src/events.lua
new file mode 100644
index 0000000..2171ade
--- /dev/null
+++ b/src/events.lua
@@ -0,0 +1,19 @@
+function new_event()
+ return {
+ systems = {},
+ compnames = {},
+ size = 0,
+ }
+end
+
+function event_bind(event, compname, system)
+ event.size = event.size + 1
+ event.systems[event.size] = system
+ event.compnames[event.size] = compname
+end
+
+function fire_event(event, ...)
+ for i=1, event.size do
+ run_system(event.compnames[i], event.systems[i], ...)
+ end
+end
diff --git a/src/init.lua b/src/init.lua
new file mode 100644
index 0000000..c18f175
--- /dev/null
+++ b/src/init.lua
@@ -0,0 +1,47 @@
+require "src.events"
+require "src.ecs"
+require "src.utils"
+require "src.input"
+require "src.textures"
+
+SCR_WIDTH = 320
+SCR_HEIGHT = 180
+WindowScale = 1
+
+register_input("Left", {{"key", "left"}, {"key", "a"}})
+register_input("Right", {{"key", "right"}, {"key", "d"}})
+register_input("Down", {{"key", "down"}, {"key", "s"}})
+register_input("Up", {{"key", "up"}, {"key", "w"}})
+
+register_input("Right_Click", {{"mouse", 1}})
+register_input("Left_Click", {{"mouse", 2}})
+
+local lg = love.graphics
+local lf = love.filesystem
+
+local function load_dir(path)
+ local files = lf.getDirectoryItems(path)
+
+ for _, file in ipairs(files) do
+ local filepath = path.."/"..file
+
+ if lf.getInfo(filepath).type == "directory" then
+ load_dir(filepath)
+ else
+ lf.load(filepath)();
+ end
+ end
+end
+
+function main_init()
+ lg.setDefaultFilter("nearest", "nearest")
+ Viewport = lg.newCanvas(SCR_WIDTH, SCR_HEIGHT)
+
+ load_textures_from()
+
+ love.window.setMode(SCR_WIDTH, SCR_HEIGHT, {fullscreen = false})
+
+ load_dir("src/objs")
+ load_dir("src/scenes")
+end
+
diff --git a/src/input.lua b/src/input.lua
new file mode 100644
index 0000000..8029be2
--- /dev/null
+++ b/src/input.lua
@@ -0,0 +1,75 @@
+local lk = love.keyboard
+local lm = love.mouse
+
+local inputs = {}
+local keyEvents = {}
+local mouseEvents = {}
+
+---@param triggers table pair like this {type, keycode/name}
+function register_input(name, triggers)
+ inputs[name] = triggers
+end
+
+function input_direction(left, right, up, down)
+ return bton(is_input_pressed(right)) - bton(is_input_pressed(left)),
+ bton(is_input_pressed(down)) - bton(is_input_pressed(up))
+end
+
+function is_input_just_pressed(name)
+ local inp = inputs[name]
+ local type
+ local code
+
+ for _, trig in ipairs(inp) do
+ type = trig[1]
+ code = trig[2]
+
+ if type == "mouse" then
+ if mouseEvents[code] ~= nil then return true end
+ end
+ if type == "key" then
+ if keyEvents[code] ~= nil then return true end
+ end
+ end
+end
+
+function is_input_pressed(name)
+ local inp = inputs[name]
+ local type
+ local code
+
+ for _, trig in ipairs(inp) do
+ type = trig[1]
+ code = trig[2]
+
+ if type == "mouse" then
+ if lm.isDown(code) then return true end
+ end
+ if type == "key" then
+ if lk.isDown(code) then return true end
+ end
+ end
+end
+
+function love.keypressed(key)
+ keyEvents[key] = true
+end
+
+function love.mousepressed(x, y, btn)
+ mouseEvents[btn] = true
+end
+
+function get_mouse_pos()
+ -- TODO: Fix mouse position relative to games canvas
+ local scrw, scrh = love.graphics.getDimensions()
+ return math.floor(lm.getX() / scrw * SCR_WIDTH), math.floor(lm.getY() / scrh * SCR_HEIGHT)
+end
+
+function input_step()
+ for key in pairs(keyEvents) do
+ keyEvents[key] = nil
+ end
+ for key in pairs(mouseEvents) do
+ mouseEvents[key] = nil
+ end
+end
diff --git a/src/objs/player.lua b/src/objs/player.lua
new file mode 100644
index 0000000..229dac9
--- /dev/null
+++ b/src/objs/player.lua
@@ -0,0 +1,32 @@
+PLAYER_SPEED = 100
+
+register_comp("Body", function (ent, x, y)
+ ent.x = x
+ ent.y = y
+ ent.vx = 0
+ ent.vy = 0
+end)
+
+register_comp("Player", TAGCOMP)
+
+function body_sys(ent, dt)
+ ent.x = ent.x + ent.vx * dt
+ ent.y = ent.y + ent.vy * dt
+end
+
+function draw_sys(ent)
+ love.graphics.circle("fill", ent.x, ent.y, 8)
+end
+
+function player_movement_sys(player, dt)
+ local inpx, inpy = input_direction("Left", "Right", "Up", "Down")
+ inpx, inpy = normalize(inpx, inpy)
+ player.vx = dlerp(player.vx, inpx * PLAYER_SPEED, 25 * dt)
+ player.vy = dlerp(player.vy, inpy * PLAYER_SPEED, 25 * dt)
+end
+
+function new_player(x, y)
+ local ent = new_entity()
+ add_comp(ent, "Body", x, y)
+ add_comp(ent, "Player")
+end
diff --git a/src/textures.lua b/src/textures.lua
new file mode 100644
index 0000000..5a3a4f9
--- /dev/null
+++ b/src/textures.lua
@@ -0,0 +1,26 @@
+require "string"
+local img_bank = {}
+
+local lf = love.filesystem
+function load_textures_from(path)
+ path = path or "assets/images"
+ local files = lf.getDirectoryItems(path)
+
+ for _, file in ipairs(files) do
+ local filepath = path.."/"..file
+
+ if lf.getInfo(filepath).type == "directory" then
+ load_textures_from(filepath)
+ else
+ if string.find(filepath, ".png") then
+ local name = string.gsub(filepath, ".png", "")
+ name = string.gsub(name, "assets/images/", "")
+ img_bank[name] = love.graphics.newImage(filepath)
+ end
+ end
+ end
+end
+
+function get_tex(name)
+ return img_bank[name]
+end
diff --git a/src/utils.lua b/src/utils.lua
new file mode 100644
index 0000000..c3e4bea
--- /dev/null
+++ b/src/utils.lua
@@ -0,0 +1,39 @@
+
+function lerp(a, b, c)
+ return a + (b - a) * c
+end
+
+function dlerp(a, b, c)
+ return lerp(b, a, 0.5^c)
+end
+
+function bton(bool)
+ return bool and 1 or 0
+end
+
+function min(a, b)
+ if a < b then
+ return a
+ end
+ return b
+end
+
+function normalize(x, y)
+ if x == 0 and y == 0 then
+ return 0, 0
+ end
+
+ local leng = math.sqrt(x*x + y*y)
+ return x / leng, y / leng
+end
+
+-- Top left corner
+function point_in_rect(px, py, rx, ry, rw, rh)
+ return px > rx and px < rx + rw and py > ry and py < ry + rh
+end
+
+function dist(x1, y1, x2, y2)
+ local dx = x1 - x2
+ local dy = y1 - y2
+ return (dx*dx + dy*dy)^0.5
+end