commit 0ca2bda7fbd4dab0a46e0bbff735fcb6b6c4a2cb Author: Axel Meyer Date: Thu May 14 18:04:01 2026 +0200 feat(P.3.8): rts-prototype v0.1.0 — initial release Pure-RTS Composer-Demo integriert P.3.4-P.3.7 Libs. - 20x20 grass map + 3 interior wall-clusters - 8 units in 3 speed-classes (100/150/200 px/s) - LMB-select + RMB-move via selection.list as target_provider - Free-pan camera (keys + edge + middle-drag, no bounds) - ESC -> launcher (ADR-0028) - SPOREL_CI=1: 60-frame self-exit + active move on frame 5 See: meta/docs/superpowers/specs/2026-05-14-p3-8-rts-prototype-design.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..59a1628 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# rts-prototype — v0.1.0 + +Pure-RTS Composer-Demo integriert P.3.4-P.3.7 Libs. + +## Controls + +- **LMB-Click**: Select a unit +- **LMB-Drag**: Box-select multiple units +- **Shift+LMB**: Add to selection +- **Ctrl+LMB**: Toggle unit in selection +- **RMB-Click**: Move selected units to clicked map-position +- **WASD / Arrows**: Pan camera +- **Mouse-Edge**: Edge-scroll camera +- **Middle-Drag**: Pan camera by dragging +- **ESC**: Return to launcher + +## Demo + +8 units in 3 speed-classes (rot=100, braun=150, gelb=200 px/s) clustered +in the left half of a 20×20 map. Move them across the map; speed-variation +visible. Units walk through wall-clusters (direct-line-move; pathfinding +deferred to P.4). + +## Deps + +- lib-core.maps, lib-core.render — map + rendering +- lib-core.camera v0.3.0 — free-pan + screen-to-world +- lib-core.input v0.3.0 — action bindings +- lib-core.selection v0.1.0 — click-select + drag-box +- lib-core.command v0.1.0 — RMB-to-move + +See `meta/docs/superpowers/specs/2026-05-14-p3-8-rts-prototype-design.md` for full design. diff --git a/assets/tiles/rts_tilemap.tilemap.json b/assets/tiles/rts_tilemap.tilemap.json new file mode 100644 index 0000000..b375cf2 --- /dev/null +++ b/assets/tiles/rts_tilemap.tilemap.json @@ -0,0 +1,8 @@ +{ + "id": "rts_tilemap", + "tile_size": 32, + "tiles": [ + {"id": "grass", "walkable": true, "color": [110, 170, 80]}, + {"id": "stone", "walkable": false, "color": [120, 120, 120]} + ] +} diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..e7f0b4d --- /dev/null +++ b/init.lua @@ -0,0 +1,159 @@ +-- ===================================================================== +-- rts-prototype v0.1.0 — Pure-RTS Composer-Demo (P.3.8) +-- See: meta/docs/superpowers/specs/2026-05-14-p3-8-rts-prototype-design.md +-- +-- Composer-Module per §3.2 Read-vs-Write-Linie: orchestriert alle +-- cross-lib-Setter-Aufrufe. Libs bleiben unabhängig. +-- +-- Demo: 8 units, 3 speed-classes (100/150/200), LMB-select + RMB-move +-- via selection.list as target_provider. ESC → Launcher (ADR-0028). +-- SPOREL_CI=1: 60-frame self-exit + active move auf Frame 5 für smoke. +-- ===================================================================== + +local maps = require("lib-core.maps") +local r_lib = require("lib-core.render") +local camera = require("lib-core.camera") +local input = require("lib-core.input") +local selection = require("lib-core.selection") +local command = require("lib-core.command") + +local rts_map = maps.load("maps/rts.map.json") +maps.set_current(rts_map) + +local m_size = maps.size() +local t_size = maps.tile_size() +local map_center_x = (m_size.w * t_size) / 2 +local map_center_y = (m_size.h * t_size) / 2 + +camera.set_target(map_center_x, map_center_y) +camera.set_zoom(1.0) + +-- ===== Input Bindings (top-level, init-time) ===== +input.bind("quit", { "escape" }) + +input.bind("pan_left", { "a", "left" }) +input.bind("pan_right", { "d", "right" }) +input.bind("pan_up", { "w", "up" }) +input.bind("pan_down", { "s", "down" }) +input.bind("pan_drag", { "mouse_middle" }) + +input.bind("lmb", { "mouse_left" }) +input.bind("mod_add", { "shift" }) +input.bind("mod_toggle", { "ctrl" }) + +input.bind("rmb", { "mouse_right" }) + +-- ===== Camera (free-pan, no bounds-clamp) ===== +camera.bind_pan_keys("pan_left", "pan_right", "pan_up", "pan_down") +camera.bind_pan_drag("pan_drag") +camera.enable_pan_edge() + +-- ===== Selection ===== +selection.bind_action("lmb") +selection.bind_modifier_add("mod_add") +selection.bind_modifier_toggle("mod_toggle") + +-- ===== Command ===== +command.bind_move_action("rmb") +command.bind_target_provider(function() return selection.list() end) + +-- ===== Units (8 total, 3 speed-classes) ===== +local units = { + -- 3 graue Krieger (speed 100, slow) + { x = 64, y = 96, w = 16, h = 16, color = {180, 60, 60}, speed = 100 }, + { x = 64, y = 128, w = 16, h = 16, color = {180, 60, 60}, speed = 100 }, + { x = 64, y = 160, w = 16, h = 16, color = {180, 60, 60}, speed = 100 }, + -- 3 braune Skouts (speed 150, mid) + { x = 96, y = 96, w = 16, h = 16, color = {200, 130, 60}, speed = 150 }, + { x = 96, y = 128, w = 16, h = 16, color = {200, 130, 60}, speed = 150 }, + { x = 96, y = 160, w = 16, h = 16, color = {200, 130, 60}, speed = 150 }, + -- 2 gelbe Späher (speed 200, fast) + { x = 128, y = 96, w = 16, h = 16, color = {220, 200, 60}, speed = 200 }, + { x = 128, y = 128, w = 16, h = 16, color = {220, 200, 60}, speed = 200 }, +} + +-- ===== CI state ===== +local frames = 0 +local TARGET_FRAMES = 60 +local CI_MOVE_FRAME = 5 + +function init(ctx) + engine.print("rts-prototype: init ok") + engine.print(string.format("map: %dx%d tiles, render: %d tiles per frame", + m_size.w, m_size.h, m_size.w * m_size.h)) + + for _, u in ipairs(units) do + u.sel_handle = selection.register(function() + return { x = u.x, y = u.y, w = u.w, h = u.h } + end) + u.cmd_handle = command.register( + function() return { x = u.x, y = u.y } end, + function(nx, ny) u.x = nx; u.y = ny end, + u.speed + ) + end + engine.print(string.format("rts-prototype: %d units registered (selectable + movable)", + #units)) + engine.print(string.format("input: %d actions bound", input.action_count())) +end + +function update(ctx, dt) + if input.was_action_pressed("quit") then + engine.print("rts-prototype: ESC -> launcher") + engine.switch_module("lib-sporel.launcher") + return + end + + camera.update(dt) + selection.update(dt) + command.update(dt) + + -- CI active-demo: programmatic move auf Frame 5 (smoke-Coverage) + frames = frames + 1 + if frames == CI_MOVE_FRAME and os.getenv("SPOREL_CI") == "1" then + local handles = {} + for _, u in ipairs(units) do handles[#handles + 1] = u.cmd_handle end + command.move_to(handles, 500, 500) + engine.print("rts-prototype: CI move dispatched (8 units -> 500,500)") + end + + -- Position trace für smoke-Assert (frame 30 — units sollten sich bewegt haben) + if frames == 30 and os.getenv("SPOREL_CI") == "1" then + engine.print(string.format("rts-prototype: frame 30 unit[1] pos=(%d,%d)", + units[1].x, units[1].y)) + end + + -- CI self-exit + if os.getenv("SPOREL_CI") == "1" and frames >= TARGET_FRAMES then + engine.exit(0) + end +end + +function render(ctx) + engine.render.clear_color(engine.render.rgb(20, 20, 30)) + camera.begin() + r_lib.draw_map() + for _, u in ipairs(units) do + engine.render.draw_rect(u.x, u.y, u.w, u.h, + engine.render.rgb(u.color[1], u.color[2], u.color[3])) + end + selection.render_highlights() + camera.finish() + selection.render_drag_box() +end + +function cleanup(ctx) + engine.print(string.format("rts-prototype: cleanup (total_frames=%d)", frames)) +end + +-- DEPRECATED-MVP: pathfinding (A*/nav-mesh) — units walk through walls (P.4) +-- DEPRECATED-MVP: faction-system (color-clusters are cosmetic only, no API) +-- DEPRECATED-MVP: win/lose-conditions (sandbox demo) +-- DEPRECATED-MVP: resource-nodes / harvest (P.4) +-- DEPRECATED-MVP: enemy-AI (P.4+) +-- DEPRECATED-MVP: save/load (no persistent state) +-- DEPRECATED-MVP: HUD/UI overlay (selection-count, unit-info) +-- DEPRECATED-MVP: mini-map (RTS polish) +-- DEPRECATED-MVP: bounds-clamp camera (currently free-roam) +-- DEPRECATED-MVP: attack-command (with faction-system, P.4) +-- DEPRECATED-MVP: stop-command (with attack, P.4) diff --git a/manifest.core b/manifest.core new file mode 100644 index 0000000..9665526 --- /dev/null +++ b/manifest.core @@ -0,0 +1,6 @@ +{ + "id": "rts-prototype.core", + "kind": "lib", + "version": "0.1.0", + "license": "Proprietary" +} diff --git a/manifest.module b/manifest.module new file mode 100644 index 0000000..0112f26 --- /dev/null +++ b/manifest.module @@ -0,0 +1,15 @@ +{ + "id": "rts-prototype", + "version": "0.1.0", + "kind": "module", + "api": "^0.1", + "entry_point": "init.lua", + "deps": [ + {"id": "lib-core.maps", "version": "0.1.1"}, + {"id": "lib-core.render", "version": "0.1.0"}, + {"id": "lib-core.camera", "version": "0.3.0"}, + {"id": "lib-core.input", "version": "0.3.0"}, + {"id": "lib-core.selection", "version": "0.1.0"}, + {"id": "lib-core.command", "version": "0.1.0"} + ] +} diff --git a/maps/rts.map.json b/maps/rts.map.json new file mode 100644 index 0000000..7d130aa --- /dev/null +++ b/maps/rts.map.json @@ -0,0 +1,27 @@ +{ + "id": "rts", + "tilemap": "rts_tilemap", + "size": {"w": 20, "h": 20}, + "tiles": [ + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,2,2,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2, + 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2 + ] +}