Map Table — power-user segment editor
Related: 21 — World content schemas, 23 — Harvest & resources, 37 — Economic map zones, 38 — Biomes, content-admin World → Map layout.
1. Problem
The canvas Map layout tab is the right tool for spatial work (paint terrain, place pins, drag poses). As segments grow (hundreds–thousands of resource nodes, wildlife anchors, NPC placements, zone rects), editing one entity at a time in the side panel does not scale.
Designers need a spreadsheet-style view over the same MapLayout draft: filter, multi-select, inline column edits, and bulk apply — without leaving the Map tab’s save/publish/validate flow.
2. Goals
| Goal | Meaning |
|---|---|
| Same data, same draft | Table reads/writes draftLayout in mountMap — not a second API or Monaco escape hatch |
| Category-first | One entity family per table (resources, wildlife, …) with columns tuned to that family |
| Power-user speed | Keyboard navigation, multi-select, bulk toolbar, virtualized rows for large segments |
| Labels over IDs | Primary columns show label / catalog display_name; id is secondary or pinned column |
| Validation parity | Inline hints for rules the sim already enforces (e.g. loot_table requires harvest_node template) before Publish |
| Canvas ↔ table sync | Select rows → highlight/focus on map; map selection → scroll table to row |
Non-goals (v1)
- Replacing the canvas or Monaco segment tab
- Real-time multiplayer editing
- Server-side query API (client filters in-memory layout)
- Interior blueprint table (follow-up; reuse same grid component)
3. UX placement
Add a view mode toggle on the Map tab toolbar (next to Select / Paint):
[ Canvas ] [ Table ]
- Canvas — current behavior (unchanged default).
- Table — full-width grid below toolbar; canvas hidden or minimized to a preview strip (optional phase 2: split pane 30% map / 70% table).
Persist last mode in map_prefs (viewMode: "canvas" | "table").
Within Table view, category tabs (horizontal):
| Tab | MapLayout source |
Priority |
|---|---|---|
| Resources | resource_nodes[] |
P0 — largest pain |
| Wildlife | wildlife_spawns[] |
P1 |
| Interactables | interactables[] |
P1 |
| Map NPCs | npc_placements[] |
P1 |
| Spawns | spawn_points[] |
P2 |
| Buildings | structures[] |
P2 |
| Doors | derived from structures[].doors (flat rows) |
P2 |
| Terrain zones | terrain_zones[] |
P3 |
| Tax / Property / Growth / Biome | respective *_zones[] |
P3 |
Row count badge on each tab (e.g. Resources (1,247)).
4. Shared table shell
New modules (keep map.ts from growing further):
| Module | Responsibility |
|---|---|
web/src/map_table/types.ts |
Row ids, column defs, category enum |
web/src/map_table/rows.ts |
Pure functions: layout → row DTOs, apply patch → layout |
web/src/map_table/validate_row.ts |
Client-side checks mirroring flatland_sim content validation for editable fields |
web/src/map_table/MapTableView.tsx |
React island: toolbar, tabs, grid, bulk bar |
web/src/map_table/columns/*.tsx |
Per-category column sets |
web/src/map_table/filters.ts |
Search + structured filters |
Mount via existing island pattern (mountMap calls mountMapTable when view is Table), same as mountMapNpcAttachments.
4.1 Grid behavior
- Virtualized body (
@tanstack/react-virtualor lightweight custom) — required forstarter-plainsscale. - Sticky header + optional sticky first columns (
☑,id). - Row select: click row, Shift+click range, ⌘/Ctrl+click toggle; header checkbox = all filtered rows.
- Inline edit: click cell → control (input, select, checkbox). Commit on blur / Enter; Escape reverts.
- Tab / Shift+Tab moves across editable cells in row order (spreadsheet habit).
- Dirty: any cell commit calls existing
markDirty()+onDirty().
4.2 Toolbar (per category)
| Control | Behavior |
|---|---|
| Search | Case-insensitive match on id, label, template/npc_ref, coords string |
| Filters | Dropdowns: item template, loot table, blocking, locked, “validation errors only” |
| Spatial (resources) | “In growth zone …”, “In tax zone …” using existing zoneAtCell / econ helpers |
| Columns | Show/hide optional columns (pose, crop_tags, tile_id) — persist in map_prefs |
| Bulk actions | Apply field to selection (see §5) |
| Export CSV | Current filter + visible columns |
| Import CSV | Merge by id (update) or append new rows with generated ids — P2 |
| Jump to map | Focus canvas selection on first selected row |
4.3 Bulk edit bar
When ≥1 row selected, show a compact bar:
N selected · [Field ▾] [Value …] [Apply] · Delete · Duplicate · Lock · Unlock
Apply writes the same patch to every selected row (with undo snapshot — see §7).
Fields bulk-editable should match what the side panel already supports per kind (reuse bindEntityPanel field names where possible).
5. Column sets (most relevant first)
5.1 Resources (ResourceNodeDef)
| Column | Edit | Notes |
|---|---|---|
| ☑ | — | Selection |
| Label | text | Primary display |
| Template | itemTemplateSelect + isWorldResourceNodeTemplate |
Show catalog display_name |
| X, Y, Z | number (0.5 step) | Bulk nudge ±1m actions |
| Loot table | catalog select | Required — rolls harvest drops |
| Qty min / max | number | Hidden when loot table set (or show muted) |
| Harvest / respawn ticks | number | |
| Blocking | checkbox | |
| Block radius | number | |
| Draw scale | number | Optional column |
| Yaw ° | number | Optional; store radians in layout |
| Crop tags | chip editor | Reuse multiChipField patterns |
| Lock | toggle | editor_locks via lockKey |
| Id | text | Secondary; warn on rename (migrate lock key) |
| ⚠ | icon | Row validation tooltip |
Row validation (client):
- Unknown
item_template/loot_table loot_tableset but template notharvest_node(same message as sim)- Missing or unknown
loot_table - Duplicate cell occupancy (two resources same rounded cell) — warning not hard fail
5.2 Wildlife (WildlifeSpawnDef)
Label (id), NPC ref (combo + display name), X/Y, radius, count, boundary radius, movement mode, respawn overrides.
5.3 Interactables
Kind, label, board_id (quest boards), X/Y/Z.
5.4 Map NPC placements
npc_ref, schedule ref if present, exterior coords, building link fields from NpcPlacement.
5.5 Spawns
label, X/Y/Z (index-based id in table: spawn#3).
5.6 Buildings / doors
Buildings: label, x/y, width/depth, tags, storage fields.
Doors: flat table with building_id, door id, offset coords, link to building row.
5.7 Zones (terrain + overlays)
id, label/kind, z_order, rect count, first rect x0..y1, tile/kind/econ fields — bulk kind/tile already exists for terrain multi-select on canvas; reuse renderBulkTerrainPanel logic in table bulk apply.
6. Integration with existing Map tab
- Selection model: extend or mirror
selections: SelectionItem[]so table row click callssetSelMultiwith{ kind: "resource", id }. - Undo: push JSON snapshot to
undoStackon bulk apply (same as canvas mutations). - New rows: “+ Add row” duplicates defaults from map paint (
defaultLootTableForResourceTemplate, template frompaintResourceTemplatepref). - Delete: selected rows → same paths as Del key on canvas.
7. Implementation phases
Phase 0 — scaffolding (1 PR)
- View toggle Canvas / Table + prefs
- Empty table shell with Resources tab only, read-only columns, virtualization, search on label/id
- Selection sync to map (select row → set selection; no inline edit yet)
Phase 1 — Resources power edit (1–2 PRs) MVP
- Inline edit all P0 resource columns
- Multi-select + bulk apply (template, loot_table, ticks, blocking, delete)
- Client validation column
npm run buildin content-admin/web
Phase 2 — scale & export
- CSV export/import for resources
- Spatial filters (growth/tax zone)
- Column show/hide persistence
- Performance budget: 5k rows scroll at 60fps on M-series laptop
Phase 3 — other categories
- Wildlife, interactables, map NPCs, spawns (same grid component, new column defs)
- Tab badges + category-specific bulk actions
Phase 4 — structures & zones
- Buildings, doors, terrain/econ/biome zone tables
- Reuse bulk terrain kind/tile from
zone-bulkform handler
Phase 5 — polish
- Split pane map+table
flatland-admin map table exportmirroring CSV (optional CLI)- Short designer doc in
tools/content-admin/README.md+docs/link
8. Technical choices
| Topic | Recommendation |
|---|---|
| UI framework | React island inside Map tab (consistent with Items, Sprites, DataRowTable) |
| Grid | TanStack Table + Virtual or lightweight custom <table> with virtual rows — avoid heavy AG Grid dependency unless needed |
| Styling | Extend styles.css .data-table / .table-wrap; sticky header patterns from perf-table |
| Catalog | Existing api.catalog() — template labels, loot ids, npc ids |
| No new REST | Table is a view over in-memory layout; save path unchanged |
| Interiors | Later: InteriorBlueprint.resource_nodes / wildlife using same column defs with room_id column |
9. Content-admin checklist (definition of done)
10. Risks
| Risk | Mitigation |
|---|---|
map.ts size |
Table logic only in map_table/*; thin mount hook |
| Undo complexity for bulk | Single undo entry per bulk apply |
| ID renames break quests/routes | Show confirm + grep hint; prefer label edits |
| CSV import corrupts layout | Dry-run preview + validate before merge |
| Locked rows | Bulk skip locked unless “include locked” override |
11. Open questions (defaults if unanswered)
- Split pane vs full table first? → Full table first (Phase 0–1), split pane Phase 5.
- Edit segment Monaco YAML in parallel? → Allowed but Table does not auto-reload until Cancel/segment change; document refresh behavior.
- Interior placements? → Phase 5+; same component, different source array.