From 6d2d3ea26df086eb6d397e0434f74e704f4a1d10 Mon Sep 17 00:00:00 2001 From: Calic Date: Sun, 24 May 2026 01:22:49 +0200 Subject: [PATCH] Wire up editor: input bindings, frame hooks, launcher script init.lua loads the work map, binds hotkeys (E/R/S/Esc), tracks mouse-cell per frame, dispatches clicks to the UI hit-tester and falls through to paint-cell for unconsumed map-area clicks. run-map-editor.sh handles copy-in/copy-out for a real target map or runs in-place against the module's work slot. --- README.md | 45 +++++++++++++++++---- init.lua | 83 ++++++++++++++++++++++++++++++++++++--- scripts/run-map-editor.sh | 51 ++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 scripts/run-map-editor.sh diff --git a/README.md b/README.md index b69b126..9d37a30 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,46 @@ # sporel-module-map-editor -Interactive map editor module for v2 Sporel maps. Click-to-paint, vertical layer-chip overlay, collapsible atlas+tile picker. - -## Status - -`v0.1.0` — initial POC. See `docs/superpowers/specs/2026-05-23-sporel-module-map-editor-design.md` in `sporel-meta` for the full spec. +Interactive map editor for v2 Sporel map files. Click-to-paint, vertical layer-chip overlay on the left edge, collapsible atlas+tile picker bottom-left. ## Run +Default — edit the module's in-place work map: + ```bash -bash scripts/run-map-editor.sh [target_map.json] +bash scripts/run-map-editor.sh ``` -(Detailed hotkey reference and UI walkthrough added in a later commit.) +With a target — copy a real map in, edit, copy back out: + +```bash +bash scripts/run-map-editor.sh ~/Projects/Sporel/sporel-modules/spine-prototype/maps/demo.map.json +``` + +## UI + +- **Layer chips** (left edge, vertical): top-of-stack down. `Rf` at top toggles roof-edit mode; the other eight chips select the active layer. + - **Left-click** = set active layer (or toggle roof mode) + - **Right-click** = toggle layer visibility (Rf chip ignores right-click) +- **Atlas + tile picker** (bottom-left): collapsed chip shows the active tile. Click to expand; click again or outside to collapse. +- **Status chip** (bottom-right): current mouse-cell coordinate plus a `*` when the map has unsaved changes. + +## Hotkeys + +| Key | Action | +|---|---| +| `Left click` on map | Paint cell with active tile (or set roof flag in roof-mode) | +| `E` | Erase the cell under the mouse | +| `R` | Cycle rotation 0 → 1 → 2 → 3 → 0 | +| `S` | Save to disk (overwrites the work map) | +| `Esc` | Quit | + +## Limitations (v0.1.0) + +- Tile thumbnails in the picker render as gray placeholders with tile-IDs as text — lib-core.maps v0.4.0 does not expose per-tile UVs to Lua, so visual thumbnails are a v0.2.0 task. +- Atlas list in the picker is purely informational; if a map declares multiple atlases, the active atlas is set from `state.lua` defaults at startup and cannot yet be switched via UI (v0.2.0 hit-tests inside the expanded picker). +- Engine bindings `engine.render.draw_rect_lines` and `engine.render.draw_line` are not yet registered — active-chip borders, hidden-layer strikethroughs, and the entity-slot divider will trigger runtime errors when their code paths execute. Adding the bindings is a small engine-side follow-up. +- No undo/redo, no multi-tile brush, no entity placement, no heightmap editing — see the design spec §1 for the full out-of-scope list. + +## Spec + +`docs/superpowers/specs/2026-05-23-sporel-module-map-editor-design.md` in the `sporel-meta` repo. diff --git a/init.lua b/init.lua index 6f00cc1..5c6e87b 100644 --- a/init.lua +++ b/init.lua @@ -1,18 +1,91 @@ -- sporel-module-map-editor v0.1.0 --- Interactive map editor; full wiring lands in subsequent commits. +-- Interactive map editor — see docs/superpowers/specs/2026-05-23-sporel-module-map-editor-design.md -local maps = require("lib-core.maps") +local maps = require("lib-core.maps") +local camera = require("lib-core.camera") +local input = require("lib-core.input") +local state = require("src.state") +local actions = require("src.actions") +local ui = require("src.ui") -local work_map_id = maps.load("maps/work.map.json") +-- ===================================================================== +-- Setup +-- ===================================================================== +local WORK_PATH = "maps/work.map.json" +local work_map_id = maps.load(WORK_PATH) maps.set_current(work_map_id) maps.load_textures(engine.module.asset_aliases()) +state.set_map(work_map_id, WORK_PATH) -engine.print("map-editor: scaffold loaded, map_id=" .. work_map_id) +-- Center camera on the map +local m_size = maps.size() +local t_size = maps.tile_size() +camera.set_target((m_size.w * t_size) / 2, (m_size.h * t_size) / 2) +camera.set_zoom(1.0) +-- ===================================================================== +-- Input bindings +-- ===================================================================== +input.bind("quit", { "escape" }) +input.bind("erase", { "e" }) +input.bind("rotate", { "r" }) +input.bind("save_map", { "s" }) + +engine.print("map-editor: ready. Hotkeys: E=erase, R=rotate, S=save, ESC=quit") + +-- ===================================================================== +-- Frame hooks +-- ===================================================================== function update(ctx, dt) - -- TODO: wired in later commits + -- Quit + if input.was_action_pressed("quit") then + engine.exit() + return + end + + -- Update mouse-cell tracking for status chip + local mx, my = engine.input.get_mouse_pos() + local wx, wy = camera.screen_to_world(mx, my) + local cell_x = math.floor(wx / t_size) + local cell_y = math.floor(wy / t_size) + if cell_x >= 0 and cell_y >= 0 and cell_x < m_size.w and cell_y < m_size.h then + state.set_mouse_cell(cell_x, cell_y) + else + state.set_mouse_cell(nil) + end + + -- Hotkey: rotate + if input.was_action_pressed("rotate") then + actions.cycle_rotation() + end + + -- Hotkey: save + if input.was_action_pressed("save_map") then + actions.save() + end + + -- Hotkey: erase (uses current mouse-hover cell) + if input.was_action_pressed("erase") then + local cell = state.get_mouse_cell() + if cell then actions.erase_cell_at(cell.x, cell.y) end + end + + -- Mouse clicks: hit-test UI first, then map area + if engine.input.was_mouse_pressed(engine.input.MOUSE_LEFT) then + local consumed = ui.handle_click(mx, my, "left") + if not consumed and state.get_mouse_cell() then + local cell = state.get_mouse_cell() + actions.paint_cell_at(cell.x, cell.y) + end + end + if engine.input.was_mouse_pressed(engine.input.MOUSE_RIGHT) then + ui.handle_click(mx, my, "right") + end + + camera.update(dt) end function draw() maps.draw_map() + ui.draw() end diff --git a/scripts/run-map-editor.sh b/scripts/run-map-editor.sh new file mode 100644 index 0000000..dd52213 --- /dev/null +++ b/scripts/run-map-editor.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# run-map-editor.sh — launcher for sporel-module-map-editor +# Usage: bash scripts/run-map-editor.sh [target_map.json] +# +# If a target is given, copy it into the editor's work-slot before +# launching, then copy the edited result back to the target after exit. +# Without a target, edits the in-place work.map.json. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EDITOR_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +WORK_SLOT="$EDITOR_DIR/maps/work.map.json" +SPOREL_ROOT="$(cd "$EDITOR_DIR/../.." && pwd)" +SPOREL_BIN="${SPOREL_BIN:-$SPOREL_ROOT/sporel-engine/build/Sporel.exe}" + +if [ ! -x "$SPOREL_BIN" ]; then + echo "error: Sporel binary not found at $SPOREL_BIN" >&2 + echo "set SPOREL_BIN env-var or build the engine first" >&2 + exit 1 +fi + +TARGET="${1:-}" +if [ -n "$TARGET" ]; then + if [ ! -f "$TARGET" ]; then + echo "error: target map not found: $TARGET" >&2 + exit 1 + fi + echo "copying $TARGET -> $WORK_SLOT" + cp "$TARGET" "$WORK_SLOT" +fi + +echo "launching map-editor on $WORK_SLOT" + +export SPOREL_LIBS_DIR="$SPOREL_ROOT/sporel-libs" +export SPOREL_MODULES_DIR="$SPOREL_ROOT/sporel-modules" +export SPOREL_MODULE=map-editor + +LOG_FILE="${SPOREL_LOG:-/tmp/sporel-last-run.log}" +set +e +"$SPOREL_BIN" "$@" 2> >(tee "$LOG_FILE" >&2) +rc=$? +set -e + +if [ -n "$TARGET" ] && [ $rc -eq 0 ]; then + echo "copying $WORK_SLOT -> $TARGET" + cp "$WORK_SLOT" "$TARGET" +fi + +echo "done (exit $rc)" +exit $rc