Overview - purpose, terminology, lifecycle, and the core editing model.
Served from /wiki

Map Editor

What This Tool Is

Popup editor Theme-aware JSON-backed

The map editor is a standalone popup app launched from the main content editor. It is not a thin form field sitting on top of map JSON. It owns its own state, controls, render loop, history stack, and save workflow, then syncs its results back into the host editor through API writes and postMessage events.

Menu Bar

Undo, redo, save, quick layer selection, and theme switching live here. This is the stable command layer.

Tools

The left tool panel hosts Information, Maps, History, Instances, Tiles, Layers, and prototype placement tabs.

Canvas

The main viewport is a tile-grid world with snapping, drag placement, context menus, zoom, pan, selection, and the minimap drawer.

The editor is intentionally moving toward becoming the main map-authoring platform. It already has enough local behavior, persistence, and rendering independence to be treated as a real app rather than a popup form.

Terminology and Mental Model

Naming

  • Menu Bar - the top command strip.
  • Tools - the left-side panel with tabs and lists.
  • Canvas - the world viewport where tiles and instances are edited.

Authored entities

  • Tile - a sprite-backed paintable map cell resource.
  • Template - a reusable instance source used like a stamp.
  • Instance - a placed or unplaced map-local entity created from a template by value, not by live linkage.

Layer numbering

Internal layer 0 is the anchored Background. The first user-facing non-background paint layer is displayed as Layer 0, because its internal id is 1.

In short: Background is special, anchored at the bottom, and every other displayed layer name is offset by one.

Ownership boundary

  • The host editor owns dataset loading, popup launch, and background refresh after save.
  • The popup editor owns editing state, rendering, history, drag logic, and the final composed save payload.

Core Principles

Edit local, save explicit

The popup mutates local runtime state first. Nothing is persisted until Save writes maps and NPCs back through the API.

History is per map

Undo and redo belong to the active map, not the whole app. Branches are truncated if you edit after undoing.

Templates are stamps

Selecting a template keeps stamp mode active so repeated clicks place new instances. A created instance is then independent.

Rendering is viewport-first

The editor draws only what the current viewport needs, then uses cached preview surfaces for pan, zoom, scroll, and the minimap.

Background is optimized

The editor can compress fully implicit background rows so huge maps do not waste JSON storing the same map-wide fill repeatedly.

Prototype tabs are honest

Monsters, Triggers, Paths, and Transitions already share selector and folder UI, but placement logic is intentionally not claimed yet.

Daily Workflows

This section is written as direct operating procedure. If you want to use the tool instead of study it, start here.

Open the editor for a map
  1. Open the main content editor and select Maps.
  2. Select the map record you want to edit.
  3. Use the dedicated launch button to open the popup editor.
  4. The host assembles bootstrap data, opens map-editor-popup.html?token=..., and hands the popup a full in-memory startup package.
Paint tiles
  1. Go to Tiles in Tools.
  2. Select a tile brush entry from the palette list.
  3. Choose your active layer from the Layers tab or the menu-bar layer selector.
  4. Click-drag on the canvas to paint.
  5. Use Alt + Drag to erase on the active layer.
  6. Use L Shift + Drag to line-lock after leaving the origin tile.
  7. Use L Ctrl + Drag for a rectangle outline, or R Ctrl + Drag for a circle outline.
Fill the background layer
  1. Switch to Background or set the current editable layer so drawing resolves to the background layer.
  2. Right-click a tile in the tile palette.
  3. Choose Fill Background.
  4. The editor stores the background tile id at map level and can compress fully implicit background rows on save.

Roomwide fill is intentionally restricted to Background.

Place instances from templates
  1. Open Instances.
  2. In the Templates section, select the template you want.
  3. Click the canvas to stamp a new instance.
  4. The template remains active so each click keeps creating a fresh instance.
  5. Each created instance copies template values by value and is no longer live-linked to the template record.
Select, center, and place an instance record
  1. In the Instances list, click an existing instance.
  2. If it is already placed, the camera recenters toward it.
  3. If it is unplaced, the editor enters ghost placement mode and shows the silhouette under the cursor.
  4. Click the canvas to drop it with grid snapping.

Placeholder instances can exist without placement. That is part of the authoring model, not a bug.

Reorder layers
  1. Open Layers.
  2. Drag a non-background layer by its handle.
  3. Dropping changes its draw depth and remaps tile and instance layer references accordingly.
  4. The background layer is anchored and cannot be dragged below or above other layers.
