From 141683438d257e73e1b82a11c3c58601bffd606e Mon Sep 17 00:00:00 2001 From: Calic Date: Tue, 12 May 2026 23:05:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(P.3.4):=20initial=20=E2=80=94=20Sporel=20L?= =?UTF-8?q?auncher=20v0.1.0=20(FSM=20+=20click-UI=20+=20quit-confirm)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fsm.lua | 25 +++++++++ init.lua | 133 ++++++++++++++++++++++++++++++++++++++++++++++++ manifest.core | 6 +++ manifest.module | 9 ++++ 4 files changed, 173 insertions(+) create mode 100644 fsm.lua create mode 100644 init.lua create mode 100644 manifest.core create mode 100644 manifest.module diff --git a/fsm.lua b/fsm.lua new file mode 100644 index 0000000..4bbfe3f --- /dev/null +++ b/fsm.lua @@ -0,0 +1,25 @@ +-- Sporel Launcher — pure-Lua finite state machine. +-- No engine.* calls; testable directly by lib-sporel.launcher-test. + +local M = {} + +M.STATE_LIST = "list" +M.STATE_QUIT_CONFIRM = "quit_confirm" +M.EXIT = "exit" -- sentinel: caller should engine.exit(0) + +-- Events: "esc", "enter", "click_yes", "click_no", "click_module" +function M.next(state, event) + if state == M.STATE_LIST then + if event == "esc" then return M.STATE_QUIT_CONFIRM end + return M.STATE_LIST + elseif state == M.STATE_QUIT_CONFIRM then + if event == "esc" then return M.STATE_LIST end + if event == "enter" then return M.EXIT end + if event == "click_yes" then return M.EXIT end + if event == "click_no" then return M.STATE_LIST end + return M.STATE_QUIT_CONFIRM + end + return state +end + +return M diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..1ad07f5 --- /dev/null +++ b/init.lua @@ -0,0 +1,133 @@ +local input = require("lib-core.input") +-- fsm.lua lives in this module's dir; engine_lua_lib's searcher only resolves +-- declared lib-deps, so multi-file modules use dofile + engine.module.dir_of. +local fsm = dofile(engine.module.dir_of("lib-sporel.launcher") .. "/fsm.lua") + +local STATE_LIST, STATE_QUIT, EXIT = fsm.STATE_LIST, fsm.STATE_QUIT_CONFIRM, fsm.EXIT +local state = STATE_LIST + +local modules = {} -- [{id, version, name, rect={x,y,w,h}}] +local quit_buttons = {} -- {yes = rect, no = rect} +local screen_w, screen_h = 1280, 720 -- matched ENGINE_WINDOW_DEFAULT_WIDTH/HEIGHT + +local TITLE = "Sporel Launcher" +local QUIT_PROMPT = "Sporel beenden?" +local YES_LABEL = "Ja" +local NO_LABEL = "Nein" +local EMPTY_LABEL = "Keine Module installiert." + +local FONT_TITLE = 28 +local FONT_BUTTON = 20 +local PAD_X = 16 +local PAD_Y = 8 + +local COLOR_BG = 0x202020FF +local COLOR_BTN = 0x404040FF +local COLOR_HOVER = 0x606060FF +local COLOR_TEXT = 0xE0E0E0FF +local COLOR_DIM = 0x000000B4 -- semi-transparent overlay +local COLOR_MODAL = 0x303030FF + +local hit = engine.spatial.aabb_contains_point + +function init(ctx) + local list = engine.module.list() + local y = 80 + for i, m in ipairs(list) do + local label = m.name or m.id + local w, h = engine.render.measure_text(label, FONT_BUTTON) + local btn_w = w + 2 * PAD_X + local btn_h = h + 2 * PAD_Y + modules[i] = { + id = m.id, + version = m.version, + name = label, + rect = { x = (screen_w - btn_w) / 2, y = y, w = btn_w, h = btn_h } + } + y = y + btn_h + 8 + end + + local wy, hy = engine.render.measure_text(YES_LABEL, FONT_BUTTON) + local wn, hn = engine.render.measure_text(NO_LABEL, FONT_BUTTON) + local btn_w = math.max(wy, wn) + 2 * PAD_X + local btn_h = math.max(hy, hn) + 2 * PAD_Y + local total_w = 2 * btn_w + 24 + quit_buttons.yes = { x = (screen_w - total_w) / 2, y = screen_h / 2 + 24, w = btn_w, h = btn_h } + quit_buttons.no = { x = (screen_w - total_w) / 2 + btn_w + 24, y = screen_h / 2 + 24, w = btn_w, h = btn_h } + + input.bind("ui_click", { "mouse_left" }) + input.bind("ui_back", { "escape" }) + input.bind("ui_confirm", { "enter" }) + + engine.print(string.format("launcher: init ok, modules=%d", #modules)) +end + +function update(ctx, dt) + local mx, my = engine.input.get_mouse_pos() + + if state == STATE_LIST then + if input.was_action_pressed("ui_back") then + state = fsm.next(state, "esc") + elseif input.was_action_pressed("ui_click") then + for _, m in ipairs(modules) do + if hit(m.rect.x, m.rect.y, m.rect.w, m.rect.h, mx, my) then + engine.switch_module(m.id) + return + end + end + end + elseif state == STATE_QUIT then + if input.was_action_pressed("ui_back") then + state = fsm.next(state, "esc") + elseif input.was_action_pressed("ui_confirm") then + engine.exit(0); return + elseif input.was_action_pressed("ui_click") then + if hit(quit_buttons.yes.x, quit_buttons.yes.y, quit_buttons.yes.w, quit_buttons.yes.h, mx, my) then + engine.exit(0); return + elseif hit(quit_buttons.no.x, quit_buttons.no.y, quit_buttons.no.w, quit_buttons.no.h, mx, my) then + state = fsm.next(state, "click_no") + end + end + end +end + +function render(ctx) + local mx, my = engine.input.get_mouse_pos() + + engine.render.clear_color(COLOR_BG) + local tw = engine.render.measure_text(TITLE, FONT_TITLE) + engine.render.draw_text(TITLE, (screen_w - tw) / 2, 24, FONT_TITLE, COLOR_TEXT) + + if #modules == 0 then + local ew = engine.render.measure_text(EMPTY_LABEL, FONT_BUTTON) + engine.render.draw_text(EMPTY_LABEL, (screen_w - ew) / 2, screen_h / 2, FONT_BUTTON, COLOR_TEXT) + else + for _, m in ipairs(modules) do + local color = hit(m.rect.x, m.rect.y, m.rect.w, m.rect.h, mx, my) and COLOR_HOVER or COLOR_BTN + engine.render.draw_rect(m.rect.x, m.rect.y, m.rect.w, m.rect.h, color) + local lw = engine.render.measure_text(m.name, FONT_BUTTON) + engine.render.draw_text(m.name, m.rect.x + (m.rect.w - lw) / 2, m.rect.y + PAD_Y, FONT_BUTTON, COLOR_TEXT) + end + end + + if state == STATE_QUIT then + engine.render.draw_rect(0, 0, screen_w, screen_h, COLOR_DIM) + local modal_w, modal_h = 360, 140 + engine.render.draw_rect((screen_w - modal_w) / 2, (screen_h - modal_h) / 2, modal_w, modal_h, COLOR_MODAL) + local pw = engine.render.measure_text(QUIT_PROMPT, FONT_BUTTON) + engine.render.draw_text(QUIT_PROMPT, (screen_w - pw) / 2, screen_h / 2 - 24, FONT_BUTTON, COLOR_TEXT) + + for _, key in ipairs({ "yes", "no" }) do + local b = quit_buttons[key] + local color = hit(b.x, b.y, b.w, b.h, mx, my) and COLOR_HOVER or COLOR_BTN + engine.render.draw_rect(b.x, b.y, b.w, b.h, color) + local label = (key == "yes") and YES_LABEL or NO_LABEL + local lw = engine.render.measure_text(label, FONT_BUTTON) + engine.render.draw_text(label, b.x + (b.w - lw) / 2, b.y + PAD_Y, FONT_BUTTON, COLOR_TEXT) + end + end +end + +function cleanup(ctx) + engine.print("launcher: cleanup") +end diff --git a/manifest.core b/manifest.core new file mode 100644 index 0000000..2cc1599 --- /dev/null +++ b/manifest.core @@ -0,0 +1,6 @@ +{ + "id": "lib-sporel.launcher.core", + "kind": "lib", + "version": "0.1.0", + "license": "Proprietary" +} diff --git a/manifest.module b/manifest.module new file mode 100644 index 0000000..a9f0351 --- /dev/null +++ b/manifest.module @@ -0,0 +1,9 @@ +{ + "id": "lib-sporel.launcher", + "version": "0.1.0", + "kind": "module", + "name": "Sporel Launcher", + "deps": [ + { "id": "lib-core.input", "version": "^0.3.0" } + ] +}