Skip to content

feat(editor): Stage 4 - palette, object/NPC browsers, terrain, and placement - #109

Merged
leocagli merged 3 commits into
Bitcoindefi:mainfrom
Shadow-MMN:feat/editor-stage4-palette-browser
Aug 27, 2026
Merged

feat(editor): Stage 4 - palette, object/NPC browsers, terrain, and placement#109
leocagli merged 3 commits into
Bitcoindefi:mainfrom
Shadow-MMN:feat/editor-stage4-palette-browser

Conversation

@Shadow-MMN

Copy link
Copy Markdown

Closes #29

Implements the visual editor content browser: object catalog, NPC catalog, terrain palette with upload, and entity placement on the map.

What's included

API (api/)

  • New game_map_tile_entities table for objects/NPCs placed on tiles (draft/published lifecycle, same model as tile overrides)
  • Admin endpoints: PUT/DELETE /admin/game-data/maps/:n/entities, GET /admin/game-data/maps/:n/terrain, GET /admin/game-data/maps/:n/overrides (with drafts)
  • ?all=true query param on objects/NPCs for full catalog in one page (1,062 objects, 340 NPCs)
  • grhIndex added to object summaries
  • publish/discard/revert/status extended to include entities
  • Restored seed files (api/src/jsons/objs.json, api/src/jsons/npcs.json) — without them internal endpoints return 500
  • 5 integration tests in world-builder.integration.test.ts

Frontend (frontend/)

  • /construccion page with tabbed sidebar (Terreno / Objetos / NPCs)
  • VirtualizedList: dependency-free virtualized list (no react-window) for 1,000+ items — no DOM nodes created for off-screen rows
  • GraphicPreview: resolves grhIndex → cropped sprite via shared lib/graphicTextures.ts (extracted from CharacterSpritePreview)
  • ObjectsBrowser: searchable, filterable by type (30+ categories with counts), each item shows real sprite preview + type color dot
  • NpcsBrowser: searchable, preview via existing CharacterSpritePreview (body + head)
  • TerrainPalette: tile grid from terrain.json + uploaded PNGs, with PNG upload button
  • EditorCanvas: PixiJS canvas with zoom (scroll wheel, cursor-anchored), pan (Shift+click / middle-click), grid overlay, blocked-tile overlay, hover highlight, entity placement with bottom-anchor positioning, batch paint (up to 500 tiles per request)
  • EditorToolbar: map selector, tool buttons, publish/discard/revert actions, live status counters
  • RecentsStrip: recently used items persisted in localStorage, click to re-select
  • editorStore: React context providing all shared editor state
  • editorApi: typed API client via /api/editor/[...path] server-side proxy
  • data/objectTypes.ts: original type catalog with labels + colors (not copied from any reference repo)
  • /api/editor/[...path] proxy route: forwards to API with session cookie + admin proxy token (token never reaches the browser)

Documentation

  • docs/licensing-notes.md: AO-object-editor used as conceptual reference only; all code is original (no LICENSE declared in reference repo)
  • docs/modo-construccion-guide.md: step-by-step manual test guide

Acceptance criteria

  • Objects can be searched by name and filtered by type
  • Preview shows the real cropped graphic, not a placeholder
  • An object can be selected and placed on the map
  • Terrain palette includes user-uploaded graphics
  • Object list stays responsive with all 1,062 objects (virtualized — zero DOM for off-screen rows)
  • Licensing situation re: AO-object-editor documented in docs/licensing-notes.md

How to test

  1. Start API with GAME_DATA_ADMIN_* env vars, frontend with API_BASE_URL pointing to it
  2. Login as admin, navigate to /construccion
  3. Paint terrain, place an object and an NPC, publish
  4. Verify an anonymous player sees the published changes

Notes

Comment thread frontend/components/editor/EditorCanvas.tsx Outdated
Comment thread frontend/components/editor/EditorCanvas.tsx Outdated
…acement