Use folders in selector lists
  1. Use the folder button at the top of a supported panel.
  2. Create folders for Tiles, Templates, Instances, Monsters, Triggers, Paths, or Transitions.
  3. Drag selectors into a folder to group them.
  4. Drag them back out to return them to the root of that panel.

Folder data is editor UI persistence only. It changes organization, not gameplay payloads.

Navigate the room quickly
  • MMB + Drag pans the room.
  • Ctrl + Wheel zooms around the pointer anchor.
  • The minimap drawer provides click-to-center navigation and a live viewport rectangle.
  • Dragging an instance near the viewport edge auto-pans the camera in that direction.
  • R Shift temporarily hides the grid so you can inspect the runtime-like composition.
Import sprites or tiles from another editor build
  1. Open Information.
  2. Expand Experimental Imports.
  3. Import from file, or open the JSON paste modal with the writing-pad button.
  4. The import pipeline accepts a single entry or a whole compatible gallery payload.
  5. Known resources are deduped by normalized dimensions, pixel scale, and row content signature.
Save without surprises
  1. Use Save in the menu bar when the save button is enabled.
  2. The popup writes maps first, then npcs.
  3. After success it posts map-editor-saved to the opener so the host editor can refresh quietly.
  4. If you undo, then make a new edit, the impossible future branch is discarded and history continues from the new point.

Feature Reference

Menu Bar

  • Undo and Redo are bound to toolbar buttons and Ctrl+Z / Ctrl+Y.
  • Save reflects dirty history state and is disabled when nothing changed or a save is running.
  • The centered layer selector mirrors the Layers tab and stays in sync with it.
  • Theme preset buttons apply editor-wide palette swaps through /api/editor-settings.

Information Tab

  • Locked map id.
  • Editable map name.
  • Width and height with explicit apply/cancel controls.
  • Map background color and background brush mode.
  • Experimental sprite/tile import tools.
  • In-editor controls reference and footer links.

Maps Tab

Switch maps, create maps, and delete maps. Switching away with unsaved changes prompts first. Creating a new map seeds a background layer and a first editable layer.

Layers Tab

  • All Layers mode for draw-depth inspection.
  • Visibility toggle per layer.
  • Layer reordering for non-background layers.
  • Context-menu rename support.
  • Background layer anchored at the bottom.

Tiles Tab

  • Sprite-backed tile selector list instead of simple swatches.
  • Right-click actions: select tile, fill background, replace on current layer, inspect id/symbol.
  • Transparency honors . as no-color data.
  • Selection reticle scales with grid size.

Instances Tab

  • Templates stamp fresh records repeatedly.
  • Placed instances recenter the camera when selected.
  • Unplaced instances enter ghost placement mode.
  • Placeholder markers use a clashing multi-color orb so they remain visible on mixed backgrounds.

Canvas Interaction Set

Selection

Tile and instance selection uses a reusable reticle with directional markers so selected cells read clearly across different grid sizes.

Dragging

Instance dragging snaps to the grid, previews the destination, and now auto-pans near the viewport edges so long repositioning feels continuous.

Context Menus

The reusable right-click panel can be attached across the editor. It already powers layer actions, tile actions, and canvas entity actions.

Minimap Drawer

The minimap is a docked drawer with a live maintained surface. Opening it reveals current state immediately rather than taking a fresh snapshot first.

Hotkey Cursor Feedback

Shift, Alt, and inspect modes swap the cursor so the canvas communicates line draw, erase, and no-grid inspection states without needing extra text.

Warm Preview Modes

Zoom, drag-pan, and wheel-scroll all use cached low-res preview frames before a sharper redraw lands, which keeps movement feeling much smoother.

Prototype panels for Monsters, Triggers, Paths, and Transitions already reuse selector, folder, and panel framing. They are scaffolding for future map-local authoring, not finished gameplay editors yet.

Technical Systems

