feat(map-editor): v0.2.0c — transform controls + Direct-mode override paint
State + actions:
- active_flip (0/1) joins active_rot (0..3). Combined the two cover
all 8 D4 orientations; single Flip button is sufficient (H+V
would be redundant).
- toggle_flip / reset_transform / paint_override_at /
erase_override_at.
Toolbar:
- New Flip (H) + Reset (0) buttons next to Rotate (R).
- Status chip extended with `slot=N rot=N flip=N` indicator.
Paint:
- Direct-mode + LMB on canvas now calls maps.set_override on the
active material-bound terrain layer. Bare slot int when canonical
(rot=0+flip=0), {slot, rot, flip} object otherwise — matches the
v0.5.1 override-format extension. Palette slot 1..14 maps to lib
slot 0..13 (palette is user-friendly 1-based; lib API is 0-based).
- Direct-mode + RMB on canvas calls maps.clear_override.
- Auto-Tile mode unchanged; transform state is ignored there (the
renderer derives slot/rot/flip from the vertex bitmask).
Hotkeys:
- h -> flip (new)
- 0 -> reset_transform (new)
- r continues to cycle rotation CW; Shift+R for CCW is out because
lib-core.input has no modifier-combo support (deferred).
Cheatsheet rows updated.
This commit is contained in:
93
init.lua
93
init.lua
@@ -1,4 +1,4 @@
|
||||
-- sporel-module-map-editor v0.2.0b.1
|
||||
-- sporel-module-map-editor v0.2.0c
|
||||
-- Interactive map editor — Rev 4 UX architecture (see design paper
|
||||
-- 2026-05-28-autotile-blob-styles-design.md §11 + plan stub
|
||||
-- 2026-05-28-map-editor-blob-v2.md). 0.2.0b adds the bottom palette
|
||||
@@ -6,12 +6,13 @@
|
||||
-- needs a lib-core.maps tile-UV getter) + Auto-Tile vertex painting
|
||||
-- via maps.set_vertex + a vertex-grid overlay. 0.2.0b.1 migrates
|
||||
-- work.map.json to schema-v3 + binds surface/wall layers to the
|
||||
-- blob_rect_stone material so Auto-Tile painting actually renders;
|
||||
-- before that the legacy fa_terrain_v1 atlas was loaded which has no
|
||||
-- blob-14 slot layout, so paint had no visible effect.
|
||||
-- Direct-mode painting still uses the v0.1 cell-paint path.
|
||||
-- Transform controls + Direct-Override + Decal/Entity sub-selectors
|
||||
-- land in 0.2.0c/d.
|
||||
-- blob_rect_stone material so Auto-Tile painting actually renders.
|
||||
-- 0.2.0c adds Transform controls (Flip H, Reset) + Direct-mode
|
||||
-- override paint that writes maps.set_override with the current
|
||||
-- {slot, rot, flip} on a material-bound terrain layer. Single
|
||||
-- `flip` flag (0/1) combined with rot (0..3) covers all 8 D4
|
||||
-- orientations; H+V buttons are redundant so we ship just one.
|
||||
-- Decal/Entity sub-selectors + Material-Properties modal land in 0.2.0d.
|
||||
--
|
||||
-- All editor logic in this single file: engine sandbox only allows
|
||||
-- require() for declared lib-deps; multi-file modules either use
|
||||
@@ -46,6 +47,7 @@ local state = (function()
|
||||
active_atlas = 0, -- atlas_index 0..N-1
|
||||
active_tile = 1, -- tile_id
|
||||
active_rot = 0, -- 0..3
|
||||
active_flip = 0, -- 0.2.0c: 0 or 1 (single flip; combined with rot covers all 8 D4 orientations)
|
||||
mode = "auto-tile", -- 0.2.0a: "auto-tile" | "direct"
|
||||
roof_mode = false, -- if true, paint targets roof
|
||||
layer_visible = {
|
||||
@@ -85,6 +87,14 @@ local state = (function()
|
||||
end
|
||||
function M.get_active_rot() return state.active_rot end
|
||||
|
||||
-- 0.2.0c: transform state for Direct-mode override paint.
|
||||
function M.toggle_flip() state.active_flip = (state.active_flip == 0) and 1 or 0 end
|
||||
function M.get_active_flip() return state.active_flip end
|
||||
function M.reset_transform()
|
||||
state.active_rot = 0
|
||||
state.active_flip = 0
|
||||
end
|
||||
|
||||
function M.toggle_roof_mode()
|
||||
state.roof_mode = not state.roof_mode
|
||||
end
|
||||
@@ -144,6 +154,7 @@ local state = (function()
|
||||
state.active_atlas = 0
|
||||
state.active_tile = 1
|
||||
state.active_rot = 0
|
||||
state.active_flip = 0
|
||||
state.mode = "auto-tile"
|
||||
state.roof_mode = false
|
||||
state.dirty = false
|
||||
@@ -200,6 +211,27 @@ local actions = (function()
|
||||
state.mark_dirty()
|
||||
end
|
||||
|
||||
-- 0.2.0c: Direct-mode override paint on a material-bound terrain layer.
|
||||
-- Converts palette tile (1..14) -> lib slot (0..13). Writes bare slot
|
||||
-- in canonical orientation (rot=0+flip=0), else {slot, rot, flip} object.
|
||||
function M.paint_override_at(cell_x, cell_y)
|
||||
local slot = state.get_active_tile() - 1 -- palette 1..14 -> lib slot 0..13
|
||||
local rot = state.get_active_rot()
|
||||
local flip = state.get_active_flip()
|
||||
if rot == 0 and flip == 0 then
|
||||
maps.set_override(state.get_active_layer(), cell_x, cell_y, slot)
|
||||
else
|
||||
maps.set_override(state.get_active_layer(), cell_x, cell_y,
|
||||
{ slot = slot, rot = rot, flip = flip })
|
||||
end
|
||||
state.mark_dirty()
|
||||
end
|
||||
|
||||
function M.erase_override_at(cell_x, cell_y)
|
||||
maps.clear_override(state.get_active_layer(), cell_x, cell_y)
|
||||
state.mark_dirty()
|
||||
end
|
||||
|
||||
-- =====================================================================
|
||||
-- Hotkey-driven actions
|
||||
-- =====================================================================
|
||||
@@ -255,6 +287,10 @@ local actions = (function()
|
||||
function M.set_mode(m) state.set_mode(m) end
|
||||
function M.toggle_cheatsheet() state.toggle_cheatsheet() end
|
||||
|
||||
-- 0.2.0c actions
|
||||
function M.toggle_flip() state.toggle_flip() end
|
||||
function M.reset_transform() state.reset_transform() end
|
||||
|
||||
return M
|
||||
end)()
|
||||
|
||||
@@ -293,7 +329,7 @@ local ui = (function()
|
||||
local PALETTE_SLOTS = 14 -- matches S-V2E2-RM-Blob baseline
|
||||
|
||||
-- Status chip (bottom-right, mouse cell + dirty)
|
||||
local STATUS_W = 200
|
||||
local STATUS_W = 320
|
||||
local STATUS_H = 22
|
||||
local STATUS_MARGIN = 8
|
||||
|
||||
@@ -349,6 +385,8 @@ local ui = (function()
|
||||
{ id="save", label="Save", hotkey="S" },
|
||||
{ id="erase", label="Erase", hotkey="E" },
|
||||
{ id="rotate", label="Rotate", hotkey="R" },
|
||||
{ id="flip", label="Flip", hotkey="H" },
|
||||
{ id="reset_xform", label="Reset", hotkey="0" },
|
||||
{ id="help", label="Help", hotkey="I" },
|
||||
}
|
||||
|
||||
@@ -358,7 +396,9 @@ local ui = (function()
|
||||
{ key="I", action="Show / hide this cheatsheet" },
|
||||
{ key="S", action="Save current map to work.map.json" },
|
||||
{ key="E", action="Erase tile at mouse cell" },
|
||||
{ key="R", action="Cycle tile rotation 0->90->180->270" },
|
||||
{ key="R", action="Cycle tile rotation 0->90->180->270 (Direct-mode override)" },
|
||||
{ key="H", action="Toggle tile flip 0<->1 (Direct-mode override)" },
|
||||
{ key="0", action="Reset transform (rot=0, flip=0)" },
|
||||
{ key="Esc", action="Quit editor" },
|
||||
{ key="LMB on canvas (Auto-Tile)", action="Paint vertex on active layer (any-corner fill)" },
|
||||
{ key="RMB on canvas (Auto-Tile)", action="Clear vertex on active layer" },
|
||||
@@ -603,7 +643,10 @@ local ui = (function()
|
||||
local cell = state.get_mouse_cell()
|
||||
local cell_str = cell and string.format("Mouse: (%d,%d)", cell.x, cell.y) or "Mouse: (-,-)"
|
||||
local dirty_str = state.is_dirty() and " * unsaved" or ""
|
||||
engine.render.draw_text(cell_str .. dirty_str, s.x + 4, s.y + 4, 12,
|
||||
-- 0.2.0c: append the current direct-paint transform state.
|
||||
local xform_str = string.format(" slot=%d rot=%d flip=%d",
|
||||
state.get_active_tile(), state.get_active_rot(), state.get_active_flip())
|
||||
engine.render.draw_text(cell_str .. dirty_str .. xform_str, s.x + 4, s.y + 4, 12,
|
||||
rgba(COL_TEXT, 0xFF))
|
||||
end
|
||||
|
||||
@@ -682,6 +725,8 @@ local ui = (function()
|
||||
local cell = state.get_mouse_cell()
|
||||
if cell then actions.erase_cell_at(cell.x, cell.y) end
|
||||
elseif id == "rotate" then actions.cycle_rotation()
|
||||
elseif id == "flip" then actions.toggle_flip()
|
||||
elseif id == "reset_xform" then actions.reset_transform()
|
||||
elseif id == "help" then actions.toggle_cheatsheet()
|
||||
end
|
||||
end
|
||||
@@ -769,8 +814,10 @@ input.bind("rotate", { "r" })
|
||||
input.bind("save_map", { "s" })
|
||||
input.bind("toggle_mode", { "tab" }) -- 0.2.0a
|
||||
input.bind("toggle_cheatsheet", { "i" }) -- 0.2.0a — `i` for Info/help (no shifted-key support in input lib)
|
||||
input.bind("flip", { "h" }) -- 0.2.0c
|
||||
input.bind("reset_transform", { "0" }) -- 0.2.0c
|
||||
|
||||
engine.print("map-editor v0.2.0b.1: ready. Tab=mode, I=help, S=save, E=erase, R=rotate, ESC=quit")
|
||||
engine.print("map-editor v0.2.0c: ready. Tab=mode, I=help, S=save, E=erase, R=rotate, H=flip, 0=reset, ESC=quit")
|
||||
|
||||
-- =====================================================================
|
||||
-- Frame hooks
|
||||
@@ -842,6 +889,14 @@ function update(ctx, dt)
|
||||
actions.toggle_cheatsheet()
|
||||
end
|
||||
|
||||
-- 0.2.0c hotkeys: transform controls (Direct-mode override paint)
|
||||
if input.was_action_pressed("flip") then
|
||||
actions.toggle_flip()
|
||||
end
|
||||
if input.was_action_pressed("reset_transform") then
|
||||
actions.reset_transform()
|
||||
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")
|
||||
@@ -850,16 +905,24 @@ function update(ctx, dt)
|
||||
local snap = state.get_snap_vertex()
|
||||
if snap then actions.paint_vertex_at(snap.vx, snap.vy, true) end
|
||||
elseif state.get_mouse_cell() then
|
||||
-- 0.2.0c: Direct-mode on a material-bound terrain layer
|
||||
-- writes an override per cell, honouring active rot+flip.
|
||||
local cell = state.get_mouse_cell()
|
||||
actions.paint_cell_at(cell.x, cell.y)
|
||||
actions.paint_override_at(cell.x, cell.y)
|
||||
end
|
||||
end
|
||||
end
|
||||
if engine.input.was_mouse_pressed(engine.input.MOUSE_RIGHT) then
|
||||
local consumed = ui.handle_click(mx, my, "right")
|
||||
if not consumed and state.get_mode() == "auto-tile" then
|
||||
local snap = state.get_snap_vertex()
|
||||
if snap then actions.paint_vertex_at(snap.vx, snap.vy, false) end
|
||||
if not consumed then
|
||||
if state.get_mode() == "auto-tile" then
|
||||
local snap = state.get_snap_vertex()
|
||||
if snap then actions.paint_vertex_at(snap.vx, snap.vy, false) end
|
||||
elseif state.get_mouse_cell() then
|
||||
-- 0.2.0c: Direct + RMB on canvas clears the override.
|
||||
local cell = state.get_mouse_cell()
|
||||
actions.erase_override_at(cell.x, cell.y)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "map-editor",
|
||||
"version": "0.2.0b.1",
|
||||
"version": "0.2.0c",
|
||||
"kind": "module",
|
||||
"api": "^0.1",
|
||||
"ci_frames": 30,
|
||||
|
||||
Reference in New Issue
Block a user