Implements the visual editor content browser (issue Bitcoindefi#29):

API (api/):
- New game_map_tile_entities table for objects/NPCs placed on tiles
  (draft/published lifecycle, same model as tile overrides)
- Admin endpoints: PUT/DELETE entities, GET terrain palette, admin overrides
- all=true query param on objects/NPCs for full catalog in one page
- grhIndex added to object summaries
- Publish/discard/revert/status extended to include entities
- Restored seed files (objs.json, npcs.json) for internal endpoints
- 5 integration tests for admin endpoint access and entity data

Frontend (frontend/):
- /construccion page with tabbed sidebar (Terrain / Objects / NPCs)
- VirtualizedList: dependency-free virtualized list for 1000+ items
- GraphicPreview: resolves grhIndex to cropped sprite via shared
  lib/graphicTextures.ts (extracted from CharacterSpritePreview)
- ObjectsBrowser: searchable, filterable by type, with graphic preview
- NpcsBrowser: searchable, NPC sprite preview via CharacterSpritePreview
- TerrainPalette: tile palette from terrain.json + uploaded PNGs, upload
- EditorCanvas: PixiJS pan/zoom/grid, places objects/NPCs via entity API
- EditorToolbar: tool selection, publish/discard/revert actions, status
- RecentsStrip: recently used items persisted in localStorage
- editorStore: React context for shared editor state
- editorApi: typed API client via /api/editor proxy
- data/objectTypes: original type catalog (not copied from reference)
- /api/editor/[...path] proxy route (server-side auth + admin token)

Documentation:
- docs/licensing-notes.md: AO-object-editor used as reference only
- docs/modo-construccion-guide.md: manual test guide

No new dependencies added. Uses existing pixi.js + react + next stack.
* Use shared 2D canvas previews and virtualized lists to improve performance.
* Fix terrain brushes to paint full tiles and correctly handle empty layers.
* Optimize tile rendering with caching and selective updates.
* Fix deletion, drawing order, map switching, and pointer handling.
* Add admin session permission checks and confirmation dialogs.
* Add tests for terrain palette data, session permissions, and catalog flags.
Comment thread frontend/components/editor/NpcsBrowser.tsx Outdated
Comment thread api/src/repositories/gameNpcs.ts Outdated
* Treat invalid catalog flags as `false` instead of returning 500 errors.
* Wait for catalogs before saving NPC recents to prevent empty thumbnails.
* Avoid saving invalid graphics when catalog loading fails.
* Add regression test for invalid catalog flags.
@gitar-bot

gitar-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 4 resolved / 4 findings

Implements the visual editor Stage 4 content browser with object/NPC catalogs, terrain palette, and map placement, addressing batch paint limits, flush race conditions, and NPC recent persistence issues.

✅ 4 resolved
Bug: Batch paint can exceed server's 500-tile cap and silently fail

📄 frontend/components/editor/EditorCanvas.tsx:551-565 📄 frontend/components/editor/EditorCanvas.tsx:611-625 📄 api/src/repositories/worldBuilder.ts:206-208
The terrain paint tool accumulates every dragged tile into pendingPaintRef.tiles with a 400ms debounce that resets on each move, so a long drag can gather far more than 500 unique tiles before flushing (maps are 100x100). flushPendingPaint then sends the whole set to paintTiles, but paintTilesSchema enforces .max(500), so the API responds 400 and the error is only console.error'd — the user's paint is silently dropped with no feedback. Chunk the tiles into batches of ≤500 (or flush eagerly once the pending set reaches 500) and surface failures in the UI.

Bug: Pending paint tiles can be lost if flush races with an in-flight apply

📄 frontend/components/editor/EditorCanvas.tsx:551-565
When the debounce timer fires (or pointerup flushes) while applyingRef.current is already true, flushPendingPaint returns early at line 561 before clearing pending.tiles, but it does not reschedule a flush. Those accumulated tiles then stay unsent until the user happens to paint again, so the last strokes of a fast drag may never reach the server. Reschedule a flush (e.g. setTimeout(flushPendingPaint, 400)) when bailing out because an apply is in progress.

Edge Case: NPC recents can persist grhIndex 0 if catalogs not loaded

📄 frontend/components/editor/NpcsBrowser.tsx:66-79
In NpcsBrowser.handleSelect, the recent entry's grhIndex is computed with resolveCharacterThumbnailGrh(bodiesDB, headsDB, ...). If a user clicks an NPC before getSharedBodiesDB()/getSharedHeadsDB() resolve, bodiesDB/headsDB are still null and the function returns 0, which gets written to localStorage recents permanently — leaving an empty thumbnail in RecentsStrip even after the catalogs load. Consider guarding the addRecent call until both DBs are loaded, or recomputing the recent's grhIndex when catalogs arrive.

Edge Case: Malformed catalog query flag throws instead of defaulting

📄 api/src/repositories/gameNpcs.ts:98-112
queryFlagSchema in gameNpcs.ts returns the raw value unchanged when it matches none of the true/false forms (e.g. ?hostileOnly=yes or ?all=maybe), which then fails z.boolean() and makes listFiltersSchema.parse throw. Unlike gameObjects.ts's all preprocess (which coerces anything unrecognized to false), this surfaces as an unhandled parse error rather than a benign default. Consider having the fallthrough return false (or undefined) so unexpected query strings degrade gracefully.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@Shadow-MMN

Copy link
Copy Markdown
Author

@leocagli ready for review

@leocagli
leocagli merged commit d4d845b into Bitcoindefi:main Aug 27, 2026
6 checks passed
@leocagli

Copy link
Copy Markdown
Collaborator

Mergeada. Y de paso arregló algo bastante más grande de lo que dice el título, así
que lo dejo escrito acá.

main estaba roto desde el primer día

El workflow de CI se agregó el 18/08 y nunca corrió en verde. Las tres
corridas sobre main fallaron, siempre en el mismo job:

API (typecheck, test, build)    failure
Test Files  2 failed | 8 passed (10)

Las dos causas eran los dos archivos que vos restauraste:

  1. api/src/jsons/npcs.json faltaba, y ensureSeeded() lo lee sin guarda, así que
    market.integration.test.ts moría con ENOENT en gameData.ts:259.
  2. api/src/jsons/objs.json faltaba, y por eso GET /wiki devolvía 500. Eso hacía
    caer platform.integration.test.ts:27 con 500 !== 200, que desde afuera no
    parecía tener nada que ver con lo anterior.

Lo verifiqué levantando el job de API en local, con el mismo postgres:18-alpine
que usa el CI, primero sin los archivos y después con ellos:

sin npcs.json ni objs.json     Test Files  2 failed | 8 passed (10)
solo con npcs.json             Test Files  1 failed | 9 passed (10)   <- el wiki seguia en 500
con los dos (esta PR)          Test Files  11 passed (11)             GET /wiki -> 200

Y después en el CI de verdad, los cuatro jobs más el Secret Scan en verde. Es la
primera vez que main está entero.

Tu diagnóstico en la descripción era el correcto y estaba escrito antes que el mío:

Restored seed files (api/src/jsons/objs.json, api/src/jsons/npcs.json)
without them internal endpoints [fail]

Lo que revisé antes de mergear

Son 18.623 líneas, así que no alcanzaba con que el CI estuviera verde:

  • Las 27 rutas /admin tienen guarda. Las dos que agregás también:
    /admin/game-data/session y /internal/game-data/objects, esta última de sólo
    lectura y detrás de requireAuth.
  • objs.json no trae datos nuevos al repositorio. Es byte a byte el mismo
    archivo que ya estaba en frontend/public/init/objs.json, mismo md5
    (1c3bf35e…), 983 entradas. Coincide con lo que decís en licensing-notes.md.
  • Server y frontend: install, lint, typecheck y build, todo en cero.

Gracias por escribir las notas de licencia sin que nadie te las pidiera. Ahorra
exactamente la discusión que hay que tener antes y no después.

Una cosa que no era culpa de nadie

Las PRs que vienen de un fork quedan en action_required y el workflow no corre
hasta que alguien lo aprueba. Nadie lo estaba aprobando, así que los 146 PRs
abiertos mostraban UNSTABLE sin que eso quisiera decir nada. Los tuyos estaban
esperando desde el 19/08. Ya aprobé el de esta y voy a ir destrabando el resto.

@Shadow-MMN

Copy link
Copy Markdown
Author

Mergeada. Y de paso arregló algo bastante más grande de lo que dice el título, así que lo dejo escrito acá.

main estaba roto desde el primer día

El workflow de CI se agregó el 18/08 y nunca corrió en verde. Las tres corridas sobre main fallaron, siempre en el mismo job:

API (typecheck, test, build)    failure
Test Files  2 failed | 8 passed (10)

Las dos causas eran los dos archivos que vos restauraste:

  1. api/src/jsons/npcs.json faltaba, y ensureSeeded() lo lee sin guarda, así que
    market.integration.test.ts moría con ENOENT en gameData.ts:259.
  2. api/src/jsons/objs.json faltaba, y por eso GET /wiki devolvía 500. Eso hacía
    caer platform.integration.test.ts:27 con 500 !== 200, que desde afuera no
    parecía tener nada que ver con lo anterior.

Lo verifiqué levantando el job de API en local, con el mismo postgres:18-alpine que usa el CI, primero sin los archivos y después con ellos:

sin npcs.json ni objs.json     Test Files  2 failed | 8 passed (10)
solo con npcs.json             Test Files  1 failed | 9 passed (10)   <- el wiki seguia en 500
con los dos (esta PR)          Test Files  11 passed (11)             GET /wiki -> 200

Y después en el CI de verdad, los cuatro jobs más el Secret Scan en verde. Es la primera vez que main está entero.

Tu diagnóstico en la descripción era el correcto y estaba escrito antes que el mío:

Restored seed files (api/src/jsons/objs.json, api/src/jsons/npcs.json)
without them internal endpoints [fail]

Lo que revisé antes de mergear

Son 18.623 líneas, así que no alcanzaba con que el CI estuviera verde:

  • Las 27 rutas /admin tienen guarda. Las dos que agregás también:
    /admin/game-data/session y /internal/game-data/objects, esta última de sólo
    lectura y detrás de requireAuth.
  • objs.json no trae datos nuevos al repositorio. Es byte a byte el mismo
    archivo que ya estaba en frontend/public/init/objs.json, mismo md5
    (1c3bf35e…), 983 entradas. Coincide con lo que decís en licensing-notes.md.
  • Server y frontend: install, lint, typecheck y build, todo en cero.

Gracias por escribir las notas de licencia sin que nadie te las pidiera. Ahorra exactamente la discusión que hay que tener antes y no después.

Una cosa que no era culpa de nadie

Las PRs que vienen de un fork quedan en action_required y el workflow no corre hasta que alguien lo aprueba. Nadie lo estaba aprobando, así que los 146 PRs abiertos mostraban UNSTABLE sin que eso quisiera decir nada. Los tuyos estaban esperando desde el 19/08. Ya aprobé el de esta y voy a ir destrabando el resto.

Thank you very much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Etapa 4: paleta y navegador de objetos, terreno y NPCs para el editor

2 participants