File Role What It Owns
src/components/MapEditorPanels.tsxHost bridgeLaunches the popup, assembles bootstrap payload, handles save/open postMessage events, and persists popup bounds.
src/mapEditorPopup/bootstrap.tsPopup handoffToken generation, opener registry, sessionStorage fallback, and bootstrap retrieval.
src/mapEditorPopup/main.tsPopup bootLoads the bootstrap, applies editor theme settings, injects popup HTML/CSS, and starts the runtime.
src/mapEditorPopup/runtime.tsState rootGlobal editor state, DOM lookup, layer helpers, data catalogs, and controller wiring.
src/mapEditorPopup/renderController.tsRender loopViewport drawing, tile surface cache, minimap surface, preview frames, overlay drawing, and meta telemetry.
src/mapEditorPopup/interactionController.tsInput systemMouse, wheel, keyboard, paint strokes, shape tools, drag logic, auto-pan, and context menu triggers.
src/mapEditorPopup/sidebarController.tsTools UITab switching, layer list, information panel logic, palette lists, folder rendering, and inline status text.
src/mapEditorPopup/npcController.tsInstance semanticsTemplate assignment, instance centering, placeholder handling, sprite binding, and instance selection behavior.
src/mapEditorPopup/historyController.tsUndo/redo engineState capture, branch truncation, persistence, restore, toolbar dirty state, and preview diffs.
src/mapEditorPopup/persistenceController.tsSave pipelinePayload rebuild, map compression rules, dual-save ordering, and host notification after save.
src/mapEditorPopup/importController.tsResource importSprite/tile import normalization, dedupe signatures, JSON modal import, and content save for imported art.

Rendering Strategy

Viewport-local canvas

The main canvas only sizes itself to the current viewport, while a spacer tracks total world dimensions. This keeps actual draw cost tied to what the user can see rather than full map size.

Tile surface cache

tileSurfaceCanvas stores the current visible tile result. Painting can patch single cells instead of forcing a full layer redraw every time.

Frame preview cache

framePreviewCanvas is a cached snapshot of the viewport used during pan, wheel scroll, and zoom-preview motion so interaction stays smooth before the sharp redraw completes.

Warm minimap surface

minimapSurfaceCanvas is maintained continuously in the background. Opening the drawer reveals current state instantly, and tile edits patch the minimap instead of waking it from scratch.

The rendering pipeline is intentionally doing the least honest work possible per frame: draw only the current viewport, reuse cached surfaces while moving, patch individual cells when feasible, and reserve full refreshes for bigger invalidations.

State and History Model

Captured state

History snapshots include map dimensions, map name, background color, background tile id, room layers, tile instances, NPC overlays, and editor UI folder layout state.

Branch behavior

If you undo and then perform a new edit, all future states beyond the current point are discarded. The new action becomes the forward branch.

Per-map storage

History persistence is scoped to the active map through a map-specific localStorage key, so switching maps does not smear history across rooms.

Save awareness

The toolbar compares the current history id to the last saved history id. That is what drives dirty-state messaging and save enablement.

Data Models That Matter

Templates vs instances

Templates are reusable creation sources. Selecting a template is a stamp tool. Once an instance is created, it copies the template values it needs and becomes a separate record. This is deliberate so authored rooms do not rewire themselves unexpectedly when a template changes.

Tile identity

Tile placement is stored by tile id, not just visible symbol. Symbol compatibility still exists, but the authoritative authored resource is the tile record id.

Background compression

If the map uses a background tile id and the background layer is fully implicit, the save pipeline can store empty background rows and reconstruct them from map metadata.

Folder persistence

Panel folder layouts are saved under editorUi.panelLayouts. They affect selector presentation only and do not change runtime gameplay data.

Import Pipeline

Experimental imports accept either a single compatible record or a full gallery payload from another build of this editor. The import controller normalizes width, height, pixelScale, and row data before signature comparison.

Step What happens
NormalizeRows are padded, width/height are inferred or clamped, and records with no valid pixel content are rejected.
SignatureThe editor builds a deterministic signature from width, height, pixelScale, and serialized rows.
DeduplicateExisting signatures and same-batch signatures are skipped.
Generate idsNew sprite ids or tile ids are generated. Imported tiles also receive the next free tile symbol.
PersistThe updated sprites or tiles payload is posted to the same content API used elsewhere in the app.

API and Communication Flow

The map editor is a cross-window system. The host editor launches it, the popup owns editing, the API persists data, and postMessage closes the loop for save and map-switch events.

Main editor shell MapEditorPanels.tsx Bootstrap handoff token + opener registry + sessionStorage fallback Popup startup main.ts -> runtime.ts Local edit loop tiles, instances, folders, history, minimap render + interaction + sidebar controllers Save pipeline saveCurrentState() POST maps, then POST npcs Express API /api/content/maps /api/content/npcs /api/editor-settings On-disk content content/maps.json content/maps/<mapId>/... content/npcs.json postMessage back to host refresh maps + npc data

Endpoints and Messages

Important GET endpoints

