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:
45
README.md
45
README.md
@@ -1,15 +1,46 @@
|
|||||||
# sporel-module-map-editor
|
# sporel-module-map-editor
|
||||||
|
|
||||||
Interactive map editor module for v2 Sporel maps. Click-to-paint, vertical layer-chip overlay, collapsible atlas+tile picker.
|
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.
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
Default — edit the module's in-place work map:
|
||||||
|
|
||||||
```bash
|
```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.
|
||||||
|
|||||||
81
init.lua
81
init.lua
@@ -1,18 +1,91 @@
|
|||||||
-- sporel-module-map-editor v0.1.0
|
-- 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.set_current(work_map_id)
|
||||||
maps.load_textures(engine.module.asset_aliases())
|
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)
|
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
|
end
|
||||||
|
|
||||||
function draw()
|
function draw()
|
||||||
maps.draw_map()
|
maps.draw_map()
|
||||||
|
ui.draw()
|
||||||
end
|
end
|
||||||
|
|||||||
51
scripts/run-map-editor.sh
Normal file
51
scripts/run-map-editor.sh
Normal 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
|
||||||
Reference in New Issue
Block a user