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.
This commit is contained in:
Calic
2026-05-24 01:22:49 +02:00
parent f8ce43784f
commit 6d2d3ea26d
3 changed files with 167 additions and 12 deletions

View File

@@ -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.

View File

@@ -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 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

51
scripts/run-map-editor.sh Normal file
View File

@@ -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