EndpointUsed for
/api/content/mapsHost editor loads and refreshes map records.
/api/content/npcsHost editor loads NPC instances and templates needed by the popup.
/api/content/tilesTile resource catalog.
/api/content/spritesSprite resource catalog used for previews and overlays.
/api/editor-settingsTheme preset load for the popup editor.
/api/imagesUI image slug catalog for small editor icons.

Important POST endpoints

EndpointUsed for
/api/content/mapsPersist the rebuilt map payload and per-map storage files.
/api/content/npcsPersist map-local NPC instances and compatibility data.
/api/content/tilesPersist imported or edited tile resources.
/api/content/spritesPersist imported sprite resources.
/api/editor-settingsPersist selected editor theme preset.
Message Sender Receiver Effect
map-editor-savedPopupMain editorTriggers a quiet refresh of map, NPC, and template background data after save.
map-editor-open-mapPopupMain editorRequests that the host reload and reopen a different map record.

Storage Layout

content/
  maps.json
  npcs.json
  npc_templates.json
  sprites.json
  tiles.json
  maps/
    <mapId>/
      tiles.json
      layer_0.json
      layer_1.json
      ...
      instances.json

Why split map files exist

The split per-map layout keeps large rooms scalable, makes layer files addressable on their own, and sets up cleaner future systems for chunking, streaming, and non-tile authoring data.

Compatibility mirrors

The server still composes payloads into the shapes older editor flows expect. That lets the storage model evolve without forcing every existing authoring surface to change at once.

If save looks successful in the UI but files do not change where you expect, check the actual content root on the server. This project supports a writable local content folder beside server.js and can also be overridden by environment configuration.

Troubleshooting

Save appears to work but data is missing after reload
  • Check the actual content root being written by the server, not just the route that served the page.
  • Confirm the host page and the popup are pointing at the same API base.
  • Inspect /api/content/maps and /api/content/npcs responses if needed.
Popup opens but content or previews look wrong
  • Verify sprite ids and tile ids exist in their catalogs.
  • Check case-sensitive image paths on Linux or VPS deployments.
  • Make sure the popup received a valid bootstrap token and did not fall back to an empty opener state.
Background fill is unavailable
  • Roomwide fill belongs to the Background layer only.
  • If you are on a non-background layer, the context action is intentionally disabled.
Large rooms feel sluggish
  • The editor already uses viewport-local rendering, surface caching, and preview frames.
  • Very large rooms still cost more when many distinct tiles are visible at once.
  • Best future wins are chunk-aware tile surfaces, coarser minimap sampling on giant maps, and brush batching tuned for extremely dense edits.
History feels odd after undo

This is usually intentional. If you undo and then make a different edit, the old future branch is removed. The history system is branch-truncating by design.

Selector folders are not affecting runtime data

Correct. Folder layout lives in editor UI state so authors can organize selectors without mutating gameplay data contracts.

Future - What Pushes This Project To The Moon

Authoring power

  • Brush presets and saved tool loadouts.
  • Tile stamp prefabs and multi-tile pattern brushes.
  • Selection transform tools for copy, move, rotate, mirror, and flood replace.
  • Real trigger, path, and transition placement layers with visual handles on the canvas.

Runtime confidence

  • Live runtime preview mode that uses the same asset and draw rules as the game.
  • Validation overlays for missing sprite ids, orphaned references, and impossible layer combinations.
  • One-click audit reports for map-local dependencies.

Performance ceiling

  • Chunked tile surfaces instead of a single viewport tile cache.
  • Background surface baking with selective dirty regions.
  • Multi-resolution minimap sampling for giant rooms.
  • Optional worker-backed serialization and save prep for very heavy rooms.

World scale

  • Map-to-map travel graph editing.
  • Biome and region metadata surfaces.
  • Cross-map search for instances, tile ids, and scripted references.
  • World atlas view that treats maps as navigable nodes instead of isolated records.

Editor maturity

  • Graduating the popup into the primary map platform while the older shell becomes a host and archive tool.
  • Shared command palette, unified modal system, and global settings panel.
  • More visual inline documentation surfaced directly inside the editor where decisions happen.

Moonshot ideas

  • Collaborative sessions with author locks and merge-safe map diffs.
  • Rule-driven procedural placement helpers that still keep authored intent visible.
  • Playback scrubbing for event layers once triggers and transitions go live.
The biggest strategic move is still the same one the editor has already started: treat the map editor like the main product. It already has the strongest identity, the richest interaction model, and the clearest path to becoming the center of world authoring.