diff --git a/init.lua b/init.lua index 0b8725b..f0c0d68 100644 --- a/init.lua +++ b/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 diff --git a/manifest.module b/manifest.module index cda932b..39bf213 100644 --- a/manifest.module +++ b/manifest.module @@ -1,6 +1,6 @@ { "id": "map-editor", - "version": "0.2.0b.1", + "version": "0.2.0c", "kind": "module", "api": "^0.1", "ci_frames": 30,