From 3ebb247a8138b2608bf291997562f2854929894b Mon Sep 17 00:00:00 2001 From: Pablo De Lucca <104434465+pablodelucca@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:37:31 +0000 Subject: [PATCH 1/6] feat: migrate to open-source assets with modular manifest-based loading (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: migrate to open-source assets with modular manifest-based loading The primary objective of this change is to replace all previously privately-licensed assets with fully open-source ones, make the asset importing process easier, and make the entire asset system more modular and extensible. ## Asset system overhaul - Replace monolithic `furniture-catalog.json` with per-folder `manifest.json` files (one per furniture item in `assets/furniture/`) - Each manifest declares its own rotation groups, state groups, and animation groups via a recursive tree structure that gets flattened at load time, eliminating the need for a separate centralized catalog - Add support for new manifest concepts: `mirrorSide` (auto-generates mirrored "left" variant from a "side" sprite), `rotationScheme` (2-way vs 4-way rotation), `animationGroup` with frame indexing (multi-frame animated sprites for on-state electronics) - Split `floors.png` strip into individual `assets/floors/floor_N.png` files and `walls.png` into `assets/walls/wall_N.png` files, making it trivial to add/remove/reorder tile patterns without regenerating combined spritesheets - Add wall set picker in the editor toolbar so users can choose between multiple wall tile styles - Lower `PNG_ALPHA_THRESHOLD` from 128 to 2 and preserve semi-transparent alpha in sprite data (`#RRGGBBAA` format) for higher-fidelity assets - Add `VOID` tile type as 255 (was 8) with migration for old layouts - Add 2 new floor tile slots (FLOOR_8, FLOOR_9) for future patterns ## Open-source asset bundle - Include 25+ original furniture pieces as individual PNGs with manifests: desks, chairs (wooden, cushioned), sofas, tables, PCs with 3-frame screen animation, bookshelves, plants, paintings, whiteboard, clock, coffee, bins, and more - Include 9 floor tile patterns and 1 wall tile set - Add new default layout (`default-layout-1.json`) using only the new open-source assets - Remove `.gitignore` rules that excluded asset files from version control — all assets are now open-source and tracked ## Layout migration & versioning - Add `layoutRevision` field to layouts; bundled defaults are versioned (e.g. `default-layout-1.json` has revision 1) - On startup, if the user's saved layout has an older revision than the bundled default, it is automatically reset to the new default - Show a one-time migration notice modal explaining the layout reset and the move to open-source assets - Add `LEGACY_TYPE_MAP` to migrate old furniture type strings (`desk`, `chair`, `bookshelf`, etc.) to new manifest IDs (`DESK_FRONT`, `WOODEN_CHAIR_FRONT`, `BOOKSHELF`, etc.) - Migrate old `VOID=8` tiles to `VOID=255` - "Export Default Layout" command now auto-increments revision numbers ## Code cleanup - Remove ~1000 lines of hardcoded pixel-art sprite data from `spriteData.ts` (DESK_SQUARE_SPRITE, PLANT_SPRITE, etc.) — all sprites now loaded from PNGs at runtime - Remove `FurnitureType` enum and `FURNITURE_CATALOG` constant — the catalog is now fully dynamic from manifests - Remove old 7-stage asset pipeline scripts (`0-import-tileset.ts` through `5-export-assets.ts`, `export-characters.ts`, `generate-walls.js`) and their working data - Consolidate asset-manager.html with expanded functionality - Simplify `createDefaultLayout()` to a minimal fallback (no furniture) since the real default comes from `default-layout.json` - Move font file from `src/fonts/` to `public/fonts/` and update CSS to use absolute path ## Renderer updates - Add horizontal flip support for mirrored furniture instances (`FurnitureInstance.mirrored` flag, canvas `scale(-1, 1)`) - Add furniture animation timer in `OfficeState` that cycles through animation group frames at 0.2s intervals for active electronics - Colorize module now preserves alpha channel through both Colorize and Adjust modes - Wall tile rendering supports multiple wall sets with `setIndex` ## README - Update Office Assets section to reflect that all assets are now fully open-source and included in the repository - Document the modular manifest-based asset structure Co-Authored-By: Claude Opus 4.6 * fix: address PR #117 review findings - Fix migration modal dismiss bug (derived state pattern) - Guard VOID migration to preserve FLOOR_8 tiles on new layouts - Extract bubble sprites to JSON files for readability - Reformat default-layout-1.json for readability * fix: fix overlay z-ordering, hide UI in debug mode, and update assets - Lower ToolOverlay z-index below settings modal so status labels don't cover the modal - Hide ToolOverlay and ZoomControls in debug mode - Position rotate hint below EditActionBar instead of shifting sideways - Remove NEW_ASSET template, add LARGE_PLANT asset Co-Authored-By: Claude Opus 4.6 * fix: tweak migration modal copy ("meant" → "means") Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Florin Timbuc Co-authored-by: Pablo De Lucca --- .gitignore | 25 +- .prettierignore | 2 +- .vscode/settings.json | 3 +- CLAUDE.md | 4 +- README.md | 14 +- package-lock.json | 4 +- package.json | 2 +- .../.tileset-working/asset-editor-output.json | 3701 ------------- .../tileset-detection-output.json | 1686 ------ .../tileset-metadata-draft.json | 4164 --------------- .../tileset-metadata-final.json | 4622 ----------------- scripts/0-import-tileset.ts | 425 -- scripts/1-detect-assets.ts | 254 - scripts/2-asset-editor.html | 921 ---- scripts/3-vision-inspect.ts | 280 - scripts/4-review-metadata.html | 509 -- scripts/5-export-assets.ts | 275 - scripts/asset-manager.html | 2897 +++++++++-- scripts/export-characters.ts | 120 - scripts/generate-walls.js | 253 - src/PixelAgentsViewProvider.ts | 35 +- src/agentManager.ts | 5 +- src/assetLoader.ts | 509 +- src/constants.ts | 4 +- src/layoutPersistence.ts | 28 +- webview-ui/.gitignore | 6 - webview-ui/public/Screenshot_v1.1.jpg | Bin 0 -> 60810 bytes .../public/assets/default-layout-1.json | 92 + webview-ui/public/assets/default-layout.json | 2802 ---------- webview-ui/public/assets/floors/floor_0.png | Bin 0 -> 1719 bytes webview-ui/public/assets/floors/floor_1.png | Bin 0 -> 3590 bytes webview-ui/public/assets/floors/floor_2.png | Bin 0 -> 3590 bytes webview-ui/public/assets/floors/floor_3.png | Bin 0 -> 3594 bytes webview-ui/public/assets/floors/floor_4.png | Bin 0 -> 3594 bytes webview-ui/public/assets/floors/floor_5.png | Bin 0 -> 3597 bytes webview-ui/public/assets/floors/floor_6.png | Bin 0 -> 3597 bytes webview-ui/public/assets/floors/floor_7.png | Bin 0 -> 3596 bytes webview-ui/public/assets/floors/floor_8.png | Bin 0 -> 3592 bytes .../public/assets/furniture/BIN/BIN.png | Bin 0 -> 252 bytes .../public/assets/furniture/BIN/manifest.json | 13 + .../assets/furniture/BOOKSHELF/BOOKSHELF.png | Bin 0 -> 388 bytes .../assets/furniture/BOOKSHELF/manifest.json | 13 + .../public/assets/furniture/CACTUS/CACTUS.png | Bin 0 -> 558 bytes .../assets/furniture/CACTUS/manifest.json | 13 + .../public/assets/furniture/CLOCK/CLOCK.png | Bin 0 -> 304 bytes .../assets/furniture/CLOCK/manifest.json | 13 + .../public/assets/furniture/COFFEE/COFFEE.png | Bin 0 -> 223 bytes .../assets/furniture/COFFEE/manifest.json | 13 + .../furniture/COFFEE_TABLE/COFFEE_TABLE.png | Bin 0 -> 274 bytes .../furniture/COFFEE_TABLE/manifest.json | 13 + .../CUSHIONED_BENCH/CUSHIONED_BENCH.png | Bin 0 -> 250 bytes .../furniture/CUSHIONED_BENCH/manifest.json | 13 + .../CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png | Bin 0 -> 205 bytes .../CUSHIONED_CHAIR/CUSHIONED_CHAIR_FRONT.png | Bin 0 -> 247 bytes .../CUSHIONED_CHAIR/CUSHIONED_CHAIR_SIDE.png | Bin 0 -> 255 bytes .../furniture/CUSHIONED_CHAIR/manifest.json | 44 + .../assets/furniture/DESK/DESK_FRONT.png | Bin 0 -> 310 bytes .../assets/furniture/DESK/DESK_SIDE.png | Bin 0 -> 278 bytes .../assets/furniture/DESK/manifest.json | 33 + .../DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png | Bin 0 -> 627 bytes .../furniture/DOUBLE_BOOKSHELF/manifest.json | 13 + .../furniture/HANGING_PLANT/HANGING_PLANT.png | Bin 0 -> 693 bytes .../furniture/HANGING_PLANT/manifest.json | 13 + .../LARGE_PAINTING/LARGE_PAINTING.png | Bin 0 -> 1056 bytes .../furniture/LARGE_PAINTING/manifest.json | 13 + .../furniture/LARGE_PLANT/LARGE_PLANT.png | Bin 0 -> 1285 bytes .../furniture/LARGE_PLANT/manifest.json | 13 + .../public/assets/furniture/PC/PC_BACK.png | Bin 0 -> 349 bytes .../assets/furniture/PC/PC_FRONT_OFF.png | Bin 0 -> 427 bytes .../assets/furniture/PC/PC_FRONT_ON_1.png | Bin 0 -> 479 bytes .../assets/furniture/PC/PC_FRONT_ON_2.png | Bin 0 -> 476 bytes .../assets/furniture/PC/PC_FRONT_ON_3.png | Bin 0 -> 485 bytes .../public/assets/furniture/PC/PC_SIDE.png | Bin 0 -> 451 bytes .../public/assets/furniture/PC/manifest.json | 88 + .../public/assets/furniture/PLANT/PLANT.png | Bin 0 -> 703 bytes .../assets/furniture/PLANT/manifest.json | 13 + .../assets/furniture/PLANT_2/PLANT_2.png | Bin 0 -> 543 bytes .../assets/furniture/PLANT_2/manifest.json | 13 + .../public/assets/furniture/POT/POT.png | Bin 0 -> 288 bytes .../public/assets/furniture/POT/manifest.json | 13 + .../SMALL_PAINTING/SMALL_PAINTING.png | Bin 0 -> 473 bytes .../furniture/SMALL_PAINTING/manifest.json | 13 + .../SMALL_PAINTING_2/SMALL_PAINTING_2.png | Bin 0 -> 473 bytes .../furniture/SMALL_PAINTING_2/manifest.json | 13 + .../SMALL_TABLE/SMALL_TABLE_FRONT.png | Bin 0 -> 240 bytes .../SMALL_TABLE/SMALL_TABLE_SIDE.png | Bin 0 -> 225 bytes .../furniture/SMALL_TABLE/manifest.json | 33 + .../assets/furniture/SOFA/SOFA_BACK.png | Bin 0 -> 192 bytes .../assets/furniture/SOFA/SOFA_FRONT.png | Bin 0 -> 210 bytes .../assets/furniture/SOFA/SOFA_SIDE.png | Bin 0 -> 255 bytes .../assets/furniture/SOFA/manifest.json | 44 + .../furniture/TABLE_FRONT/TABLE_FRONT.png | Bin 0 -> 400 bytes .../furniture/TABLE_FRONT/manifest.json | 13 + .../furniture/WHITEBOARD/WHITEBOARD.png | Bin 0 -> 336 bytes .../assets/furniture/WHITEBOARD/manifest.json | 13 + .../furniture/WOODEN_BENCH/WOODEN_BENCH.png | Bin 0 -> 208 bytes .../furniture/WOODEN_BENCH/manifest.json | 13 + .../WOODEN_CHAIR/WOODEN_CHAIR_BACK.png | Bin 0 -> 290 bytes .../WOODEN_CHAIR/WOODEN_CHAIR_FRONT.png | Bin 0 -> 295 bytes .../WOODEN_CHAIR/WOODEN_CHAIR_SIDE.png | Bin 0 -> 261 bytes .../furniture/WOODEN_CHAIR/manifest.json | 44 + .../assets/{walls.png => walls/wall_0.png} | Bin .../fonts/FSPixelSansUnicode-Regular.ttf | Bin webview-ui/src/App.tsx | 101 +- webview-ui/src/constants.ts | 3 + webview-ui/src/hooks/useEditorActions.ts | 10 + webview-ui/src/hooks/useExtensionMessages.ts | 19 +- webview-ui/src/index.css | 6 +- webview-ui/src/office/colorize.ts | 19 +- .../src/office/components/OfficeCanvas.tsx | 5 + .../src/office/editor/EditorToolbar.tsx | 91 + webview-ui/src/office/editor/editorState.ts | 5 +- webview-ui/src/office/engine/officeState.ts | 24 +- webview-ui/src/office/engine/renderer.ts | 40 +- webview-ui/src/office/floorTiles.ts | 4 +- .../src/office/layout/furnitureCatalog.ts | 220 +- webview-ui/src/office/layout/index.ts | 7 +- .../src/office/layout/layoutSerializer.ts | 133 +- .../src/office/sprites/bubble-permission.json | 27 + .../src/office/sprites/bubble-waiting.json | 27 + webview-ui/src/office/sprites/index.ts | 12 +- webview-ui/src/office/sprites/spriteData.ts | 1104 +--- webview-ui/src/office/types.ts | 29 +- webview-ui/src/office/wallTiles.ts | 78 +- 124 files changed, 4335 insertions(+), 21804 deletions(-) delete mode 100644 scripts/.tileset-working/asset-editor-output.json delete mode 100644 scripts/.tileset-working/tileset-detection-output.json delete mode 100644 scripts/.tileset-working/tileset-metadata-draft.json delete mode 100644 scripts/.tileset-working/tileset-metadata-final.json delete mode 100644 scripts/0-import-tileset.ts delete mode 100644 scripts/1-detect-assets.ts delete mode 100644 scripts/2-asset-editor.html delete mode 100644 scripts/3-vision-inspect.ts delete mode 100644 scripts/4-review-metadata.html delete mode 100644 scripts/5-export-assets.ts delete mode 100644 scripts/export-characters.ts delete mode 100644 scripts/generate-walls.js create mode 100644 webview-ui/public/Screenshot_v1.1.jpg create mode 100644 webview-ui/public/assets/default-layout-1.json delete mode 100644 webview-ui/public/assets/default-layout.json create mode 100644 webview-ui/public/assets/floors/floor_0.png create mode 100644 webview-ui/public/assets/floors/floor_1.png create mode 100644 webview-ui/public/assets/floors/floor_2.png create mode 100644 webview-ui/public/assets/floors/floor_3.png create mode 100644 webview-ui/public/assets/floors/floor_4.png create mode 100644 webview-ui/public/assets/floors/floor_5.png create mode 100644 webview-ui/public/assets/floors/floor_6.png create mode 100644 webview-ui/public/assets/floors/floor_7.png create mode 100644 webview-ui/public/assets/floors/floor_8.png create mode 100644 webview-ui/public/assets/furniture/BIN/BIN.png create mode 100644 webview-ui/public/assets/furniture/BIN/manifest.json create mode 100644 webview-ui/public/assets/furniture/BOOKSHELF/BOOKSHELF.png create mode 100644 webview-ui/public/assets/furniture/BOOKSHELF/manifest.json create mode 100644 webview-ui/public/assets/furniture/CACTUS/CACTUS.png create mode 100644 webview-ui/public/assets/furniture/CACTUS/manifest.json create mode 100644 webview-ui/public/assets/furniture/CLOCK/CLOCK.png create mode 100644 webview-ui/public/assets/furniture/CLOCK/manifest.json create mode 100644 webview-ui/public/assets/furniture/COFFEE/COFFEE.png create mode 100644 webview-ui/public/assets/furniture/COFFEE/manifest.json create mode 100644 webview-ui/public/assets/furniture/COFFEE_TABLE/COFFEE_TABLE.png create mode 100644 webview-ui/public/assets/furniture/COFFEE_TABLE/manifest.json create mode 100644 webview-ui/public/assets/furniture/CUSHIONED_BENCH/CUSHIONED_BENCH.png create mode 100644 webview-ui/public/assets/furniture/CUSHIONED_BENCH/manifest.json create mode 100644 webview-ui/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png create mode 100644 webview-ui/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_FRONT.png create mode 100644 webview-ui/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_SIDE.png create mode 100644 webview-ui/public/assets/furniture/CUSHIONED_CHAIR/manifest.json create mode 100644 webview-ui/public/assets/furniture/DESK/DESK_FRONT.png create mode 100644 webview-ui/public/assets/furniture/DESK/DESK_SIDE.png create mode 100644 webview-ui/public/assets/furniture/DESK/manifest.json create mode 100644 webview-ui/public/assets/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png create mode 100644 webview-ui/public/assets/furniture/DOUBLE_BOOKSHELF/manifest.json create mode 100644 webview-ui/public/assets/furniture/HANGING_PLANT/HANGING_PLANT.png create mode 100644 webview-ui/public/assets/furniture/HANGING_PLANT/manifest.json create mode 100644 webview-ui/public/assets/furniture/LARGE_PAINTING/LARGE_PAINTING.png create mode 100644 webview-ui/public/assets/furniture/LARGE_PAINTING/manifest.json create mode 100644 webview-ui/public/assets/furniture/LARGE_PLANT/LARGE_PLANT.png create mode 100644 webview-ui/public/assets/furniture/LARGE_PLANT/manifest.json create mode 100644 webview-ui/public/assets/furniture/PC/PC_BACK.png create mode 100644 webview-ui/public/assets/furniture/PC/PC_FRONT_OFF.png create mode 100644 webview-ui/public/assets/furniture/PC/PC_FRONT_ON_1.png create mode 100644 webview-ui/public/assets/furniture/PC/PC_FRONT_ON_2.png create mode 100644 webview-ui/public/assets/furniture/PC/PC_FRONT_ON_3.png create mode 100644 webview-ui/public/assets/furniture/PC/PC_SIDE.png create mode 100644 webview-ui/public/assets/furniture/PC/manifest.json create mode 100644 webview-ui/public/assets/furniture/PLANT/PLANT.png create mode 100644 webview-ui/public/assets/furniture/PLANT/manifest.json create mode 100644 webview-ui/public/assets/furniture/PLANT_2/PLANT_2.png create mode 100644 webview-ui/public/assets/furniture/PLANT_2/manifest.json create mode 100644 webview-ui/public/assets/furniture/POT/POT.png create mode 100644 webview-ui/public/assets/furniture/POT/manifest.json create mode 100644 webview-ui/public/assets/furniture/SMALL_PAINTING/SMALL_PAINTING.png create mode 100644 webview-ui/public/assets/furniture/SMALL_PAINTING/manifest.json create mode 100644 webview-ui/public/assets/furniture/SMALL_PAINTING_2/SMALL_PAINTING_2.png create mode 100644 webview-ui/public/assets/furniture/SMALL_PAINTING_2/manifest.json create mode 100644 webview-ui/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_FRONT.png create mode 100644 webview-ui/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_SIDE.png create mode 100644 webview-ui/public/assets/furniture/SMALL_TABLE/manifest.json create mode 100644 webview-ui/public/assets/furniture/SOFA/SOFA_BACK.png create mode 100644 webview-ui/public/assets/furniture/SOFA/SOFA_FRONT.png create mode 100644 webview-ui/public/assets/furniture/SOFA/SOFA_SIDE.png create mode 100644 webview-ui/public/assets/furniture/SOFA/manifest.json create mode 100644 webview-ui/public/assets/furniture/TABLE_FRONT/TABLE_FRONT.png create mode 100644 webview-ui/public/assets/furniture/TABLE_FRONT/manifest.json create mode 100644 webview-ui/public/assets/furniture/WHITEBOARD/WHITEBOARD.png create mode 100644 webview-ui/public/assets/furniture/WHITEBOARD/manifest.json create mode 100644 webview-ui/public/assets/furniture/WOODEN_BENCH/WOODEN_BENCH.png create mode 100644 webview-ui/public/assets/furniture/WOODEN_BENCH/manifest.json create mode 100644 webview-ui/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_BACK.png create mode 100644 webview-ui/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_FRONT.png create mode 100644 webview-ui/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_SIDE.png create mode 100644 webview-ui/public/assets/furniture/WOODEN_CHAIR/manifest.json rename webview-ui/public/assets/{walls.png => walls/wall_0.png} (100%) rename webview-ui/{src => public}/fonts/FSPixelSansUnicode-Regular.ttf (100%) create mode 100644 webview-ui/src/office/sprites/bubble-permission.json create mode 100644 webview-ui/src/office/sprites/bubble-waiting.json diff --git a/.gitignore b/.gitignore index e5d31d54..30ab455b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,30 @@ +# Compiled output out dist node_modules +*.tsbuildinfo + +# Logs +/logs +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# IDE .vscode-test/ +/.idea + +# Build artifacts *.vsix +*.map + +# Environment files +.env* +!.env.example + +# Project-specific .claude/ -/logs /sprites-export -.env \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index eb7a71ab..f2bc6400 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,4 @@ webview-ui/dist/ *.md webview-ui/public/ scripts/*.html -webview-ui/src/office/sprites/spriteData.ts + diff --git a/.vscode/settings.json b/.vscode/settings.json index b49e3c2f..6b55c503 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,6 @@ }, "[markdown]": { "editor.formatOnSave": false - } + }, + "liveServer.settings.port": 5501 } \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 25c0dfc7..9ac5e341 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,7 +131,7 @@ Toggle via "Layout" button. Tools: SELECT (default), Floor paint, Wall paint, Er ## Asset System -**Loading**: `esbuild.js` copies `webview-ui/public/assets/` → `dist/assets/`. Loader checks bundled path first, falls back to workspace root. PNG → pngjs → SpriteData (2D hex array, alpha≥128 = opaque). `loadDefaultLayout()` reads `assets/default-layout.json` (JSON OfficeLayout) as fallback for new workspaces. +**Loading**: `esbuild.js` copies `webview-ui/public/assets/` → `dist/assets/`. Loader checks bundled path first, falls back to workspace root. PNG → pngjs → SpriteData (2D hex array, alpha≥2 = visible, `#RRGGBBAA` for semi-transparent). `loadDefaultLayout()` reads `assets/default-layout.json` (JSON OfficeLayout) as fallback for new workspaces. **Catalog**: `furniture-catalog.json` with id, name, label, category, footprint, isDesk, canPlaceOnWalls, groupId?, orientation?, state?, canPlaceOnSurfaces?, backgroundTiles?. String-based type system (no enum constraint). Categories: desks, chairs, storage, electronics, decor, wall, misc. Wall-placeable items (`canPlaceOnWalls: true`) use the `wall` category and appear in a dedicated "Wall" tab in the editor. Asset naming convention: `{BASE}[_{ORIENTATION}][_{STATE}]` (e.g., `MONITOR_FRONT_OFF`, `CRT_MONITOR_BACK`). `orientation` is stored on `FurnitureCatalogEntry` and used for chair z-sorting and seat facing direction. @@ -167,7 +167,7 @@ Toggle via "Layout" button. Tools: SELECT (default), Floor paint, Wall paint, Er - `/clear` creates NEW JSONL file (old file just stops) - `--output-format stream-json` needs non-TTY stdin — can't use with VS Code terminals - Hook-based IPC failed (hooks captured at startup, env vars don't propagate). JSONL watching works -- PNG→SpriteData: pngjs for RGBA buffer, alpha threshold 128 +- PNG→SpriteData: pngjs for RGBA buffer, alpha threshold 2 (`PNG_ALPHA_THRESHOLD`), supports `#RRGGBBAA` semi-transparent pixels - OfficeCanvas selection changes are imperative (`editorState.selectedFurnitureUid`); must call `onEditorSelectionChange()` to trigger React re-render for toolbar ## Build & Dev diff --git a/README.md b/README.md index 94b7f129..9f64a9a5 100644 --- a/README.md +++ b/README.md @@ -89,19 +89,15 @@ The grid is expandable up to 64×64 tiles. Click the ghost border outside the cu ### Office Assets -**Free assets are COMING VERY SOON!** +All office assets (furniture, floors, walls) are now **fully open-source** and included in this repository under `webview-ui/public/assets/`. No external purchases or imports are needed — everything works out of the box. -The office tileset currently used in this project and available via the extension is **[Office Interior Tileset (16x16)](https://donarg.itch.io/officetileset)** by **Donarg**, available on itch.io for **$2 USD**. +Each furniture item lives in its own folder under `assets/furniture/` with a `manifest.json` that declares its sprites, rotation groups, state groups (on/off), and animation frames. Floor tiles are individual PNGs in `assets/floors/`, and wall tile sets are in `assets/walls/`. This modular structure makes it easy to add, remove, or modify assets without touching any code. -This is the only part of the project that is currently not freely available. The tileset is not included in this repository due to its license. To use Pixel Agents locally with the full set of office furniture and decorations, purchase the tileset and run the asset import pipeline: +To add a new furniture item, create a folder in `webview-ui/public/assets/furniture/` with your PNG sprite(s) and a `manifest.json`, then rebuild. The asset manager (`scripts/asset-manager.html`) provides a visual editor for creating and editing manifests. -```bash -npm run import-tileset -``` - -Fair warning: the import pipeline is not exactly straightforward — the out-of-the-box tileset assets aren't the easiest to work with, and while I've done my best to make the process as smooth as possible, it may require some manual tweaking. If you have experience creating pixel art office assets and would like to contribute freely usable tilesets for the community, that would be hugely appreciated. +Detailed documentation on the manifest format and asset pipeline is coming soon. -The extension will still work without the tileset — you'll get the default characters and basic layout, but the full furniture catalog requires the imported assets. +Characters are based on the amazing work of [JIK-A-4, Metro City](https://jik-a-4.itch.io/metrocity-free-topdown-character-pack). ## How It Works diff --git a/package-lock.json b/package-lock.json index 2d81a91c..a03e1019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pixel-agents", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pixel-agents", - "version": "1.0.2", + "version": "1.1.0", "license": "MIT", "devDependencies": { "@anthropic-ai/sdk": "^0.74.0", diff --git a/package.json b/package.json index 5381e1a1..216bfcec 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "pixel-agents", "displayName": "Pixel Agents", "description": "Pixel art office where your Claude Code agents come to life as animated characters", - "version": "1.0.2", + "version": "1.1.0", "publisher": "pablodelucca", "repository": { "type": "git", diff --git a/scripts/.tileset-working/asset-editor-output.json b/scripts/.tileset-working/asset-editor-output.json deleted file mode 100644 index 3d938177..00000000 --- a/scripts/.tileset-working/asset-editor-output.json +++ /dev/null @@ -1,3701 +0,0 @@ -{ - "version": 1, - "timestamp": "2026-02-09T17:12:55.257Z", - "sourceFile": "assets/office_tileset_16x16.png", - "tileset": { - "width": 256, - "height": 512 - }, - "backgroundColor": "#00000000", - "totalPixels": 131072, - "backgroundPixels": 63253, - "assets": [ - { - "id": "ASSET_0", - "x": 20, - "y": 0, - "width": 40, - "height": 22, - "paddedX": 16, - "paddedY": -10, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_1", - "x": 68, - "y": 0, - "width": 40, - "height": 22, - "paddedX": 64, - "paddedY": -10, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_4", - "x": 112, - "y": 12, - "width": 112, - "height": 20, - "paddedX": 128, - "paddedY": 0, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_6", - "x": 4, - "y": 32, - "width": 40, - "height": 22, - "paddedX": 0, - "paddedY": 22, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_7", - "x": 48, - "y": 32, - "width": 176, - "height": 32, - "paddedX": 80, - "paddedY": 22, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_10", - "x": 0, - "y": 64, - "width": 32, - "height": 22, - "paddedX": 0, - "paddedY": 54, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_11", - "x": 0, - "y": 64, - "width": 128, - "height": 86, - "paddedX": 0, - "paddedY": 86, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_15", - "x": 128, - "y": 76, - "width": 48, - "height": 20, - "paddedX": 128, - "paddedY": 64, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_17", - "x": 182, - "y": 95, - "width": 20, - "height": 25, - "paddedX": 176, - "paddedY": 88, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_18", - "x": 214, - "y": 95, - "width": 20, - "height": 25, - "paddedX": 208, - "paddedY": 88, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_25", - "x": 132, - "y": 143, - "width": 40, - "height": 25, - "paddedX": 128, - "paddedY": 136, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_23", - "x": 177, - "y": 138, - "width": 30, - "height": 30, - "paddedX": 176, - "paddedY": 136, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_24", - "x": 209, - "y": 138, - "width": 30, - "height": 30, - "paddedX": 208, - "paddedY": 136, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_26", - "x": 246, - "y": 149, - "width": 4, - "height": 27, - "paddedX": 240, - "paddedY": 144, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_27_A", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 0, - "paddedY": 160, - "paddedWidth": 32, - "paddedHeight": 48, - "splitMarker": true - }, - { - "id": "ASSET_27_B_A_A", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 128, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_A", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 144, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_B_A", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 160, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_B_B_A", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 176, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_B_B_B", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 192, - "paddedY": 183, - "paddedWidth": 32, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_0", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 128, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_1", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 144, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_2", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 160, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_3", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 176, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_4", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 192, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_5", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 208, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true, - "discard": false - }, - { - "id": "ASSET_28", - "x": 193, - "y": 185, - "width": 29, - "height": 31, - "paddedX": 192, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_29", - "x": 225, - "y": 185, - "width": 29, - "height": 31, - "paddedX": 224, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_30", - "x": 230, - "y": 229, - "width": 4, - "height": 27, - "paddedX": 224, - "paddedY": 224, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_32", - "x": 2, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 0, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_38", - "x": 18, - "y": 260, - "width": 12, - "height": 14, - "paddedX": 16, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_33", - "x": 34, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 32, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_34", - "x": 50, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 48, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_35", - "x": 66, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 64, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_39", - "x": 82, - "y": 260, - "width": 12, - "height": 14, - "paddedX": 80, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_36", - "x": 98, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 96, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_37", - "x": 114, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 112, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_42", - "x": 144, - "y": 267, - "width": 12, - "height": 29, - "paddedX": 142, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_41_0_0", - "x": 192, - "y": 265, - "width": 32, - "height": 31, - "paddedX": 192, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true - }, - { - "id": "ASSET_41_0_1", - "x": 192, - "y": 265, - "width": 32, - "height": 31, - "paddedX": 208, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32, - "splitMarker": true - }, - { - "id": "ASSET_40", - "x": 229, - "y": 264, - "width": 23, - "height": 32, - "paddedX": 225, - "paddedY": 264, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_44", - "x": 134, - "y": 281, - "width": 8, - "height": 13, - "paddedX": 130, - "paddedY": 278, - "paddedWidth": 16, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 14, - "y": 1 - }, - { - "x": 14, - "y": 2 - }, - { - "x": 14, - "y": 3 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 14, - "y": 7 - }, - { - "x": 14, - "y": 8 - }, - { - "x": 14, - "y": 9 - }, - { - "x": 14, - "y": 10 - }, - { - "x": 14, - "y": 11 - }, - { - "x": 14, - "y": 12 - }, - { - "x": 14, - "y": 13 - }, - { - "x": 14, - "y": 14 - }, - { - "x": 14, - "y": 15 - }, - { - "x": 15, - "y": 15 - }, - { - "x": 15, - "y": 14 - }, - { - "x": 15, - "y": 13 - }, - { - "x": 15, - "y": 12 - }, - { - "x": 15, - "y": 11 - }, - { - "x": 15, - "y": 10 - }, - { - "x": 15, - "y": 9 - }, - { - "x": 15, - "y": 8 - }, - { - "x": 15, - "y": 7 - }, - { - "x": 15, - "y": 6 - }, - { - "x": 15, - "y": 5 - }, - { - "x": 15, - "y": 4 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 14, - "y": 0 - } - ] - }, - { - "id": "ASSET_46", - "x": 18, - "y": 288, - "width": 12, - "height": 32, - "paddedX": 16, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_47", - "x": 34, - "y": 288, - "width": 12, - "height": 32, - "paddedX": 32, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_49", - "x": 2, - "y": 293, - "width": 12, - "height": 13, - "paddedX": 0, - "paddedY": 290, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_50_0_0", - "x": 64, - "y": 299, - "width": 64, - "height": 16, - "paddedX": 64, - "paddedY": 299, - "paddedWidth": 32, - "paddedHeight": 16, - "splitMarker": true - }, - { - "id": "ASSET_50_0_1", - "x": 64, - "y": 299, - "width": 64, - "height": 16, - "paddedX": 96, - "paddedY": 299, - "paddedWidth": 32, - "paddedHeight": 16, - "splitMarker": true - }, - { - "id": "ASSET_51", - "x": 229, - "y": 307, - "width": 8, - "height": 8, - "paddedX": 225, - "paddedY": 299, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_54", - "x": 131, - "y": 316, - "width": 13, - "height": 19, - "paddedX": 125, - "paddedY": 303, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 1, - "y": 10 - }, - { - "x": 1, - "y": 9 - }, - { - "x": 1, - "y": 8 - }, - { - "x": 1, - "y": 7 - }, - { - "x": 1, - "y": 6 - }, - { - "x": 1, - "y": 5 - }, - { - "x": 1, - "y": 4 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 0, - "y": 4 - }, - { - "x": 0, - "y": 5 - }, - { - "x": 0, - "y": 6 - }, - { - "x": 0, - "y": 7 - }, - { - "x": 0, - "y": 8 - }, - { - "x": 0, - "y": 9 - }, - { - "x": 0, - "y": 10 - }, - { - "x": 2, - "y": 7 - }, - { - "x": 2, - "y": 6 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 2, - "y": 4 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 2, - "y": 0 - } - ] - }, - { - "id": "ASSET_55", - "x": 163, - "y": 316, - "width": 13, - "height": 19, - "paddedX": 161, - "paddedY": 303, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_61", - "x": 199, - "y": 330, - "width": 3, - "height": 1, - "paddedX": 192, - "paddedY": 315, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_63", - "x": 35, - "y": 331, - "width": 25, - "height": 15, - "paddedX": 32, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_64", - "x": 67, - "y": 331, - "width": 25, - "height": 15, - "paddedX": 64, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_65", - "x": 99, - "y": 331, - "width": 25, - "height": 15, - "paddedX": 96, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_70", - "x": 135, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 131, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_71", - "x": 151, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 147, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_72", - "x": 167, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 163, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_73", - "x": 183, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 179, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 14, - "y": 3 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 15, - "y": 7 - }, - { - "x": 15, - "y": 8 - }, - { - "x": 15, - "y": 9 - }, - { - "x": 15, - "y": 10 - }, - { - "x": 15, - "y": 11 - }, - { - "x": 15, - "y": 12 - }, - { - "x": 14, - "y": 10 - }, - { - "x": 14, - "y": 9 - }, - { - "x": 14, - "y": 8 - }, - { - "x": 14, - "y": 7 - }, - { - "x": 15, - "y": 5 - }, - { - "x": 15, - "y": 6 - }, - { - "x": 15, - "y": 4 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 14, - "y": 11 - } - ] - }, - { - "id": "ASSET_78", - "x": 192, - "y": 360, - "width": 16, - "height": 15, - "paddedX": 192, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_79", - "x": 224, - "y": 360, - "width": 16, - "height": 15, - "paddedX": 224, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_83", - "x": 2, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 0, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_84", - "x": 18, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 16, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_87", - "x": 66, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 64, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_74", - "x": 129, - "y": 360, - "width": 14, - "height": 16, - "paddedX": 128, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_75", - "x": 144, - "y": 360, - "width": 7, - "height": 16, - "paddedX": 128, - "paddedY": 360, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_76", - "x": 161, - "y": 360, - "width": 14, - "height": 16, - "paddedX": 160, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_77", - "x": 176, - "y": 360, - "width": 7, - "height": 16, - "paddedX": 160, - "paddedY": 360, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_80", - "x": 80, - "y": 363, - "width": 15, - "height": 15, - "paddedX": 80, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_81", - "x": 97, - "y": 363, - "width": 14, - "height": 15, - "paddedX": 96, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_82", - "x": 113, - "y": 363, - "width": 14, - "height": 15, - "paddedX": 112, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_90", - "x": 195, - "y": 377, - "width": 10, - "height": 5, - "paddedX": 192, - "paddedY": 352, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_92", - "x": 227, - "y": 377, - "width": 10, - "height": 5, - "paddedX": 224, - "paddedY": 352, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_94", - "x": 131, - "y": 378, - "width": 10, - "height": 5, - "paddedX": 128, - "paddedY": 353, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_95", - "x": 143, - "y": 378, - "width": 4, - "height": 5, - "paddedX": 160, - "paddedY": 353, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_98", - "x": 160, - "y": 385, - "width": 13, - "height": 17, - "paddedX": 159, - "paddedY": 370, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 4, - "y": 12 - }, - { - "x": 5, - "y": 12 - }, - { - "x": 6, - "y": 12 - }, - { - "x": 7, - "y": 12 - }, - { - "x": 8, - "y": 12 - }, - { - "x": 9, - "y": 12 - }, - { - "x": 10, - "y": 12 - }, - { - "x": 11, - "y": 12 - }, - { - "x": 12, - "y": 12 - }, - { - "x": 13, - "y": 12 - }, - { - "x": 14, - "y": 11 - }, - { - "x": 14, - "y": 10 - }, - { - "x": 14, - "y": 9 - }, - { - "x": 13, - "y": 10 - }, - { - "x": 13, - "y": 11 - }, - { - "x": 12, - "y": 11 - }, - { - "x": 12, - "y": 10 - }, - { - "x": 13, - "y": 9 - }, - { - "x": 11, - "y": 11 - }, - { - "x": 10, - "y": 11 - }, - { - "x": 10, - "y": 10 - }, - { - "x": 11, - "y": 9 - }, - { - "x": 12, - "y": 8 - }, - { - "x": 13, - "y": 8 - }, - { - "x": 12, - "y": 9 - }, - { - "x": 11, - "y": 10 - }, - { - "x": 9, - "y": 11 - }, - { - "x": 8, - "y": 11 - }, - { - "x": 9, - "y": 10 - }, - { - "x": 10, - "y": 9 - }, - { - "x": 8, - "y": 10 - }, - { - "x": 7, - "y": 11 - }, - { - "x": 6, - "y": 11 - }, - { - "x": 6, - "y": 10 - }, - { - "x": 7, - "y": 10 - }, - { - "x": 8, - "y": 9 - }, - { - "x": 9, - "y": 9 - }, - { - "x": 5, - "y": 11 - }, - { - "x": 4, - "y": 11 - }, - { - "x": 3, - "y": 11 - }, - { - "x": 3, - "y": 10 - }, - { - "x": 4, - "y": 9 - }, - { - "x": 5, - "y": 9 - }, - { - "x": 5, - "y": 8 - }, - { - "x": 6, - "y": 8 - }, - { - "x": 6, - "y": 9 - }, - { - "x": 4, - "y": 8 - }, - { - "x": 5, - "y": 10 - }, - { - "x": 4, - "y": 7 - }, - { - "x": 7, - "y": 9 - }, - { - "x": 3, - "y": 9 - }, - { - "x": 8, - "y": 8 - }, - { - "x": 9, - "y": 8 - }, - { - "x": 10, - "y": 8 - }, - { - "x": 11, - "y": 8 - }, - { - "x": 7, - "y": 8 - }, - { - "x": 4, - "y": 10 - }, - { - "x": 8, - "y": 7 - }, - { - "x": 9, - "y": 7 - }, - { - "x": 9, - "y": 6 - }, - { - "x": 8, - "y": 6 - }, - { - "x": 7, - "y": 7 - }, - { - "x": 6, - "y": 7 - }, - { - "x": 5, - "y": 7 - }, - { - "x": 3, - "y": 8 - }, - { - "x": 7, - "y": 6 - }, - { - "x": 7, - "y": 5 - }, - { - "x": 6, - "y": 5 - }, - { - "x": 8, - "y": 5 - }, - { - "x": 9, - "y": 5 - }, - { - "x": 10, - "y": 6 - }, - { - "x": 11, - "y": 6 - }, - { - "x": 10, - "y": 5 - }, - { - "x": 5, - "y": 5 - }, - { - "x": 4, - "y": 5 - }, - { - "x": 3, - "y": 5 - }, - { - "x": 3, - "y": 6 - }, - { - "x": 4, - "y": 6 - }, - { - "x": 5, - "y": 6 - }, - { - "x": 6, - "y": 6 - }, - { - "x": 11, - "y": 5 - }, - { - "x": 12, - "y": 6 - }, - { - "x": 12, - "y": 5 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 4, - "y": 4 - }, - { - "x": 5, - "y": 4 - }, - { - "x": 6, - "y": 4 - }, - { - "x": 7, - "y": 4 - }, - { - "x": 8, - "y": 4 - }, - { - "x": 9, - "y": 4 - }, - { - "x": 10, - "y": 4 - }, - { - "x": 11, - "y": 4 - }, - { - "x": 12, - "y": 4 - }, - { - "x": 13, - "y": 4 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 13, - "y": 3 - }, - { - "x": 14, - "y": 3 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 12, - "y": 3 - }, - { - "x": 11, - "y": 3 - }, - { - "x": 10, - "y": 3 - }, - { - "x": 9, - "y": 3 - }, - { - "x": 8, - "y": 3 - }, - { - "x": 7, - "y": 3 - }, - { - "x": 6, - "y": 3 - }, - { - "x": 5, - "y": 3 - }, - { - "x": 4, - "y": 3 - }, - { - "x": 3, - "y": 3 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 4, - "y": 2 - }, - { - "x": 3, - "y": 4 - }, - { - "x": 3, - "y": 2 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 6, - "y": 2 - }, - { - "x": 7, - "y": 2 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 8, - "y": 2 - }, - { - "x": 9, - "y": 2 - }, - { - "x": 10, - "y": 2 - }, - { - "x": 11, - "y": 2 - }, - { - "x": 12, - "y": 2 - }, - { - "x": 13, - "y": 2 - }, - { - "x": 14, - "y": 2 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 13, - "y": 1 - }, - { - "x": 14, - "y": 1 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 13, - "y": 6 - }, - { - "x": 13, - "y": 7 - }, - { - "x": 13, - "y": 5 - }, - { - "x": 12, - "y": 7 - }, - { - "x": 11, - "y": 7 - }, - { - "x": 10, - "y": 7 - }, - { - "x": 5, - "y": 13 - }, - { - "x": 6, - "y": 13 - }, - { - "x": 7, - "y": 13 - }, - { - "x": 8, - "y": 13 - }, - { - "x": 9, - "y": 13 - }, - { - "x": 10, - "y": 13 - }, - { - "x": 11, - "y": 13 - }, - { - "x": 9, - "y": 14 - }, - { - "x": 8, - "y": 14 - }, - { - "x": 7, - "y": 14 - }, - { - "x": 6, - "y": 14 - }, - { - "x": 5, - "y": 14 - }, - { - "x": 4, - "y": 13 - }, - { - "x": 3, - "y": 13 - }, - { - "x": 2, - "y": 13 - }, - { - "x": 2, - "y": 12 - }, - { - "x": 3, - "y": 12 - }, - { - "x": 14, - "y": 12 - }, - { - "x": 15, - "y": 12 - }, - { - "x": 15, - "y": 11 - }, - { - "x": 15, - "y": 10 - }, - { - "x": 15, - "y": 9 - }, - { - "x": 14, - "y": 8 - }, - { - "x": 15, - "y": 8 - }, - { - "x": 13, - "y": 13 - }, - { - "x": 12, - "y": 13 - }, - { - "x": 12, - "y": 14 - }, - { - "x": 11, - "y": 14 - } - ] - }, - { - "id": "ASSET_99", - "x": 179, - "y": 385, - "width": 13, - "height": 17, - "paddedX": 177, - "paddedY": 370, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 2, - "y": 9 - }, - { - "x": 1, - "y": 10 - }, - { - "x": 1, - "y": 11 - }, - { - "x": 0, - "y": 11 - }, - { - "x": 0, - "y": 12 - }, - { - "x": 0, - "y": 10 - }, - { - "x": 0, - "y": 9 - }, - { - "x": 0, - "y": 8 - }, - { - "x": 1, - "y": 7 - }, - { - "x": 1, - "y": 8 - }, - { - "x": 1, - "y": 9 - }, - { - "x": 1, - "y": 12 - }, - { - "x": 0, - "y": 7 - }, - { - "x": 0, - "y": 6 - }, - { - "x": 0, - "y": 5 - }, - { - "x": 1, - "y": 5 - }, - { - "x": 2, - "y": 6 - }, - { - "x": 3, - "y": 6 - }, - { - "x": 4, - "y": 6 - }, - { - "x": 5, - "y": 6 - }, - { - "x": 5, - "y": 5 - }, - { - "x": 5, - "y": 4 - }, - { - "x": 5, - "y": 3 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 4, - "y": 3 - }, - { - "x": 4, - "y": 4 - }, - { - "x": 3, - "y": 4 - }, - { - "x": 3, - "y": 5 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 1, - "y": 6 - }, - { - "x": 4, - "y": 5 - }, - { - "x": 4, - "y": 2 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 3, - "y": 2 - }, - { - "x": 3, - "y": 3 - }, - { - "x": 2, - "y": 4 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 1, - "y": 4 - }, - { - "x": 0, - "y": 4 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 13, - "y": 1 - }, - { - "x": 14, - "y": 1 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 6, - "y": 1 - } - ] - }, - { - "id": "ASSET_100", - "x": 243, - "y": 388, - "width": 9, - "height": 8, - "paddedX": 240, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_107", - "x": 130, - "y": 397, - "width": 12, - "height": 17, - "paddedX": 128, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 3, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 2, - "y": 0 - } - ] - }, - { - "id": "ASSET_108", - "x": 146, - "y": 397, - "width": 12, - "height": 17, - "paddedX": 144, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 2, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - } - ] - }, - { - "id": "ASSET_110", - "x": 228, - "y": 398, - "width": 8, - "height": 9, - "paddedX": 224, - "paddedY": 394, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_109", - "x": 202, - "y": 398, - "width": 12, - "height": 10, - "paddedX": 200, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_101", - "x": 1, - "y": 395, - "width": 30, - "height": 16, - "paddedX": 0, - "paddedY": 395, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_102", - "x": 33, - "y": 395, - "width": 30, - "height": 16, - "paddedX": 32, - "paddedY": 395, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_103", - "x": 65, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 64, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_104", - "x": 81, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 80, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_105", - "x": 97, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 96, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_106", - "x": 113, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 112, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_111", - "x": 244, - "y": 404, - "width": 9, - "height": 8, - "paddedX": 241, - "paddedY": 400, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_122", - "x": 5, - "y": 426, - "width": 22, - "height": 17, - "paddedX": 0, - "paddedY": 418, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_118", - "x": 35, - "y": 425, - "width": 26, - "height": 19, - "paddedX": 32, - "paddedY": 418, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_119", - "x": 67, - "y": 425, - "width": 26, - "height": 19, - "paddedX": 64, - "paddedY": 418, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 5, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 25, - "y": 31 - }, - { - "x": 26, - "y": 31 - }, - { - "x": 27, - "y": 31 - }, - { - "x": 28, - "y": 31 - }, - { - "x": 4, - "y": 31 - } - ] - }, - { - "id": "ASSET_120", - "x": 97, - "y": 425, - "width": 30, - "height": 20, - "paddedX": 96, - "paddedY": 419, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 10, - "y": 30 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 9, - "y": 30 - }, - { - "x": 11, - "y": 30 - }, - { - "x": 12, - "y": 30 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 30 - }, - { - "x": 18, - "y": 30 - }, - { - "x": 19, - "y": 30 - }, - { - "x": 20, - "y": 30 - }, - { - "x": 21, - "y": 30 - }, - { - "x": 22, - "y": 30 - }, - { - "x": 23, - "y": 30 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 17, - "y": 31 - } - ] - }, - { - "id": "ASSET_114", - "x": 193, - "y": 418, - "width": 14, - "height": 14, - "paddedX": 192, - "paddedY": 416, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_112", - "x": 208, - "y": 416, - "width": 7, - "height": 16, - "paddedX": 192, - "paddedY": 416, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_121", - "x": 152, - "y": 425, - "width": 16, - "height": 15, - "paddedX": 152, - "paddedY": 424, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_123", - "x": 135, - "y": 426, - "width": 7, - "height": 16, - "paddedX": 131, - "paddedY": 426, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_125", - "x": 183, - "y": 432, - "width": 18, - "height": 15, - "paddedX": 176, - "paddedY": 431, - "paddedWidth": 32, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 20, - "y": 1 - }, - { - "x": 21, - "y": 1 - }, - { - "x": 22, - "y": 1 - }, - { - "x": 23, - "y": 1 - }, - { - "x": 24, - "y": 1 - }, - { - "x": 25, - "y": 1 - }, - { - "x": 26, - "y": 1 - }, - { - "x": 27, - "y": 1 - }, - { - "x": 27, - "y": 0 - }, - { - "x": 26, - "y": 0 - }, - { - "x": 25, - "y": 0 - }, - { - "x": 24, - "y": 0 - }, - { - "x": 23, - "y": 0 - }, - { - "x": 22, - "y": 0 - }, - { - "x": 21, - "y": 0 - }, - { - "x": 20, - "y": 0 - } - ] - }, - { - "id": "ASSET_126", - "x": 216, - "y": 432, - "width": 16, - "height": 15, - "paddedX": 216, - "paddedY": 431, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_127", - "x": 242, - "y": 434, - "width": 13, - "height": 14, - "paddedX": 241, - "paddedY": 432, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_133_0_0", - "x": 51, - "y": 455, - "width": 10, - "height": 18, - "paddedX": 48, - "paddedY": 443, - "paddedWidth": 16, - "paddedHeight": 32, - "discard": false, - "erasedPixels": [ - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 6, - "y": 0 - } - ], - "splitMarker": true - }, - { - "id": "ASSET_133_1_0", - "x": 51, - "y": 455, - "width": 10, - "height": 18, - "paddedX": 49, - "paddedY": 463, - "paddedWidth": 16, - "paddedHeight": 16, - "discard": false, - "splitMarker": true - }, - { - "id": "ASSET_134", - "x": 1, - "y": 457, - "width": 30, - "height": 20, - "paddedX": 0, - "paddedY": 451, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 4, - "y": 31 - }, - { - "x": 5, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 25, - "y": 31 - }, - { - "x": 26, - "y": 31 - }, - { - "x": 27, - "y": 31 - } - ] - }, - { - "id": "ASSET_128", - "x": 66, - "y": 449, - "width": 13, - "height": 15, - "paddedX": 65, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_129", - "x": 82, - "y": 449, - "width": 13, - "height": 15, - "paddedX": 81, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_130", - "x": 99, - "y": 449, - "width": 11, - "height": 15, - "paddedX": 97, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_131", - "x": 115, - "y": 449, - "width": 11, - "height": 15, - "paddedX": 113, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_138", - "x": 130, - "y": 459, - "width": 13, - "height": 21, - "paddedX": 129, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_135", - "x": 146, - "y": 458, - "width": 13, - "height": 22, - "paddedX": 145, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_136", - "x": 163, - "y": 458, - "width": 26, - "height": 22, - "paddedX": 160, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_137", - "x": 195, - "y": 458, - "width": 26, - "height": 22, - "paddedX": 192, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_139", - "x": 227, - "y": 460, - "width": 26, - "height": 20, - "paddedX": 224, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_132", - "x": 35, - "y": 455, - "width": 10, - "height": 16, - "paddedX": 32, - "paddedY": 455, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_140", - "x": 66, - "y": 465, - "width": 13, - "height": 15, - "paddedX": 65, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_141", - "x": 82, - "y": 465, - "width": 13, - "height": 15, - "paddedX": 81, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_142", - "x": 99, - "y": 465, - "width": 11, - "height": 15, - "paddedX": 97, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_143", - "x": 115, - "y": 465, - "width": 11, - "height": 15, - "paddedX": 113, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_144", - "x": 5, - "y": 482, - "width": 22, - "height": 12, - "paddedX": 0, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_145", - "x": 37, - "y": 482, - "width": 22, - "height": 12, - "paddedX": 32, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_146", - "x": 69, - "y": 482, - "width": 22, - "height": 12, - "paddedX": 64, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 2 - }, - { - "x": 7, - "y": 2 - }, - { - "x": 8, - "y": 2 - }, - { - "x": 9, - "y": 2 - }, - { - "x": 10, - "y": 2 - }, - { - "x": 11, - "y": 2 - }, - { - "x": 12, - "y": 2 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 13, - "y": 1 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 14, - "y": 1 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 16, - "y": 1 - }, - { - "x": 17, - "y": 1 - }, - { - "x": 18, - "y": 0 - }, - { - "x": 19, - "y": 0 - }, - { - "x": 20, - "y": 0 - }, - { - "x": 21, - "y": 0 - }, - { - "x": 22, - "y": 0 - }, - { - "x": 21, - "y": 1 - }, - { - "x": 20, - "y": 1 - }, - { - "x": 22, - "y": 1 - }, - { - "x": 23, - "y": 1 - }, - { - "x": 24, - "y": 1 - }, - { - "x": 25, - "y": 1 - }, - { - "x": 26, - "y": 1 - }, - { - "x": 25, - "y": 0 - }, - { - "x": 24, - "y": 0 - }, - { - "x": 23, - "y": 0 - }, - { - "x": 27, - "y": 1 - }, - { - "x": 28, - "y": 1 - }, - { - "x": 29, - "y": 1 - }, - { - "x": 28, - "y": 0 - }, - { - "x": 27, - "y": 0 - }, - { - "x": 26, - "y": 0 - } - ] - }, - { - "id": "ASSET_147", - "x": 108, - "y": 482, - "width": 16, - "height": 13, - "paddedX": 108, - "paddedY": 479, - "paddedWidth": 16, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 0, - "y": 1 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 15, - "y": 0 - } - ] - }, - { - "id": "ASSET_148", - "x": 5, - "y": 498, - "width": 22, - "height": 12, - "paddedX": 0, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_149", - "x": 37, - "y": 498, - "width": 22, - "height": 12, - "paddedX": 32, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_150", - "x": 69, - "y": 498, - "width": 22, - "height": 12, - "paddedX": 64, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_151", - "x": 108, - "y": 498, - "width": 16, - "height": 13, - "paddedX": 108, - "paddedY": 495, - "paddedWidth": 16, - "paddedHeight": 16 - } - ] -} \ No newline at end of file diff --git a/scripts/.tileset-working/tileset-detection-output.json b/scripts/.tileset-working/tileset-detection-output.json deleted file mode 100644 index af62f382..00000000 --- a/scripts/.tileset-working/tileset-detection-output.json +++ /dev/null @@ -1,1686 +0,0 @@ -{ - "version": 1, - "timestamp": "2026-02-09T17:12:55.257Z", - "sourceFile": "assets/office_tileset_16x16.png", - "tileset": { - "width": 256, - "height": 512 - }, - "backgroundColor": "#00000000", - "totalPixels": 131072, - "backgroundPixels": 63253, - "assets": [ - { - "id": "ASSET_0", - "x": 20, - "y": 0, - "width": 40, - "height": 22, - "paddedX": 16, - "paddedY": 0, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_1", - "x": 68, - "y": 0, - "width": 40, - "height": 22, - "paddedX": 64, - "paddedY": 0, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_2", - "x": 112, - "y": 0, - "width": 16, - "height": 8, - "paddedX": 112, - "paddedY": 0, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_4", - "x": 112, - "y": 12, - "width": 112, - "height": 20, - "paddedX": 112, - "paddedY": 0, - "paddedWidth": 112, - "paddedHeight": 32 - }, - { - "id": "ASSET_3", - "x": 230, - "y": 7, - "width": 20, - "height": 9, - "paddedX": 224, - "paddedY": 0, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_5", - "x": 230, - "y": 23, - "width": 20, - "height": 9, - "paddedX": 224, - "paddedY": 16, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_6", - "x": 4, - "y": 32, - "width": 40, - "height": 22, - "paddedX": 0, - "paddedY": 22, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_7", - "x": 48, - "y": 32, - "width": 176, - "height": 32, - "paddedX": 48, - "paddedY": 32, - "paddedWidth": 176, - "paddedHeight": 32 - }, - { - "id": "ASSET_8", - "x": 225, - "y": 34, - "width": 30, - "height": 14, - "paddedX": 224, - "paddedY": 32, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_9", - "x": 225, - "y": 50, - "width": 30, - "height": 14, - "paddedX": 224, - "paddedY": 48, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_10", - "x": 0, - "y": 64, - "width": 32, - "height": 22, - "paddedX": 0, - "paddedY": 54, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_11", - "x": 0, - "y": 64, - "width": 128, - "height": 86, - "paddedX": 0, - "paddedY": 54, - "paddedWidth": 128, - "paddedHeight": 96 - }, - { - "id": "ASSET_15", - "x": 128, - "y": 76, - "width": 48, - "height": 20, - "paddedX": 128, - "paddedY": 64, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_12", - "x": 177, - "y": 65, - "width": 29, - "height": 15, - "paddedX": 176, - "paddedY": 64, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_13", - "x": 209, - "y": 65, - "width": 29, - "height": 15, - "paddedX": 208, - "paddedY": 64, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_14", - "x": 240, - "y": 68, - "width": 16, - "height": 12, - "paddedX": 240, - "paddedY": 64, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_16", - "x": 240, - "y": 84, - "width": 16, - "height": 12, - "paddedX": 240, - "paddedY": 80, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_17", - "x": 182, - "y": 95, - "width": 20, - "height": 25, - "paddedX": 176, - "paddedY": 88, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_18", - "x": 214, - "y": 95, - "width": 20, - "height": 25, - "paddedX": 208, - "paddedY": 88, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_20", - "x": 132, - "y": 103, - "width": 40, - "height": 9, - "paddedX": 128, - "paddedY": 96, - "paddedWidth": 48, - "paddedHeight": 16 - }, - { - "id": "ASSET_19", - "x": 240, - "y": 100, - "width": 16, - "height": 12, - "paddedX": 240, - "paddedY": 96, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_22", - "x": 132, - "y": 119, - "width": 40, - "height": 9, - "paddedX": 128, - "paddedY": 112, - "paddedWidth": 48, - "paddedHeight": 16 - }, - { - "id": "ASSET_21", - "x": 240, - "y": 116, - "width": 16, - "height": 12, - "paddedX": 240, - "paddedY": 112, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_25", - "x": 132, - "y": 143, - "width": 40, - "height": 25, - "paddedX": 128, - "paddedY": 136, - "paddedWidth": 48, - "paddedHeight": 32 - }, - { - "id": "ASSET_23", - "x": 177, - "y": 138, - "width": 30, - "height": 30, - "paddedX": 176, - "paddedY": 136, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_24", - "x": 209, - "y": 138, - "width": 30, - "height": 30, - "paddedX": 208, - "paddedY": 136, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_26", - "x": 246, - "y": 149, - "width": 4, - "height": 27, - "paddedX": 240, - "paddedY": 144, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_27", - "x": 0, - "y": 160, - "width": 224, - "height": 96, - "paddedX": 0, - "paddedY": 160, - "paddedWidth": 224, - "paddedHeight": 96 - }, - { - "id": "ASSET_28", - "x": 193, - "y": 185, - "width": 29, - "height": 31, - "paddedX": 192, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_29", - "x": 225, - "y": 185, - "width": 29, - "height": 31, - "paddedX": 224, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_30", - "x": 230, - "y": 229, - "width": 4, - "height": 27, - "paddedX": 224, - "paddedY": 224, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_31", - "x": 246, - "y": 229, - "width": 4, - "height": 27, - "paddedX": 240, - "paddedY": 224, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_32", - "x": 2, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 0, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_38", - "x": 18, - "y": 260, - "width": 12, - "height": 14, - "paddedX": 16, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_33", - "x": 34, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 32, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_34", - "x": 50, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 48, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_35", - "x": 66, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 64, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_39", - "x": 82, - "y": 260, - "width": 12, - "height": 14, - "paddedX": 80, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_36", - "x": 98, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 96, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_37", - "x": 114, - "y": 258, - "width": 12, - "height": 16, - "paddedX": 112, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_42", - "x": 144, - "y": 267, - "width": 12, - "height": 29, - "paddedX": 142, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_43", - "x": 164, - "y": 267, - "width": 12, - "height": 29, - "paddedX": 162, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_41", - "x": 192, - "y": 265, - "width": 32, - "height": 31, - "paddedX": 192, - "paddedY": 264, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_40", - "x": 229, - "y": 264, - "width": 23, - "height": 32, - "paddedX": 225, - "paddedY": 264, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_44", - "x": 134, - "y": 281, - "width": 8, - "height": 13, - "paddedX": 130, - "paddedY": 278, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_45", - "x": 178, - "y": 281, - "width": 8, - "height": 13, - "paddedX": 174, - "paddedY": 278, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_46", - "x": 18, - "y": 288, - "width": 12, - "height": 32, - "paddedX": 16, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_47", - "x": 34, - "y": 288, - "width": 12, - "height": 32, - "paddedX": 32, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_48", - "x": 50, - "y": 288, - "width": 12, - "height": 32, - "paddedX": 48, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_49", - "x": 2, - "y": 293, - "width": 12, - "height": 13, - "paddedX": 0, - "paddedY": 290, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_50", - "x": 64, - "y": 299, - "width": 64, - "height": 16, - "paddedX": 64, - "paddedY": 299, - "paddedWidth": 64, - "paddedHeight": 16 - }, - { - "id": "ASSET_51", - "x": 229, - "y": 307, - "width": 8, - "height": 8, - "paddedX": 225, - "paddedY": 299, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_52", - "x": 241, - "y": 307, - "width": 8, - "height": 8, - "paddedX": 237, - "paddedY": 299, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_53", - "x": 211, - "y": 309, - "width": 8, - "height": 8, - "paddedX": 207, - "paddedY": 301, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_54", - "x": 131, - "y": 316, - "width": 13, - "height": 19, - "paddedX": 130, - "paddedY": 303, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_55", - "x": 163, - "y": 316, - "width": 13, - "height": 19, - "paddedX": 162, - "paddedY": 303, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_56", - "x": 195, - "y": 317, - "width": 10, - "height": 13, - "paddedX": 192, - "paddedY": 314, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_61", - "x": 199, - "y": 330, - "width": 3, - "height": 1, - "paddedX": 193, - "paddedY": 315, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_57", - "x": 211, - "y": 323, - "width": 8, - "height": 8, - "paddedX": 207, - "paddedY": 315, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_58", - "x": 229, - "y": 325, - "width": 8, - "height": 8, - "paddedX": 225, - "paddedY": 317, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_59", - "x": 145, - "y": 326, - "width": 8, - "height": 8, - "paddedX": 141, - "paddedY": 318, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_60", - "x": 177, - "y": 326, - "width": 8, - "height": 8, - "paddedX": 173, - "paddedY": 318, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_63", - "x": 35, - "y": 331, - "width": 25, - "height": 15, - "paddedX": 32, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_64", - "x": 67, - "y": 331, - "width": 25, - "height": 15, - "paddedX": 64, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_65", - "x": 99, - "y": 331, - "width": 25, - "height": 15, - "paddedX": 96, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_62", - "x": 0, - "y": 331, - "width": 32, - "height": 16, - "paddedX": 0, - "paddedY": 331, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_66", - "x": 193, - "y": 339, - "width": 8, - "height": 9, - "paddedX": 189, - "paddedY": 332, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_67", - "x": 209, - "y": 339, - "width": 8, - "height": 9, - "paddedX": 205, - "paddedY": 332, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_68", - "x": 225, - "y": 339, - "width": 8, - "height": 9, - "paddedX": 221, - "paddedY": 332, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_69", - "x": 241, - "y": 339, - "width": 8, - "height": 9, - "paddedX": 237, - "paddedY": 332, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_70", - "x": 135, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 131, - "paddedY": 333, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_71", - "x": 151, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 147, - "paddedY": 333, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_72", - "x": 167, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 163, - "paddedY": 333, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_73", - "x": 183, - "y": 340, - "width": 8, - "height": 9, - "paddedX": 179, - "paddedY": 333, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_78", - "x": 192, - "y": 360, - "width": 16, - "height": 15, - "paddedX": 192, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_88", - "x": 210, - "y": 367, - "width": 8, - "height": 8, - "paddedX": 206, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_79", - "x": 224, - "y": 360, - "width": 16, - "height": 15, - "paddedX": 224, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_89", - "x": 242, - "y": 367, - "width": 8, - "height": 8, - "paddedX": 238, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_83", - "x": 2, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 0, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_84", - "x": 18, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 16, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_85", - "x": 34, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 32, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_86", - "x": 50, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 48, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_87", - "x": 66, - "y": 365, - "width": 11, - "height": 11, - "paddedX": 64, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_74", - "x": 129, - "y": 360, - "width": 14, - "height": 16, - "paddedX": 128, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_75", - "x": 144, - "y": 360, - "width": 7, - "height": 16, - "paddedX": 140, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_76", - "x": 161, - "y": 360, - "width": 14, - "height": 16, - "paddedX": 160, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_77", - "x": 176, - "y": 360, - "width": 7, - "height": 16, - "paddedX": 172, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_80", - "x": 80, - "y": 363, - "width": 15, - "height": 15, - "paddedX": 80, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_81", - "x": 97, - "y": 363, - "width": 14, - "height": 15, - "paddedX": 96, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_82", - "x": 113, - "y": 363, - "width": 14, - "height": 15, - "paddedX": 112, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_90", - "x": 195, - "y": 377, - "width": 10, - "height": 5, - "paddedX": 192, - "paddedY": 366, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_91", - "x": 207, - "y": 377, - "width": 4, - "height": 5, - "paddedX": 201, - "paddedY": 366, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_92", - "x": 227, - "y": 377, - "width": 10, - "height": 5, - "paddedX": 224, - "paddedY": 366, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_93", - "x": 239, - "y": 377, - "width": 4, - "height": 5, - "paddedX": 233, - "paddedY": 366, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_94", - "x": 131, - "y": 378, - "width": 10, - "height": 5, - "paddedX": 128, - "paddedY": 367, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_95", - "x": 143, - "y": 378, - "width": 4, - "height": 5, - "paddedX": 137, - "paddedY": 367, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_96", - "x": 163, - "y": 378, - "width": 10, - "height": 5, - "paddedX": 160, - "paddedY": 367, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_97", - "x": 175, - "y": 378, - "width": 4, - "height": 5, - "paddedX": 169, - "paddedY": 367, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_98", - "x": 160, - "y": 385, - "width": 13, - "height": 17, - "paddedX": 159, - "paddedY": 370, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_99", - "x": 179, - "y": 385, - "width": 13, - "height": 17, - "paddedX": 178, - "paddedY": 370, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_100", - "x": 243, - "y": 388, - "width": 9, - "height": 8, - "paddedX": 240, - "paddedY": 380, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_107", - "x": 130, - "y": 397, - "width": 12, - "height": 17, - "paddedX": 128, - "paddedY": 382, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_108", - "x": 146, - "y": 397, - "width": 12, - "height": 17, - "paddedX": 144, - "paddedY": 382, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_110", - "x": 228, - "y": 398, - "width": 8, - "height": 9, - "paddedX": 224, - "paddedY": 391, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_109", - "x": 202, - "y": 398, - "width": 12, - "height": 10, - "paddedX": 200, - "paddedY": 392, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_101", - "x": 1, - "y": 395, - "width": 30, - "height": 16, - "paddedX": 0, - "paddedY": 395, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_102", - "x": 33, - "y": 395, - "width": 30, - "height": 16, - "paddedX": 32, - "paddedY": 395, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_103", - "x": 65, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 64, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_104", - "x": 81, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 80, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_105", - "x": 97, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 96, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_106", - "x": 113, - "y": 395, - "width": 14, - "height": 16, - "paddedX": 112, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_111", - "x": 244, - "y": 404, - "width": 9, - "height": 8, - "paddedX": 241, - "paddedY": 396, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_113", - "x": 188, - "y": 417, - "width": 4, - "height": 5, - "paddedX": 182, - "paddedY": 406, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_122", - "x": 5, - "y": 426, - "width": 22, - "height": 17, - "paddedX": 0, - "paddedY": 411, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_118", - "x": 35, - "y": 425, - "width": 26, - "height": 19, - "paddedX": 32, - "paddedY": 412, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_119", - "x": 67, - "y": 425, - "width": 26, - "height": 19, - "paddedX": 64, - "paddedY": 412, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_116", - "x": 231, - "y": 420, - "width": 8, - "height": 8, - "paddedX": 227, - "paddedY": 412, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_115", - "x": 242, - "y": 418, - "width": 12, - "height": 10, - "paddedX": 240, - "paddedY": 412, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_120", - "x": 97, - "y": 425, - "width": 30, - "height": 20, - "paddedX": 96, - "paddedY": 413, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_117", - "x": 182, - "y": 423, - "width": 8, - "height": 8, - "paddedX": 178, - "paddedY": 415, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_114", - "x": 193, - "y": 418, - "width": 14, - "height": 14, - "paddedX": 192, - "paddedY": 416, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_112", - "x": 208, - "y": 416, - "width": 7, - "height": 16, - "paddedX": 204, - "paddedY": 416, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_124", - "x": 147, - "y": 430, - "width": 4, - "height": 5, - "paddedX": 141, - "paddedY": 419, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_121", - "x": 152, - "y": 425, - "width": 16, - "height": 15, - "paddedX": 152, - "paddedY": 424, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_123", - "x": 135, - "y": 426, - "width": 7, - "height": 16, - "paddedX": 131, - "paddedY": 426, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_125", - "x": 183, - "y": 432, - "width": 18, - "height": 15, - "paddedX": 176, - "paddedY": 431, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_126", - "x": 216, - "y": 432, - "width": 16, - "height": 15, - "paddedX": 216, - "paddedY": 431, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_127", - "x": 242, - "y": 434, - "width": 13, - "height": 14, - "paddedX": 241, - "paddedY": 432, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_133", - "x": 51, - "y": 455, - "width": 10, - "height": 18, - "paddedX": 48, - "paddedY": 441, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_134", - "x": 1, - "y": 457, - "width": 30, - "height": 20, - "paddedX": 0, - "paddedY": 445, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_128", - "x": 66, - "y": 449, - "width": 13, - "height": 15, - "paddedX": 65, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_129", - "x": 82, - "y": 449, - "width": 13, - "height": 15, - "paddedX": 81, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_130", - "x": 99, - "y": 449, - "width": 11, - "height": 15, - "paddedX": 97, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_131", - "x": 115, - "y": 449, - "width": 11, - "height": 15, - "paddedX": 113, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_138", - "x": 130, - "y": 459, - "width": 13, - "height": 21, - "paddedX": 129, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_135", - "x": 146, - "y": 458, - "width": 13, - "height": 22, - "paddedX": 145, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 32 - }, - { - "id": "ASSET_136", - "x": 163, - "y": 458, - "width": 26, - "height": 22, - "paddedX": 160, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_137", - "x": 195, - "y": 458, - "width": 26, - "height": 22, - "paddedX": 192, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_139", - "x": 227, - "y": 460, - "width": 26, - "height": 20, - "paddedX": 224, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32 - }, - { - "id": "ASSET_132", - "x": 35, - "y": 455, - "width": 10, - "height": 16, - "paddedX": 32, - "paddedY": 455, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_140", - "x": 66, - "y": 465, - "width": 13, - "height": 15, - "paddedX": 65, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_141", - "x": 82, - "y": 465, - "width": 13, - "height": 15, - "paddedX": 81, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_142", - "x": 99, - "y": 465, - "width": 11, - "height": 15, - "paddedX": 97, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_143", - "x": 115, - "y": 465, - "width": 11, - "height": 15, - "paddedX": 113, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_144", - "x": 5, - "y": 482, - "width": 22, - "height": 12, - "paddedX": 0, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_145", - "x": 37, - "y": 482, - "width": 22, - "height": 12, - "paddedX": 32, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_146", - "x": 69, - "y": 482, - "width": 22, - "height": 12, - "paddedX": 64, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_147", - "x": 108, - "y": 482, - "width": 16, - "height": 13, - "paddedX": 108, - "paddedY": 479, - "paddedWidth": 16, - "paddedHeight": 16 - }, - { - "id": "ASSET_148", - "x": 5, - "y": 498, - "width": 22, - "height": 12, - "paddedX": 0, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_149", - "x": 37, - "y": 498, - "width": 22, - "height": 12, - "paddedX": 32, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_150", - "x": 69, - "y": 498, - "width": 22, - "height": 12, - "paddedX": 64, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16 - }, - { - "id": "ASSET_151", - "x": 108, - "y": 498, - "width": 16, - "height": 13, - "paddedX": 108, - "paddedY": 495, - "paddedWidth": 16, - "paddedHeight": 16 - } - ] -} \ No newline at end of file diff --git a/scripts/.tileset-working/tileset-metadata-draft.json b/scripts/.tileset-working/tileset-metadata-draft.json deleted file mode 100644 index 76915342..00000000 --- a/scripts/.tileset-working/tileset-metadata-draft.json +++ /dev/null @@ -1,4164 +0,0 @@ -{ - "version": 1, - "timestamp": "2026-02-09T18:37:03.833Z", - "sourceFile": "assets/office_tileset_16x16.png", - "tileset": { - "width": 256, - "height": 512 - }, - "backgroundColor": "#00000000", - "assets": [ - { - "id": "ASSET_0", - "paddedX": 16, - "paddedY": -10, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "DRESSER_WOOD_SM", - "label": "Small Wooden Dresser", - "category": "storage", - "footprintW": 3, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_1", - "paddedX": 64, - "paddedY": -10, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "CABINET_WOOD_SM", - "label": "Small Wooden Cabinet", - "category": "storage", - "footprintW": 3, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_4", - "paddedX": 128, - "paddedY": 0, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "SHELF_WALL_WOOD_SM", - "label": "Small Wooden Wall Shelf", - "category": "storage", - "footprintW": 3, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_6", - "paddedX": 0, - "paddedY": 22, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "CREDENZA_MODERN_LG", - "label": "Modern Credenza Large", - "category": "wall", - "footprintW": 3, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_7", - "paddedX": 80, - "paddedY": 22, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "DESK_WOOD_SM", - "label": "Small Wood Desk", - "category": "desks", - "footprintW": 2, - "footprintH": 2, - "isDesk": true, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_10", - "paddedX": 0, - "paddedY": 54, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "DRESSER_WOOD_SM", - "label": "Small Wood Dresser", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_11", - "paddedX": 0, - "paddedY": 86, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CABINET_WOOD_SM", - "label": "Small Wooden Cabinet", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_15", - "paddedX": 128, - "paddedY": 64, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "SHELF_WALL_SM", - "label": "Small Wall Shelf", - "category": "wall", - "footprintW": 3, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_17", - "paddedX": 176, - "paddedY": 88, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "BOOKSHELF_WOOD_SM", - "label": "Small Wooden Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_18", - "paddedX": 208, - "paddedY": 88, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "BOOKSHELF_SMALL_COLORFUL", - "label": "Small Colorful Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_25", - "paddedX": 128, - "paddedY": 136, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "BOOKSHELF_DOUBLE_TALL", - "label": "Double Tall Bookshelf", - "category": "storage", - "footprintW": 3, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_23", - "paddedX": 176, - "paddedY": 136, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "BOOKSHELF_WALL_SM", - "label": "Small Wall Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_24", - "paddedX": 208, - "paddedY": 136, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "BOOKSHELF_TALL", - "label": "Tall Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_26", - "paddedX": 240, - "paddedY": 144, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "POLE_WOOD_VERTICAL", - "label": "Wooden Pole", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_A", - "paddedX": 0, - "paddedY": 160, - "paddedWidth": 32, - "paddedHeight": 48, - "name": "WALL_PANEL_BEIGE", - "label": "Beige Wall Panel", - "category": "wall", - "footprintW": 2, - "footprintH": 3, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_27_B_A_A", - "paddedX": 128, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "BOOKSHELF_TALL", - "label": "Tall Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_A", - "paddedX": 144, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "BOOKSHELF_TALL_GLASS", - "label": "Tall Glass Display Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_B_A", - "paddedX": 160, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FRIDGE_STEEL_LG", - "label": "Large Steel Refrigerator", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_B_B_A", - "paddedX": 176, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "BOOKSHELF_TALL", - "label": "Tall Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_A_B_B_B_B", - "paddedX": 192, - "paddedY": 183, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "BOOKSHELF_WOOD_SM", - "label": "Small Wooden Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_0", - "paddedX": 128, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "DRESSER_WOOD_SM", - "label": "Small Wooden Dresser", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_1", - "paddedX": 144, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FILING_CABINET_SM", - "label": "Small Filing Cabinet", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_2", - "paddedX": 160, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CABINET_WOOD_SM", - "label": "Small Wooden Cabinet", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_3", - "paddedX": 176, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CABINET_TALL_GREY", - "label": "Tall Grey Cabinet", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_4", - "paddedX": 192, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FILING_CABINET_SM", - "label": "Small Filing Cabinet", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_27_B_B_0_5", - "paddedX": 208, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CABINET_TALL_BLUE", - "label": "Tall Blue Cabinet", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_28", - "paddedX": 192, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "BOOKSHELF_METAL_SM", - "label": "Small Metal Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_29", - "paddedX": 224, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "BOOKSHELF_TALL", - "label": "Tall Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_30", - "paddedX": 224, - "paddedY": 224, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PILLAR_STONE_TALL", - "label": "Tall Stone Pillar", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_32", - "paddedX": 0, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_OFFICE_CUSHIONED", - "label": "Cushioned Office Chair", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_38", - "paddedX": 16, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_CUSHION_SM", - "label": "Cushioned Chair", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_33", - "paddedX": 32, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOKSHELF_SM", - "label": "Small Bookshelf", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_34", - "paddedX": 48, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "SUITCASE_SM", - "label": "Small Suitcase", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_35", - "paddedX": 64, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "SERVER_RACK_SM", - "label": "Small Server Rack", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_39", - "paddedX": 80, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "SHELF_BOOKCASE_SM", - "label": "Small Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_36", - "paddedX": 96, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PHONE_DESK_BLACK", - "label": "Desk Phone Black", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_37", - "paddedX": 112, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "GLASS_WATER_SM", - "label": "Glass of Water", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_42", - "paddedX": 142, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WATER_COOLER", - "label": "Water Cooler", - "category": "misc", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_41_0_0", - "paddedX": 192, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FILING_CABINET_SM", - "label": "Small Filing Cabinet", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_41_0_1", - "paddedX": 208, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FILING_CABINET_SM", - "label": "Small Filing Cabinet", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_40", - "paddedX": 225, - "paddedY": 264, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "VENDING_MACHINE_SNACK", - "label": "Snack Vending Machine", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_44", - "paddedX": 130, - "paddedY": 278, - "paddedWidth": 16, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 14, - "y": 1 - }, - { - "x": 14, - "y": 2 - }, - { - "x": 14, - "y": 3 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 14, - "y": 7 - }, - { - "x": 14, - "y": 8 - }, - { - "x": 14, - "y": 9 - }, - { - "x": 14, - "y": 10 - }, - { - "x": 14, - "y": 11 - }, - { - "x": 14, - "y": 12 - }, - { - "x": 14, - "y": 13 - }, - { - "x": 14, - "y": 14 - }, - { - "x": 14, - "y": 15 - }, - { - "x": 15, - "y": 15 - }, - { - "x": 15, - "y": 14 - }, - { - "x": 15, - "y": 13 - }, - { - "x": 15, - "y": 12 - }, - { - "x": 15, - "y": 11 - }, - { - "x": 15, - "y": 10 - }, - { - "x": 15, - "y": 9 - }, - { - "x": 15, - "y": 8 - }, - { - "x": 15, - "y": 7 - }, - { - "x": 15, - "y": 6 - }, - { - "x": 15, - "y": 5 - }, - { - "x": 15, - "y": 4 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 14, - "y": 0 - } - ], - "name": "CAN_SODA_SM", - "label": "Soda Can", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_46", - "paddedX": 16, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "BOOKSHELF_TALL_WOOD", - "label": "Tall Wooden Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_47", - "paddedX": 32, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CABINET_TALL_BLUE", - "label": "Tall Blue Cabinet", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_49", - "paddedX": 0, - "paddedY": 290, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BARREL_WOOD_SM", - "label": "Small Wooden Barrel", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_50_0_0", - "paddedX": 64, - "paddedY": 299, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "CREDENZA_WOOD_SM", - "label": "Small Wooden Credenza", - "category": "storage", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_50_0_1", - "paddedX": 96, - "paddedY": 299, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "AC_UNIT_WALL_SM", - "label": "Wall Mounted Air Conditioner", - "category": "electronics", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_51", - "paddedX": 225, - "paddedY": 299, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BALL_SOCCER", - "label": "Soccer Ball", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_54", - "paddedX": 125, - "paddedY": 303, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 1, - "y": 10 - }, - { - "x": 1, - "y": 9 - }, - { - "x": 1, - "y": 8 - }, - { - "x": 1, - "y": 7 - }, - { - "x": 1, - "y": 6 - }, - { - "x": 1, - "y": 5 - }, - { - "x": 1, - "y": 4 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 0, - "y": 4 - }, - { - "x": 0, - "y": 5 - }, - { - "x": 0, - "y": 6 - }, - { - "x": 0, - "y": 7 - }, - { - "x": 0, - "y": 8 - }, - { - "x": 0, - "y": 9 - }, - { - "x": 0, - "y": 10 - }, - { - "x": 2, - "y": 7 - }, - { - "x": 2, - "y": 6 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 2, - "y": 4 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 2, - "y": 0 - } - ], - "name": "PHONE_MOBILE_SM", - "label": "Mobile Phone", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_55", - "paddedX": 161, - "paddedY": 303, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WATER_COOLER", - "label": "Water Cooler", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_61", - "paddedX": 192, - "paddedY": 315, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MIC_STUDIO_STAND", - "label": "Studio Microphone On Stand", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_63", - "paddedX": 32, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WINDOW_TRIPLE_WOOD", - "label": "Triple Pane Wood Window", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_64", - "paddedX": 64, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WINDOW_DOUBLE_BLUE", - "label": "Double Window Blue", - "category": "wall", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_65", - "paddedX": 96, - "paddedY": 330, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WINDOW_DOUBLE_SM", - "label": "Small Double Window", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_70", - "paddedX": 131, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PLANT_SMALL_POTTED", - "label": "Small Potted Plant", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_71", - "paddedX": 147, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOK_SINGLE_BLUE", - "label": "Blue Book", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_72", - "paddedX": 163, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOK_SINGLE_SM", - "label": "Small Book", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_73", - "paddedX": 179, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 14, - "y": 3 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 15, - "y": 7 - }, - { - "x": 15, - "y": 8 - }, - { - "x": 15, - "y": 9 - }, - { - "x": 15, - "y": 10 - }, - { - "x": 15, - "y": 11 - }, - { - "x": 15, - "y": 12 - }, - { - "x": 14, - "y": 10 - }, - { - "x": 14, - "y": 9 - }, - { - "x": 14, - "y": 8 - }, - { - "x": 14, - "y": 7 - }, - { - "x": 15, - "y": 5 - }, - { - "x": 15, - "y": 6 - }, - { - "x": 15, - "y": 4 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 14, - "y": 11 - } - ], - "name": "CRATE_WOOD_SM", - "label": "Small Wooden Crate", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_78", - "paddedX": 192, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_DESKTOP_SM", - "label": "Desktop Monitor Small", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_79", - "paddedX": 224, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_DESKTOP_SM", - "label": "Small Desktop Monitor", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_83", - "paddedX": 0, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CLOCK_WALL_ROUND", - "label": "Round Wall Clock", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_84", - "paddedX": 16, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "SIGN_NO_ENTRY", - "label": "No Entry Sign", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_87", - "paddedX": 64, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CLOCK_WALL_ROUND_SM", - "label": "Small Round Wall Clock", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_74", - "paddedX": 128, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_CRT_SM", - "label": "Small CRT Monitor", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_75", - "paddedX": 128, - "paddedY": 360, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "PHONE_MOBILE_SM", - "label": "Mobile Phone", - "category": "electronics", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_76", - "paddedX": 160, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_CRT_SM", - "label": "Small CRT Monitor", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_77", - "paddedX": 160, - "paddedY": 360, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "COMPUTER_DESKTOP_MONITOR", - "label": "Desktop Computer Monitor", - "category": "electronics", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_80", - "paddedX": 80, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOKSHELF_WOOD_SM", - "label": "Small Wooden Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_81", - "paddedX": 96, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOKSHELF_TALL", - "label": "Tall Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_82", - "paddedX": 112, - "paddedY": 362, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "WINDOW_FRAME_SM", - "label": "Small Window Frame", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_90", - "paddedX": 192, - "paddedY": 352, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "COMPUTER_DESKTOP_MONITOR", - "label": "Desktop Computer Monitor", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_92", - "paddedX": 224, - "paddedY": 352, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "COMPUTER_DESKTOP_SM", - "label": "Desktop Computer", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_94", - "paddedX": 128, - "paddedY": 353, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "COMPUTER_DESKTOP_CRT", - "label": "Desktop Computer With CRT Monitor", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_95", - "paddedX": 160, - "paddedY": 353, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "COMPUTER_DESKTOP_TOWER", - "label": "Desktop Computer With Monitor", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_98", - "paddedX": 159, - "paddedY": 370, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 4, - "y": 12 - }, - { - "x": 5, - "y": 12 - }, - { - "x": 6, - "y": 12 - }, - { - "x": 7, - "y": 12 - }, - { - "x": 8, - "y": 12 - }, - { - "x": 9, - "y": 12 - }, - { - "x": 10, - "y": 12 - }, - { - "x": 11, - "y": 12 - }, - { - "x": 12, - "y": 12 - }, - { - "x": 13, - "y": 12 - }, - { - "x": 14, - "y": 11 - }, - { - "x": 14, - "y": 10 - }, - { - "x": 14, - "y": 9 - }, - { - "x": 13, - "y": 10 - }, - { - "x": 13, - "y": 11 - }, - { - "x": 12, - "y": 11 - }, - { - "x": 12, - "y": 10 - }, - { - "x": 13, - "y": 9 - }, - { - "x": 11, - "y": 11 - }, - { - "x": 10, - "y": 11 - }, - { - "x": 10, - "y": 10 - }, - { - "x": 11, - "y": 9 - }, - { - "x": 12, - "y": 8 - }, - { - "x": 13, - "y": 8 - }, - { - "x": 12, - "y": 9 - }, - { - "x": 11, - "y": 10 - }, - { - "x": 9, - "y": 11 - }, - { - "x": 8, - "y": 11 - }, - { - "x": 9, - "y": 10 - }, - { - "x": 10, - "y": 9 - }, - { - "x": 8, - "y": 10 - }, - { - "x": 7, - "y": 11 - }, - { - "x": 6, - "y": 11 - }, - { - "x": 6, - "y": 10 - }, - { - "x": 7, - "y": 10 - }, - { - "x": 8, - "y": 9 - }, - { - "x": 9, - "y": 9 - }, - { - "x": 5, - "y": 11 - }, - { - "x": 4, - "y": 11 - }, - { - "x": 3, - "y": 11 - }, - { - "x": 3, - "y": 10 - }, - { - "x": 4, - "y": 9 - }, - { - "x": 5, - "y": 9 - }, - { - "x": 5, - "y": 8 - }, - { - "x": 6, - "y": 8 - }, - { - "x": 6, - "y": 9 - }, - { - "x": 4, - "y": 8 - }, - { - "x": 5, - "y": 10 - }, - { - "x": 4, - "y": 7 - }, - { - "x": 7, - "y": 9 - }, - { - "x": 3, - "y": 9 - }, - { - "x": 8, - "y": 8 - }, - { - "x": 9, - "y": 8 - }, - { - "x": 10, - "y": 8 - }, - { - "x": 11, - "y": 8 - }, - { - "x": 7, - "y": 8 - }, - { - "x": 4, - "y": 10 - }, - { - "x": 8, - "y": 7 - }, - { - "x": 9, - "y": 7 - }, - { - "x": 9, - "y": 6 - }, - { - "x": 8, - "y": 6 - }, - { - "x": 7, - "y": 7 - }, - { - "x": 6, - "y": 7 - }, - { - "x": 5, - "y": 7 - }, - { - "x": 3, - "y": 8 - }, - { - "x": 7, - "y": 6 - }, - { - "x": 7, - "y": 5 - }, - { - "x": 6, - "y": 5 - }, - { - "x": 8, - "y": 5 - }, - { - "x": 9, - "y": 5 - }, - { - "x": 10, - "y": 6 - }, - { - "x": 11, - "y": 6 - }, - { - "x": 10, - "y": 5 - }, - { - "x": 5, - "y": 5 - }, - { - "x": 4, - "y": 5 - }, - { - "x": 3, - "y": 5 - }, - { - "x": 3, - "y": 6 - }, - { - "x": 4, - "y": 6 - }, - { - "x": 5, - "y": 6 - }, - { - "x": 6, - "y": 6 - }, - { - "x": 11, - "y": 5 - }, - { - "x": 12, - "y": 6 - }, - { - "x": 12, - "y": 5 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 4, - "y": 4 - }, - { - "x": 5, - "y": 4 - }, - { - "x": 6, - "y": 4 - }, - { - "x": 7, - "y": 4 - }, - { - "x": 8, - "y": 4 - }, - { - "x": 9, - "y": 4 - }, - { - "x": 10, - "y": 4 - }, - { - "x": 11, - "y": 4 - }, - { - "x": 12, - "y": 4 - }, - { - "x": 13, - "y": 4 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 13, - "y": 3 - }, - { - "x": 14, - "y": 3 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 12, - "y": 3 - }, - { - "x": 11, - "y": 3 - }, - { - "x": 10, - "y": 3 - }, - { - "x": 9, - "y": 3 - }, - { - "x": 8, - "y": 3 - }, - { - "x": 7, - "y": 3 - }, - { - "x": 6, - "y": 3 - }, - { - "x": 5, - "y": 3 - }, - { - "x": 4, - "y": 3 - }, - { - "x": 3, - "y": 3 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 4, - "y": 2 - }, - { - "x": 3, - "y": 4 - }, - { - "x": 3, - "y": 2 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 6, - "y": 2 - }, - { - "x": 7, - "y": 2 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 8, - "y": 2 - }, - { - "x": 9, - "y": 2 - }, - { - "x": 10, - "y": 2 - }, - { - "x": 11, - "y": 2 - }, - { - "x": 12, - "y": 2 - }, - { - "x": 13, - "y": 2 - }, - { - "x": 14, - "y": 2 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 13, - "y": 1 - }, - { - "x": 14, - "y": 1 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 13, - "y": 6 - }, - { - "x": 13, - "y": 7 - }, - { - "x": 13, - "y": 5 - }, - { - "x": 12, - "y": 7 - }, - { - "x": 11, - "y": 7 - }, - { - "x": 10, - "y": 7 - }, - { - "x": 5, - "y": 13 - }, - { - "x": 6, - "y": 13 - }, - { - "x": 7, - "y": 13 - }, - { - "x": 8, - "y": 13 - }, - { - "x": 9, - "y": 13 - }, - { - "x": 10, - "y": 13 - }, - { - "x": 11, - "y": 13 - }, - { - "x": 9, - "y": 14 - }, - { - "x": 8, - "y": 14 - }, - { - "x": 7, - "y": 14 - }, - { - "x": 6, - "y": 14 - }, - { - "x": 5, - "y": 14 - }, - { - "x": 4, - "y": 13 - }, - { - "x": 3, - "y": 13 - }, - { - "x": 2, - "y": 13 - }, - { - "x": 2, - "y": 12 - }, - { - "x": 3, - "y": 12 - }, - { - "x": 14, - "y": 12 - }, - { - "x": 15, - "y": 12 - }, - { - "x": 15, - "y": 11 - }, - { - "x": 15, - "y": 10 - }, - { - "x": 15, - "y": 9 - }, - { - "x": 14, - "y": 8 - }, - { - "x": 15, - "y": 8 - }, - { - "x": 13, - "y": 13 - }, - { - "x": 12, - "y": 13 - }, - { - "x": 12, - "y": 14 - }, - { - "x": 11, - "y": 14 - } - ], - "name": "FOLDER_FILE_BLUE", - "label": "Blue File Folder", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_99", - "paddedX": 177, - "paddedY": 370, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 2, - "y": 9 - }, - { - "x": 1, - "y": 10 - }, - { - "x": 1, - "y": 11 - }, - { - "x": 0, - "y": 11 - }, - { - "x": 0, - "y": 12 - }, - { - "x": 0, - "y": 10 - }, - { - "x": 0, - "y": 9 - }, - { - "x": 0, - "y": 8 - }, - { - "x": 1, - "y": 7 - }, - { - "x": 1, - "y": 8 - }, - { - "x": 1, - "y": 9 - }, - { - "x": 1, - "y": 12 - }, - { - "x": 0, - "y": 7 - }, - { - "x": 0, - "y": 6 - }, - { - "x": 0, - "y": 5 - }, - { - "x": 1, - "y": 5 - }, - { - "x": 2, - "y": 6 - }, - { - "x": 3, - "y": 6 - }, - { - "x": 4, - "y": 6 - }, - { - "x": 5, - "y": 6 - }, - { - "x": 5, - "y": 5 - }, - { - "x": 5, - "y": 4 - }, - { - "x": 5, - "y": 3 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 4, - "y": 3 - }, - { - "x": 4, - "y": 4 - }, - { - "x": 3, - "y": 4 - }, - { - "x": 3, - "y": 5 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 1, - "y": 6 - }, - { - "x": 4, - "y": 5 - }, - { - "x": 4, - "y": 2 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 3, - "y": 2 - }, - { - "x": 3, - "y": 3 - }, - { - "x": 2, - "y": 4 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 1, - "y": 4 - }, - { - "x": 0, - "y": 4 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 13, - "y": 1 - }, - { - "x": 14, - "y": 1 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 6, - "y": 1 - } - ], - "name": "FOLDER_FILE_BLUE", - "label": "Blue File Folder", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_100", - "paddedX": 240, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "WINDOW_SMALL_SQ", - "label": "Small Square Window", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_107", - "paddedX": 128, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 3, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 2, - "y": 0 - } - ], - "name": "MONITOR_CRT_SM", - "label": "Small CRT Monitor", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_108", - "paddedX": 144, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 2, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - } - ], - "name": "MONITOR_DESKTOP_SM", - "label": "Small Desktop Monitor", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_110", - "paddedX": 224, - "paddedY": 394, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOK_OPEN_SM", - "label": "Open Book Small", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_109", - "paddedX": 200, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "FRAME_PICTURE_SM", - "label": "Small Picture Frame", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_101", - "paddedX": 0, - "paddedY": 395, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "PAINTING_LANDSCAPE_SM", - "label": "Small Landscape Painting", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_102", - "paddedX": 32, - "paddedY": 395, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WINDOW_LANDSCAPE_SM", - "label": "Small Landscape Window", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_103", - "paddedX": 64, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PICTURE_FRAME_LANDSCAPE_SM", - "label": "Small Landscape Picture Frame", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_104", - "paddedX": 80, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOKSHELF_WOOD_SM", - "label": "Small Wooden Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_105", - "paddedX": 96, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOKSHELF_WOOD_SM", - "label": "Small Wooden Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_106", - "paddedX": 112, - "paddedY": 395, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CLIPBOARD_DOCUMENT", - "label": "Clipboard With Document", - "category": "misc", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_111", - "paddedX": 241, - "paddedY": 400, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "WINDOW_SMALL", - "label": "Small Window", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_122", - "paddedX": 0, - "paddedY": 418, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CHALKBOARD_WALL_SM", - "label": "Small Wall Chalkboard", - "category": "decor", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_118", - "paddedX": 32, - "paddedY": 418, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "MONITOR_DESKTOP_SM", - "label": "Small Desktop Monitor", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_119", - "paddedX": 64, - "paddedY": 418, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 5, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 25, - "y": 31 - }, - { - "x": 26, - "y": 31 - }, - { - "x": 27, - "y": 31 - }, - { - "x": 28, - "y": 31 - }, - { - "x": 4, - "y": 31 - } - ], - "name": "CERTIFICATE_WALL_FRAME", - "label": "Wall Certificate Frame", - "category": "decor", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_120", - "paddedX": 96, - "paddedY": 419, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 10, - "y": 30 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 9, - "y": 30 - }, - { - "x": 11, - "y": 30 - }, - { - "x": 12, - "y": 30 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 30 - }, - { - "x": 18, - "y": 30 - }, - { - "x": 19, - "y": 30 - }, - { - "x": 20, - "y": 30 - }, - { - "x": 21, - "y": 30 - }, - { - "x": 22, - "y": 30 - }, - { - "x": 23, - "y": 30 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 17, - "y": 31 - } - ], - "name": "MONITOR_DESKTOP_SM", - "label": "Small Desktop Monitor", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_114", - "paddedX": 192, - "paddedY": 416, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "TRASH_CAN_METAL_SM", - "label": "Small Metal Trash Can", - "category": "misc", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_112", - "paddedX": 192, - "paddedY": 416, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "FILING_CABINET_SM", - "label": "Small Filing Cabinet", - "category": "storage", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_121", - "paddedX": 152, - "paddedY": 424, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "WINDOW_CURTAIN_SM", - "label": "Small Window with Curtain", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_123", - "paddedX": 131, - "paddedY": 426, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PHONE_MOBILE_SM", - "label": "Mobile Phone", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_125", - "paddedX": 176, - "paddedY": 431, - "paddedWidth": 32, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 20, - "y": 1 - }, - { - "x": 21, - "y": 1 - }, - { - "x": 22, - "y": 1 - }, - { - "x": 23, - "y": 1 - }, - { - "x": 24, - "y": 1 - }, - { - "x": 25, - "y": 1 - }, - { - "x": 26, - "y": 1 - }, - { - "x": 27, - "y": 1 - }, - { - "x": 27, - "y": 0 - }, - { - "x": 26, - "y": 0 - }, - { - "x": 25, - "y": 0 - }, - { - "x": 24, - "y": 0 - }, - { - "x": 23, - "y": 0 - }, - { - "x": 22, - "y": 0 - }, - { - "x": 21, - "y": 0 - }, - { - "x": 20, - "y": 0 - } - ], - "name": "PRINTER_OFFICE_SM", - "label": "Office Printer Small", - "category": "electronics", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_126", - "paddedX": 216, - "paddedY": 431, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PRINTER_DESKTOP", - "label": "Desktop Printer", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_127", - "paddedX": 241, - "paddedY": 432, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CABINET_WOOD_SM", - "label": "Small Wooden Cabinet", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_133_0_0", - "paddedX": 48, - "paddedY": 443, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 6, - "y": 0 - } - ], - "name": "PLANT_CACTUS_POT_SM", - "label": "Small Potted Cactus", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_133_1_0", - "paddedX": 49, - "paddedY": 463, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CUP_COFFEE_SM", - "label": "Small Coffee Cup", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_134", - "paddedX": 0, - "paddedY": 451, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 4, - "y": 31 - }, - { - "x": 5, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 25, - "y": 31 - }, - { - "x": 26, - "y": 31 - }, - { - "x": 27, - "y": 31 - } - ], - "name": "CERTIFICATE_WALL_SM", - "label": "Wall Certificate", - "category": "decor", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_128", - "paddedX": 65, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PLANT_CACTUS_SM", - "label": "Small Cactus Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_129", - "paddedX": 81, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PLANT_CACTUS_SM", - "label": "Small Cactus Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_130", - "paddedX": 97, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PLANT_CACTUS_SM", - "label": "Small Potted Cactus", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_131", - "paddedX": 113, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PLANT_CACTUS_POT_SM", - "label": "Small Potted Cactus", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_138", - "paddedX": 129, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CABINET_WOOD_SM", - "label": "Small Wooden Cabinet", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_135", - "paddedX": 145, - "paddedY": 448, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "BOOKSHELF_WOOD_SM", - "label": "Small Wooden Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_136", - "paddedX": 160, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "SHELF_WOOD_SM", - "label": "Small Wooden Shelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_137", - "paddedX": 192, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "SHELF_WOOD_SM", - "label": "Small Wooden Shelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_139", - "paddedX": 224, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CRATES_STACKED_WOOD", - "label": "Stacked Wooden Crates", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_132", - "paddedX": 32, - "paddedY": 455, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CACTUS_POT_SM", - "label": "Small Potted Cactus", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_140", - "paddedX": 65, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CACTUS_PLANT_POT_SM", - "label": "Small Potted Cactus", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_141", - "paddedX": 81, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CACTUS_PLANT_POT_SM", - "label": "Small Potted Cactus", - "category": "wall", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_142", - "paddedX": 97, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PLANT_CACTUS_SM", - "label": "Small Cactus Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_143", - "paddedX": 113, - "paddedY": 464, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "PLANT_CACTUS_SM", - "label": "Small Potted Cactus", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_144", - "paddedX": 0, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WHITEBOARD_SM", - "label": "Small Whiteboard", - "category": "wall", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_145", - "paddedX": 32, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WHITEBOARD_WALL_SM", - "label": "Small Wall Whiteboard", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_146", - "paddedX": 64, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 2 - }, - { - "x": 7, - "y": 2 - }, - { - "x": 8, - "y": 2 - }, - { - "x": 9, - "y": 2 - }, - { - "x": 10, - "y": 2 - }, - { - "x": 11, - "y": 2 - }, - { - "x": 12, - "y": 2 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 13, - "y": 1 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 14, - "y": 1 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 16, - "y": 1 - }, - { - "x": 17, - "y": 1 - }, - { - "x": 18, - "y": 0 - }, - { - "x": 19, - "y": 0 - }, - { - "x": 20, - "y": 0 - }, - { - "x": 21, - "y": 0 - }, - { - "x": 22, - "y": 0 - }, - { - "x": 21, - "y": 1 - }, - { - "x": 20, - "y": 1 - }, - { - "x": 22, - "y": 1 - }, - { - "x": 23, - "y": 1 - }, - { - "x": 24, - "y": 1 - }, - { - "x": 25, - "y": 1 - }, - { - "x": 26, - "y": 1 - }, - { - "x": 25, - "y": 0 - }, - { - "x": 24, - "y": 0 - }, - { - "x": 23, - "y": 0 - }, - { - "x": 27, - "y": 1 - }, - { - "x": 28, - "y": 1 - }, - { - "x": 29, - "y": 1 - }, - { - "x": 28, - "y": 0 - }, - { - "x": 27, - "y": 0 - }, - { - "x": 26, - "y": 0 - } - ], - "name": "WALL_DECOR_PLATES_ROW", - "label": "Decorative Wall Plates Row", - "category": "wall", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_147", - "paddedX": 108, - "paddedY": 479, - "paddedWidth": 16, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 0, - "y": 1 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 15, - "y": 0 - } - ], - "name": "MONITOR_CRT_SM", - "label": "Small CRT Monitor", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_148", - "paddedX": 0, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "CHESS_BOARD_SM", - "label": "Chess Board", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_149", - "paddedX": 32, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "CHESS_BOARD_SM", - "label": "Chess Board", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - }, - { - "id": "ASSET_150", - "paddedX": 64, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "PLANTER_BOX_TRIPLE", - "label": "Triple Planter Box", - "category": "wall", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": true, - "discard": false - }, - { - "id": "ASSET_151", - "paddedX": 108, - "paddedY": 495, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "WHITEBOARD_SM", - "label": "Small Whiteboard", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false - } - ] -} diff --git a/scripts/.tileset-working/tileset-metadata-final.json b/scripts/.tileset-working/tileset-metadata-final.json deleted file mode 100644 index 7fa2b630..00000000 --- a/scripts/.tileset-working/tileset-metadata-final.json +++ /dev/null @@ -1,4622 +0,0 @@ -{ - "version": 1, - "timestamp": "2026-02-16T22:28:09.674Z", - "sourceFile": "assets/office_tileset_16x16.png", - "tileset": { - "width": 256, - "height": 512 - }, - "backgroundColor": "#00000000", - "assets": [ - { - "id": "ASSET_4", - "paddedX": 128, - "paddedY": 0, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "COUNTER_WOOD_MD", - "label": "Solid Wooden Counter", - "category": "desks", - "footprintW": 3, - "footprintH": 2, - "isDesk": true, - "colorEditable": true, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_6", - "paddedX": 0, - "paddedY": 22, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "DESK_MODERN_SM", - "label": "Modern Desk Small", - "category": "desks", - "footprintW": 3, - "footprintH": 2, - "isDesk": true, - "colorEditable": true, - "discard": true, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_7", - "paddedX": 80, - "paddedY": 23, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "COUNTER_WHITE_SM", - "label": "Small White Counter", - "category": "desks", - "footprintW": 2, - "footprintH": 2, - "isDesk": true, - "colorEditable": true, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_10", - "paddedX": 0, - "paddedY": 54, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "TABLE_WOOD_SM", - "label": "Small Wooden Table", - "category": "desks", - "footprintW": 2, - "footprintH": 2, - "isDesk": true, - "colorEditable": false, - "discard": true, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_11", - "paddedX": 0, - "paddedY": 86, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "TABLE_PLAIN_WOOD_SM", - "label": "Small Plain Wooden Table", - "category": "desks", - "footprintW": 2, - "footprintH": 2, - "isDesk": true, - "colorEditable": false, - "discard": true, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_15", - "paddedX": 128, - "paddedY": 64, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "COUNTER_PLASTIC_SM", - "label": "Small Plastic Counter", - "category": "desks", - "footprintW": 3, - "footprintH": 2, - "isDesk": true, - "colorEditable": true, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_17", - "paddedX": 112, - "paddedY": 512, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "WOODEN_BOOKSHELF_SMALL", - "label": "Small Wooden Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_18", - "paddedX": 144, - "paddedY": 512, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "FULL_WOODEN_BOOKSHELF_SMALL", - "label": "Full Small Wooden Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_25", - "paddedX": 128, - "paddedY": 136, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "BOOKSHELF_DOUBLE", - "label": "Double Bookshelf", - "category": "storage", - "footprintW": 3, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_23", - "paddedX": 112, - "paddedY": 560, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "WHITE_BOOKSHELF_1", - "label": "White Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_24", - "paddedX": 144, - "paddedY": 560, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "FULL_WHITE_BOOKSHELF_1", - "label": "Full Small White Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_26", - "paddedX": 240, - "paddedY": 144, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "DOOR_WOOD_VERTICAL", - "label": "Wooden Door - Vertical", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "DOOR_WOOD", - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false, - "orientation": "front" - }, - { - "id": "ASSET_27_A", - "paddedX": 16, - "paddedY": 528, - "paddedWidth": 32, - "paddedHeight": 64, - "name": "TABLE_WOOD_LG", - "label": "Large Table", - "category": "desks", - "footprintW": 2, - "footprintH": 4, - "isDesk": true, - "colorEditable": true, - "discard": false, - "partOfGroup": true, - "groupId": "TABLE_LG", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1, - "orientation": "front" - }, - { - "id": "ASSET_27_B_A_A", - "paddedX": 128, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "BOOKSHELF_TALL", - "label": "Tall Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_27_B_A_B_A", - "paddedX": 144, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FULL_BOOKSHELF_TALL", - "label": "Full Tall Bookshelf", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_27_B_A_B_B_A", - "paddedX": 160, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CABINET_TALL", - "label": "Tall Cabinet", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_27_B_A_B_B_B_A", - "paddedX": 176, - "paddedY": 183, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FULL_CABINET_TALL", - "label": "Full Tall Cabinet", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_27_B_B_0_0", - "paddedX": 128, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "DOOR_WOOD_1", - "label": "Wooden Door 1", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": true, - "discard": true, - "partOfGroup": true, - "groupId": "DOOR_WOOD", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_27_B_B_0_1", - "paddedX": 144, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "DOOR_WHITE_1", - "label": "White Door 1", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "DOOR_WHITE", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_27_B_B_0_2", - "paddedX": 160, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "DOOR_WOOD_2", - "label": "Wooden Door 2", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": true, - "discard": true, - "partOfGroup": true, - "groupId": "DOOR_WOOD", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_27_B_B_0_3", - "paddedX": 176, - "paddedY": 221, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "DOOR_WHITE_2", - "label": "White Door 2", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "DOOR_WHITE", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_28", - "paddedX": 192, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "WHITE_BOOKSHELF_2", - "label": "White Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_29", - "paddedX": 224, - "paddedY": 184, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "FULL_WHITE_BOOKSHELF_2", - "label": "Full White Bookshelf", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_30", - "paddedX": 224, - "paddedY": 224, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "DOOR_WHITE_VERTICAL", - "label": "White Door - Vertical", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "DOOR_WHITE", - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false, - "orientation": "front" - }, - { - "id": "ASSET_32", - "paddedX": 0, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_CUSHIONED_FRONT", - "label": "Cushioned Chair - Front", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_38", - "paddedX": 16, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_CUSHIONED_BACK", - "label": "Cushioned Chair - Back", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_33", - "paddedX": 32, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_CUSHIONED_RIGHT", - "label": "Cushioned Chair - Right", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_34", - "paddedX": 48, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_CUSHIONED_LEFT", - "label": "Cushioned Chair - Left", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_35", - "paddedX": 64, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_ROTATING_FRONT", - "label": "Rotating Chair - Front", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "ROTATING_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_39", - "paddedX": 80, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_ROTATING_BACK", - "label": "Rotating Chair - Back", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "ROTATING_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_36", - "paddedX": 96, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_ROTATING_RIGHT", - "label": "Rotating Chair - Right", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "ROTATING_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_37", - "paddedX": 112, - "paddedY": 258, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CHAIR_ROTATING_LEFT", - "label": "Rotating Chair - Left", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "ROTATING_CHAIR", - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_42", - "paddedX": 142, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WATER_COOLER", - "label": "Water Cooler", - "category": "misc", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_41_0_1", - "paddedX": 208, - "paddedY": 264, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "FRIDGE", - "label": "Fridge", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_40", - "paddedX": 225, - "paddedY": 264, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "VENDING_MACHINE", - "label": "Snack Vending Machine", - "category": "misc", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_44", - "paddedX": 130, - "paddedY": 278, - "paddedWidth": 16, - "paddedHeight": 16, - "erasedPixels": [ - { - "x": 14, - "y": 1 - }, - { - "x": 14, - "y": 2 - }, - { - "x": 14, - "y": 3 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 14, - "y": 7 - }, - { - "x": 14, - "y": 8 - }, - { - "x": 14, - "y": 9 - }, - { - "x": 14, - "y": 10 - }, - { - "x": 14, - "y": 11 - }, - { - "x": 14, - "y": 12 - }, - { - "x": 14, - "y": 13 - }, - { - "x": 14, - "y": 14 - }, - { - "x": 14, - "y": 15 - }, - { - "x": 15, - "y": 15 - }, - { - "x": 15, - "y": 14 - }, - { - "x": 15, - "y": 13 - }, - { - "x": 15, - "y": 12 - }, - { - "x": 15, - "y": 11 - }, - { - "x": 15, - "y": 10 - }, - { - "x": 15, - "y": 9 - }, - { - "x": 15, - "y": 8 - }, - { - "x": 15, - "y": 7 - }, - { - "x": 15, - "y": 6 - }, - { - "x": 15, - "y": 5 - }, - { - "x": 15, - "y": 4 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 14, - "y": 0 - } - ], - "name": "BIN", - "label": "Trash Bin", - "category": "misc", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_46", - "paddedX": 16, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "TABLE_WOOD_VERTICAL", - "label": "Wooden Table - Vertical", - "category": "desks", - "footprintW": 1, - "footprintH": 2, - "isDesk": true, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_47", - "paddedX": 32, - "paddedY": 288, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "TABLE_WHITE_VERTICAL", - "label": "White Table - Vertical", - "category": "desks", - "footprintW": 1, - "footprintH": 2, - "isDesk": true, - "colorEditable": false, - "discard": true, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_49", - "paddedX": 0, - "paddedY": 290, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "STOOL", - "label": "Small Wooden Stool", - "category": "chairs", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_50_0_0", - "paddedX": 64, - "paddedY": 299, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WOOD_COFEE_TABLE", - "label": "Wooden Coffee Table", - "category": "desks", - "footprintW": 2, - "footprintH": 1, - "isDesk": true, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_50_0_1", - "paddedX": 96, - "paddedY": 299, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "WHITE_COFEE_TABLE", - "label": "White Cofee Table", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_51", - "paddedX": 225, - "paddedY": 299, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "COFFEE_MUG", - "label": "Coffee Mug", - "category": "misc", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true, - "backgroundTiles": 0, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_54", - "paddedX": 125, - "paddedY": 303, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 1, - "y": 10 - }, - { - "x": 1, - "y": 9 - }, - { - "x": 1, - "y": 8 - }, - { - "x": 1, - "y": 7 - }, - { - "x": 1, - "y": 6 - }, - { - "x": 1, - "y": 5 - }, - { - "x": 1, - "y": 4 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 0, - "y": 4 - }, - { - "x": 0, - "y": 5 - }, - { - "x": 0, - "y": 6 - }, - { - "x": 0, - "y": 7 - }, - { - "x": 0, - "y": 8 - }, - { - "x": 0, - "y": 9 - }, - { - "x": 0, - "y": 10 - }, - { - "x": 2, - "y": 7 - }, - { - "x": 2, - "y": 6 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 2, - "y": 4 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 2, - "y": 0 - } - ], - "name": "COFFEE_MACHINE_MUG", - "label": "Coffe Machine + Mug", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_55", - "paddedX": 161, - "paddedY": 309, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "COFFEE_MACHINE", - "label": "Coffee Machine", - "category": "misc", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "erasedPixels": [ - { - "x": 4, - "y": 31 - }, - { - "x": 5, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 3, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 31 - } - ], - "canPlaceOnWalls": false - }, - { - "id": "ASSET_61", - "paddedX": 192, - "paddedY": 304, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "TELEPHONE", - "label": "Telephone", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_63", - "paddedX": 32, - "paddedY": 319, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "WINDOW_DOUBLE_WOOD", - "label": "Double Pane Wood Window", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_64", - "paddedX": 64, - "paddedY": 319, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "WINDOW_DOUBLE_WHITE", - "label": "Double Window White", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_65", - "paddedX": 96, - "paddedY": 319, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "WINDOW_DOUBLE_WHITE_2", - "label": "White Double Window", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_71", - "paddedX": 147, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOK_SINGLE_BLUE", - "label": "Small Book", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true - }, - { - "id": "ASSET_72", - "paddedX": 163, - "paddedY": 336, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "BOOK_SINGLE_RED", - "label": "Small Book", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true - }, - { - "id": "ASSET_78", - "paddedX": 192, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_FRONT_OFF", - "label": "Monitor - Front - Off", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "MONITOR", - "canPlaceOnSurfaces": true - }, - { - "id": "ASSET_79", - "paddedX": 224, - "paddedY": 359, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_FRONT_ON", - "label": "Monitor - Front - On", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "MONITOR", - "canPlaceOnSurfaces": true - }, - { - "id": "ASSET_83", - "paddedX": 0, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CLOCK_WALL_WHITE", - "label": "White Wall Clock", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_84", - "paddedX": 15, - "paddedY": 352, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CLOCK_WALL_COLOR", - "label": "Colorful Wall Clock", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_87", - "paddedX": 64, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CLOCK_WALL_BLACK", - "label": "Black Wall Clock", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_74", - "paddedX": 128, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_CRT_OFF", - "label": "CRT Monitor - Off", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "CRT_MONITOR", - "canPlaceOnSurfaces": true - }, - { - "id": "ASSET_75", - "paddedX": 128, - "paddedY": 360, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "COMPUTER_FRONT_OFF", - "label": "Computer - Front - Off", - "category": "electronics", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "COMPUTER", - "canPlaceOnSurfaces": true, - "backgroundTiles": 0, - "canPlaceOnWalls": false, - "orientation": "front" - }, - { - "id": "ASSET_76", - "paddedX": 160, - "paddedY": 360, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "MONITOR_CRT_ON", - "label": "CRT Monitor - On", - "category": "electronics", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "CRT_MONITOR", - "canPlaceOnSurfaces": true - }, - { - "id": "ASSET_77", - "paddedX": 160, - "paddedY": 360, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "COMPUTER_FRONT_ON", - "label": "Computer - Front - On", - "category": "electronics", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "COMPUTER", - "canPlaceOnSurfaces": true, - "backgroundTiles": 0, - "canPlaceOnWalls": false, - "orientation": "front" - }, - { - "id": "ASSET_80", - "paddedX": 80, - "paddedY": 351, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WOOD_WINDOW_SM", - "label": "Small Wooden Window", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_81", - "paddedX": 96, - "paddedY": 351, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WHITE_WINDOW_SM", - "label": "Small White Window", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_82", - "paddedX": 112, - "paddedY": 351, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WHITE_WINDOW_SM_2", - "label": "Small White Window", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_90", - "paddedX": 192, - "paddedY": 358, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "FULL_COMPUTER_COFFEE_OFF", - "label": "Full Computer with Coffee", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "FULL_COMPUTER_COFFEE", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_92", - "paddedX": 224, - "paddedY": 358, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "FULL_COMPUTER_COFFEE_ON", - "label": "Full Computer with Coffee", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "FULL_COMPUTER_COFFEE", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false, - "erasedPixels": [ - { - "x": 20, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 25, - "y": 31 - }, - { - "x": 26, - "y": 31 - }, - { - "x": 27, - "y": 31 - }, - { - "x": 27, - "y": 30 - }, - { - "x": 26, - "y": 30 - }, - { - "x": 25, - "y": 30 - }, - { - "x": 24, - "y": 30 - }, - { - "x": 23, - "y": 30 - }, - { - "x": 22, - "y": 30 - }, - { - "x": 21, - "y": 30 - }, - { - "x": 20, - "y": 29 - }, - { - "x": 19, - "y": 29 - }, - { - "x": 19, - "y": 30 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 20, - "y": 30 - } - ] - }, - { - "id": "ASSET_94", - "paddedX": 128, - "paddedY": 353, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "FULL_COMPUTER_OFF", - "label": "Full Computer - Off", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "FULL_COMPUTER", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_95", - "paddedX": 160, - "paddedY": 353, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "FULL_COMPUTER_ON", - "label": "Full Computer - On", - "category": "electronics", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "FULL_COMPUTER", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "front", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_98", - "paddedX": 159, - "paddedY": 386, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [], - "name": "LAPTOP_RIGHT", - "label": "Laptop - Right", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "LAPTOP", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "right", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_99", - "paddedX": 177, - "paddedY": 386, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 13, - "y": 31 - }, - { - "x": 12, - "y": 31 - } - ], - "name": "LAPTOP_LEFT", - "label": "Laptop - Left", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": true, - "groupId": "LAPTOP", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "left", - "canPlaceOnWalls": false - }, - { - "id": "ASSET_100", - "paddedX": 240, - "paddedY": 392, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PAPER_SIDE", - "label": "Paper - Side", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "PAPER", - "canPlaceOnSurfaces": true, - "erasedPixels": [ - { - "x": 4, - "y": 0 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 4, - "y": 2 - }, - { - "x": 4, - "y": 3 - }, - { - "x": 4, - "y": 4 - }, - { - "x": 5, - "y": 4 - }, - { - "x": 6, - "y": 4 - }, - { - "x": 7, - "y": 4 - }, - { - "x": 7, - "y": 3 - }, - { - "x": 8, - "y": 3 - }, - { - "x": 9, - "y": 3 - }, - { - "x": 10, - "y": 3 - }, - { - "x": 11, - "y": 3 - }, - { - "x": 11, - "y": 2 - }, - { - "x": 10, - "y": 2 - }, - { - "x": 9, - "y": 2 - }, - { - "x": 8, - "y": 2 - }, - { - "x": 7, - "y": 2 - }, - { - "x": 6, - "y": 2 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 5, - "y": 3 - }, - { - "x": 8, - "y": 4 - }, - { - "x": 9, - "y": 4 - }, - { - "x": 10, - "y": 4 - }, - { - "x": 6, - "y": 3 - }, - { - "x": 3, - "y": 3 - }, - { - "x": 3, - "y": 2 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 2, - "y": 26 - }, - { - "x": 3, - "y": 26 - }, - { - "x": 4, - "y": 27 - }, - { - "x": 5, - "y": 27 - }, - { - "x": 6, - "y": 27 - }, - { - "x": 7, - "y": 27 - }, - { - "x": 8, - "y": 27 - }, - { - "x": 9, - "y": 27 - }, - { - "x": 10, - "y": 27 - }, - { - "x": 11, - "y": 27 - }, - { - "x": 12, - "y": 27 - }, - { - "x": 13, - "y": 27 - }, - { - "x": 13, - "y": 26 - }, - { - "x": 12, - "y": 26 - }, - { - "x": 11, - "y": 26 - }, - { - "x": 10, - "y": 26 - }, - { - "x": 9, - "y": 26 - }, - { - "x": 8, - "y": 26 - }, - { - "x": 7, - "y": 26 - }, - { - "x": 6, - "y": 26 - }, - { - "x": 5, - "y": 26 - }, - { - "x": 4, - "y": 26 - }, - { - "x": 2, - "y": 27 - }, - { - "x": 2, - "y": 28 - }, - { - "x": 2, - "y": 29 - }, - { - "x": 2, - "y": 30 - }, - { - "x": 3, - "y": 29 - }, - { - "x": 3, - "y": 28 - }, - { - "x": 4, - "y": 28 - }, - { - "x": 3, - "y": 30 - }, - { - "x": 3, - "y": 31 - }, - { - "x": 2, - "y": 31 - }, - { - "x": 3, - "y": 27 - }, - { - "x": 4, - "y": 29 - }, - { - "x": 5, - "y": 28 - }, - { - "x": 6, - "y": 28 - }, - { - "x": 5, - "y": 29 - }, - { - "x": 4, - "y": 30 - }, - { - "x": 9, - "y": 28 - }, - { - "x": 8, - "y": 28 - }, - { - "x": 7, - "y": 28 - }, - { - "x": 6, - "y": 29 - }, - { - "x": 5, - "y": 30 - }, - { - "x": 6, - "y": 30 - }, - { - "x": 7, - "y": 30 - }, - { - "x": 8, - "y": 29 - }, - { - "x": 9, - "y": 29 - }, - { - "x": 10, - "y": 28 - }, - { - "x": 11, - "y": 28 - }, - { - "x": 7, - "y": 29 - }, - { - "x": 5, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 30 - }, - { - "x": 10, - "y": 29 - }, - { - "x": 11, - "y": 29 - }, - { - "x": 12, - "y": 29 - }, - { - "x": 12, - "y": 28 - }, - { - "x": 10, - "y": 30 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 11, - "y": 30 - }, - { - "x": 12, - "y": 30 - }, - { - "x": 8, - "y": 30 - }, - { - "x": 4, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 13, - "y": 30 - }, - { - "x": 13, - "y": 29 - }, - { - "x": 13, - "y": 28 - }, - { - "x": 14, - "y": 27 - }, - { - "x": 14, - "y": 28 - }, - { - "x": 14, - "y": 29 - }, - { - "x": 14, - "y": 30 - }, - { - "x": 14, - "y": 31 - } - ], - "backgroundTiles": 1, - "canPlaceOnWalls": false, - "orientation": "front" - }, - { - "id": "ASSET_107", - "paddedX": 128, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 3, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 2, - "y": 0 - } - ], - "name": "LAPTOP_FRONT_OFF", - "label": "Laptop - Front - Off", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "LAPTOP", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "front" - }, - { - "id": "ASSET_108", - "paddedX": 144, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 2, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - } - ], - "name": "LAPTOP_FRONT_ON", - "label": "Laptop - Front - On", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "LAPTOP", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "orientation": "front" - }, - { - "id": "ASSET_110", - "paddedX": 224, - "paddedY": 386, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PAPER_FRONT", - "label": "Paper - Front", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "PAPER", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "canPlaceOnWalls": false, - "orientation": "front" - }, - { - "id": "ASSET_109", - "paddedX": 192, - "paddedY": 386, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "LAPTOP_BACK", - "label": "Laptop - Back", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "LAPTOP", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "canPlaceOnWalls": false, - "orientation": "back" - }, - { - "id": "ASSET_101", - "paddedX": 0, - "paddedY": 384, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "PAINTING_LANDSCAPE", - "label": "Landscape Painting", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_102", - "paddedX": 32, - "paddedY": 384, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "PAINTING_LANDSCAPE_2", - "label": "Landscape Painting", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_103", - "paddedX": 64, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PAINTING_SM", - "label": "Small Painting", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_104", - "paddedX": 80, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PAINTING_SM_2", - "label": "Small Painting", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_105", - "paddedX": 96, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PAINTING_SM_3", - "label": "Small Painting", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_106", - "paddedX": 112, - "paddedY": 384, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "TEXT_FRAME", - "label": "Framed Text", - "category": "wall", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_122", - "paddedX": 0, - "paddedY": 415, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CHALKBOARD_WALL_SM", - "label": "Small Wall Chalkboard", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - }, - { - "id": "ASSET_118", - "paddedX": 32, - "paddedY": 415, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CHART_SM_1", - "label": "Small Chart", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_119", - "paddedX": 64, - "paddedY": 415, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 5, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 25, - "y": 31 - }, - { - "x": 26, - "y": 31 - }, - { - "x": 27, - "y": 31 - }, - { - "x": 28, - "y": 31 - }, - { - "x": 4, - "y": 31 - } - ], - "name": "CHART_SM_2", - "label": "Small Chart", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_120", - "paddedX": 96, - "paddedY": 415, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [], - "name": "CHART_1", - "label": "Chart", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_114", - "paddedX": 192, - "paddedY": 408, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "MONITOR_CRT_BACK", - "label": "CRT Monitor - Back", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "CRT_MONITOR", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "canPlaceOnWalls": false, - "orientation": "back", - "erasedPixels": [ - { - "x": 2, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 1, - "y": 25 - }, - { - "x": 2, - "y": 25 - }, - { - "x": 3, - "y": 25 - }, - { - "x": 4, - "y": 25 - }, - { - "x": 3, - "y": 24 - }, - { - "x": 2, - "y": 24 - }, - { - "x": 2, - "y": 23 - }, - { - "x": 1, - "y": 23 - }, - { - "x": 0, - "y": 23 - }, - { - "x": 1, - "y": 24 - }, - { - "x": 0, - "y": 24 - }, - { - "x": 0, - "y": 25 - }, - { - "x": 0, - "y": 26 - }, - { - "x": 0, - "y": 27 - }, - { - "x": 0, - "y": 28 - }, - { - "x": 1, - "y": 27 - }, - { - "x": 1, - "y": 26 - }, - { - "x": 2, - "y": 26 - }, - { - "x": 2, - "y": 27 - }, - { - "x": 2, - "y": 28 - }, - { - "x": 2, - "y": 29 - }, - { - "x": 2, - "y": 30 - }, - { - "x": 1, - "y": 31 - }, - { - "x": 1, - "y": 30 - }, - { - "x": 1, - "y": 29 - }, - { - "x": 1, - "y": 28 - }, - { - "x": 2, - "y": 31 - }, - { - "x": 0, - "y": 31 - }, - { - "x": 0, - "y": 30 - }, - { - "x": 0, - "y": 29 - }, - { - "x": 3, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 5, - "y": 30 - }, - { - "x": 5, - "y": 29 - }, - { - "x": 5, - "y": 28 - }, - { - "x": 4, - "y": 28 - }, - { - "x": 4, - "y": 27 - }, - { - "x": 4, - "y": 30 - }, - { - "x": 4, - "y": 29 - }, - { - "x": 4, - "y": 26 - }, - { - "x": 4, - "y": 31 - }, - { - "x": 3, - "y": 30 - }, - { - "x": 3, - "y": 29 - }, - { - "x": 3, - "y": 28 - }, - { - "x": 3, - "y": 27 - }, - { - "x": 5, - "y": 27 - }, - { - "x": 6, - "y": 28 - }, - { - "x": 6, - "y": 27 - }, - { - "x": 5, - "y": 26 - }, - { - "x": 3, - "y": 26 - }, - { - "x": 7, - "y": 28 - }, - { - "x": 7, - "y": 27 - }, - { - "x": 8, - "y": 27 - }, - { - "x": 8, - "y": 28 - }, - { - "x": 9, - "y": 28 - }, - { - "x": 8, - "y": 29 - }, - { - "x": 7, - "y": 30 - }, - { - "x": 6, - "y": 30 - }, - { - "x": 5, - "y": 31 - }, - { - "x": 8, - "y": 30 - }, - { - "x": 9, - "y": 29 - }, - { - "x": 10, - "y": 28 - }, - { - "x": 7, - "y": 29 - }, - { - "x": 6, - "y": 29 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 30 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 8, - "y": 26 - } - ] - }, - { - "id": "ASSET_112", - "paddedX": 192, - "paddedY": 416, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "COMPUTER_BACK", - "label": "Computer - Back", - "category": "storage", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "COMPUTER", - "canPlaceOnSurfaces": true, - "backgroundTiles": 0, - "canPlaceOnWalls": false, - "orientation": "back" - }, - { - "id": "ASSET_121", - "paddedX": 152, - "paddedY": 415, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "MONITOR_BACK", - "label": "Monitor - Back", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": true, - "groupId": "MONITOR", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1, - "canPlaceOnWalls": false, - "orientation": "back" - }, - { - "id": "ASSET_123", - "paddedX": 131, - "paddedY": 417, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "SERVER", - "label": "Server", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": "PC", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_126", - "paddedX": 216, - "paddedY": 423, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PRINTER_DESKTOP", - "label": "Desktop Printer", - "category": "electronics", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true, - "erasedPixels": [ - { - "x": 15, - "y": 4 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 15, - "y": 0 - } - ], - "backgroundTiles": 1, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_127", - "paddedX": 240, - "paddedY": 431, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "CRATE", - "label": "Crate", - "category": "storage", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true - }, - { - "id": "ASSET_133_0_0", - "paddedX": 240, - "paddedY": 528, - "paddedWidth": 16, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 6, - "y": 0 - } - ], - "name": "WHITE_PLANT_1", - "label": "Small Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true, - "backgroundTiles": 1 - }, - { - "id": "ASSET_133_1_0", - "paddedX": 49, - "paddedY": 463, - "paddedWidth": 16, - "paddedHeight": 16, - "name": "WHITE_PLANT_BOTTOM", - "label": "White Plant - Bottom", - "category": "decor", - "footprintW": 1, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": true, - "partOfGroup": true, - "groupId": "WHITE_PLANT", - "canPlaceOnSurfaces": true, - "backgroundTiles": 0, - "orientation": "front" - }, - { - "id": "ASSET_134", - "paddedX": 0, - "paddedY": 448, - "paddedWidth": 32, - "paddedHeight": 32, - "erasedPixels": [ - { - "x": 4, - "y": 31 - }, - { - "x": 5, - "y": 31 - }, - { - "x": 6, - "y": 31 - }, - { - "x": 7, - "y": 31 - }, - { - "x": 8, - "y": 31 - }, - { - "x": 9, - "y": 31 - }, - { - "x": 10, - "y": 31 - }, - { - "x": 11, - "y": 31 - }, - { - "x": 12, - "y": 31 - }, - { - "x": 13, - "y": 31 - }, - { - "x": 14, - "y": 31 - }, - { - "x": 15, - "y": 31 - }, - { - "x": 16, - "y": 31 - }, - { - "x": 17, - "y": 31 - }, - { - "x": 18, - "y": 31 - }, - { - "x": 19, - "y": 31 - }, - { - "x": 20, - "y": 31 - }, - { - "x": 21, - "y": 31 - }, - { - "x": 22, - "y": 31 - }, - { - "x": 23, - "y": 31 - }, - { - "x": 24, - "y": 31 - }, - { - "x": 25, - "y": 31 - }, - { - "x": 26, - "y": 31 - }, - { - "x": 27, - "y": 31 - } - ], - "name": "CHART_2", - "label": "Chart", - "category": "wall", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 0, - "canPlaceOnWalls": true - }, - { - "id": "ASSET_138", - "paddedX": 130, - "paddedY": 447, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CRATES_SM", - "label": "Crates", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_135", - "paddedX": 143, - "paddedY": 447, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CRATES_SM_2", - "label": "Crates", - "category": "storage", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_136", - "paddedX": 157, - "paddedY": 447, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CRATES", - "label": "Crates", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "erasedPixels": [ - { - "x": 1, - "y": 12 - }, - { - "x": 1, - "y": 13 - }, - { - "x": 1, - "y": 14 - }, - { - "x": 1, - "y": 15 - }, - { - "x": 1, - "y": 16 - }, - { - "x": 1, - "y": 17 - }, - { - "x": 1, - "y": 18 - }, - { - "x": 1, - "y": 19 - }, - { - "x": 1, - "y": 20 - }, - { - "x": 1, - "y": 21 - }, - { - "x": 1, - "y": 22 - }, - { - "x": 1, - "y": 23 - }, - { - "x": 1, - "y": 24 - }, - { - "x": 1, - "y": 25 - }, - { - "x": 1, - "y": 26 - }, - { - "x": 1, - "y": 27 - }, - { - "x": 1, - "y": 28 - }, - { - "x": 1, - "y": 29 - }, - { - "x": 1, - "y": 30 - }, - { - "x": 1, - "y": 31 - }, - { - "x": 0, - "y": 31 - }, - { - "x": 0, - "y": 30 - }, - { - "x": 0, - "y": 29 - }, - { - "x": 0, - "y": 28 - }, - { - "x": 0, - "y": 27 - }, - { - "x": 0, - "y": 26 - }, - { - "x": 0, - "y": 25 - }, - { - "x": 0, - "y": 24 - }, - { - "x": 0, - "y": 23 - }, - { - "x": 0, - "y": 22 - }, - { - "x": 0, - "y": 21 - }, - { - "x": 0, - "y": 20 - }, - { - "x": 0, - "y": 19 - }, - { - "x": 0, - "y": 18 - }, - { - "x": 0, - "y": 17 - }, - { - "x": 2, - "y": 12 - }, - { - "x": 1, - "y": 11 - }, - { - "x": 0, - "y": 11 - }, - { - "x": 0, - "y": 12 - }, - { - "x": 0, - "y": 13 - }, - { - "x": 0, - "y": 14 - }, - { - "x": 0, - "y": 15 - }, - { - "x": 0, - "y": 16 - }, - { - "x": 1, - "y": 10 - }, - { - "x": 1, - "y": 9 - }, - { - "x": 2, - "y": 10 - }, - { - "x": 2, - "y": 11 - }, - { - "x": 2, - "y": 13 - }, - { - "x": 2, - "y": 17 - }, - { - "x": 2, - "y": 16 - }, - { - "x": 2, - "y": 15 - }, - { - "x": 2, - "y": 14 - }, - { - "x": 2, - "y": 18 - }, - { - "x": 2, - "y": 19 - }, - { - "x": 2, - "y": 20 - }, - { - "x": 0, - "y": 10 - } - ] - }, - { - "id": "ASSET_137", - "paddedX": 195, - "paddedY": 447, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CRATES_2", - "label": "Crates", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_139", - "paddedX": 224, - "paddedY": 447, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "CRATES_3", - "label": "Crates", - "category": "storage", - "footprintW": 2, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_132", - "paddedX": 176, - "paddedY": 560, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PLANT_1", - "label": "Small Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": "PLANT", - "canPlaceOnSurfaces": true, - "backgroundTiles": 1 - }, - { - "id": "ASSET_140", - "paddedX": 224, - "paddedY": 560, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WHITE_PLANT_2", - "label": "Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": "WHITE_PLANT", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_141", - "paddedX": 240, - "paddedY": 560, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "WHITE_PLANT_3", - "label": "Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": "WHITE_PLANT", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_142", - "paddedX": 192, - "paddedY": 560, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PLANT_2", - "label": "Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": "WHITE_PLANT", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_143", - "paddedX": 208, - "paddedY": 560, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "PLANT_3", - "label": "Plant", - "category": "decor", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": "WHITE_PLANT", - "canPlaceOnSurfaces": false, - "backgroundTiles": 1 - }, - { - "id": "ASSET_145", - "paddedX": 32, - "paddedY": 478, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "MAT_SQUARE", - "label": "Square Pattern Mat", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_148", - "paddedX": 0, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "MAT_CHESS_BOARD", - "label": "Chess Board Mat", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_150", - "paddedX": 64, - "paddedY": 494, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "MAT_CIRCLES", - "label": "Circle Pattern Mat", - "category": "decor", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "colorEditable": true, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": false - }, - { - "id": "ASSET_151", - "paddedX": 108, - "paddedY": 487, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "MICROWAVE", - "label": "Microwave", - "category": "misc", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "colorEditable": false, - "discard": false, - "partOfGroup": false, - "groupId": null, - "canPlaceOnSurfaces": true, - "erasedPixels": [ - { - "x": 0, - "y": 7 - }, - { - "x": 1, - "y": 7 - }, - { - "x": 2, - "y": 7 - }, - { - "x": 3, - "y": 7 - }, - { - "x": 4, - "y": 7 - }, - { - "x": 5, - "y": 7 - }, - { - "x": 6, - "y": 7 - }, - { - "x": 7, - "y": 7 - }, - { - "x": 8, - "y": 7 - }, - { - "x": 9, - "y": 7 - }, - { - "x": 10, - "y": 7 - }, - { - "x": 11, - "y": 7 - }, - { - "x": 12, - "y": 7 - }, - { - "x": 13, - "y": 7 - }, - { - "x": 14, - "y": 7 - }, - { - "x": 15, - "y": 7 - }, - { - "x": 1, - "y": 6 - }, - { - "x": 1, - "y": 5 - }, - { - "x": 1, - "y": 4 - }, - { - "x": 1, - "y": 3 - }, - { - "x": 1, - "y": 2 - }, - { - "x": 1, - "y": 1 - }, - { - "x": 1, - "y": 0 - }, - { - "x": 1, - "y": 8 - }, - { - "x": 0, - "y": 6 - }, - { - "x": 0, - "y": 5 - }, - { - "x": 0, - "y": 4 - }, - { - "x": 0, - "y": 3 - }, - { - "x": 0, - "y": 2 - }, - { - "x": 0, - "y": 1 - }, - { - "x": 0, - "y": 0 - }, - { - "x": 2, - "y": 6 - }, - { - "x": 5, - "y": 6 - }, - { - "x": 6, - "y": 6 - }, - { - "x": 7, - "y": 6 - }, - { - "x": 8, - "y": 6 - }, - { - "x": 9, - "y": 6 - }, - { - "x": 10, - "y": 6 - }, - { - "x": 11, - "y": 6 - }, - { - "x": 12, - "y": 6 - }, - { - "x": 13, - "y": 6 - }, - { - "x": 14, - "y": 6 - }, - { - "x": 15, - "y": 6 - }, - { - "x": 4, - "y": 6 - }, - { - "x": 3, - "y": 5 - }, - { - "x": 2, - "y": 5 - }, - { - "x": 3, - "y": 8 - }, - { - "x": 3, - "y": 9 - }, - { - "x": 4, - "y": 9 - }, - { - "x": 4, - "y": 8 - }, - { - "x": 3, - "y": 6 - }, - { - "x": 4, - "y": 5 - }, - { - "x": 5, - "y": 5 - }, - { - "x": 6, - "y": 5 - }, - { - "x": 7, - "y": 5 - }, - { - "x": 8, - "y": 5 - }, - { - "x": 9, - "y": 5 - }, - { - "x": 10, - "y": 5 - }, - { - "x": 11, - "y": 5 - }, - { - "x": 11, - "y": 4 - }, - { - "x": 10, - "y": 4 - }, - { - "x": 9, - "y": 4 - }, - { - "x": 8, - "y": 4 - }, - { - "x": 7, - "y": 4 - }, - { - "x": 6, - "y": 4 - }, - { - "x": 5, - "y": 4 - }, - { - "x": 12, - "y": 5 - }, - { - "x": 13, - "y": 5 - }, - { - "x": 14, - "y": 5 - }, - { - "x": 15, - "y": 5 - }, - { - "x": 2, - "y": 4 - }, - { - "x": 2, - "y": 3 - }, - { - "x": 2, - "y": 2 - }, - { - "x": 3, - "y": 3 - }, - { - "x": 3, - "y": 4 - }, - { - "x": 3, - "y": 2 - }, - { - "x": 4, - "y": 2 - }, - { - "x": 5, - "y": 3 - }, - { - "x": 6, - "y": 3 - }, - { - "x": 4, - "y": 3 - }, - { - "x": 2, - "y": 1 - }, - { - "x": 3, - "y": 1 - }, - { - "x": 4, - "y": 1 - }, - { - "x": 5, - "y": 2 - }, - { - "x": 2, - "y": 0 - }, - { - "x": 3, - "y": 0 - }, - { - "x": 4, - "y": 0 - }, - { - "x": 5, - "y": 1 - }, - { - "x": 6, - "y": 1 - }, - { - "x": 6, - "y": 2 - }, - { - "x": 4, - "y": 4 - }, - { - "x": 7, - "y": 1 - }, - { - "x": 8, - "y": 1 - }, - { - "x": 9, - "y": 1 - }, - { - "x": 9, - "y": 2 - }, - { - "x": 8, - "y": 3 - }, - { - "x": 10, - "y": 1 - }, - { - "x": 11, - "y": 1 - }, - { - "x": 12, - "y": 1 - }, - { - "x": 11, - "y": 2 - }, - { - "x": 10, - "y": 3 - }, - { - "x": 9, - "y": 3 - }, - { - "x": 9, - "y": 0 - }, - { - "x": 8, - "y": 0 - }, - { - "x": 7, - "y": 0 - }, - { - "x": 6, - "y": 0 - }, - { - "x": 5, - "y": 0 - }, - { - "x": 13, - "y": 0 - }, - { - "x": 14, - "y": 0 - }, - { - "x": 10, - "y": 2 - }, - { - "x": 8, - "y": 2 - }, - { - "x": 12, - "y": 0 - }, - { - "x": 11, - "y": 0 - }, - { - "x": 10, - "y": 0 - }, - { - "x": 13, - "y": 1 - }, - { - "x": 14, - "y": 1 - }, - { - "x": 15, - "y": 1 - }, - { - "x": 7, - "y": 3 - }, - { - "x": 7, - "y": 2 - }, - { - "x": 11, - "y": 3 - }, - { - "x": 12, - "y": 3 - }, - { - "x": 13, - "y": 3 - }, - { - "x": 14, - "y": 2 - }, - { - "x": 15, - "y": 2 - }, - { - "x": 15, - "y": 3 - }, - { - "x": 15, - "y": 4 - }, - { - "x": 14, - "y": 3 - }, - { - "x": 13, - "y": 4 - }, - { - "x": 14, - "y": 4 - }, - { - "x": 15, - "y": 0 - }, - { - "x": 12, - "y": 4 - }, - { - "x": 12, - "y": 2 - }, - { - "x": 13, - "y": 2 - } - ], - "backgroundTiles": 1, - "canPlaceOnWalls": false - }, - { - "id": "ASSET_NEW_106", - "paddedX": 16, - "paddedY": -9, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "TABLE_WOOD", - "label": "Wooden Table", - "category": "desks", - "footprintW": 3, - "footprintH": 2, - "isDesk": true, - "colorEditable": true, - "discard": false, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "backgroundTiles": 1 - }, - { - "id": "ASSET_NEW_107", - "paddedX": 64, - "paddedY": -10, - "paddedWidth": 48, - "paddedHeight": 32, - "name": "TABLE_PLAIN_WOOD", - "label": "Plain Wooden Table", - "category": "desks", - "footprintW": 3, - "footprintH": 2, - "isDesk": true, - "colorEditable": true, - "discard": false, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null, - "backgroundTiles": 1 - }, - { - "id": "ASSET_NEW_108", - "paddedX": 16, - "paddedY": 608, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "CHAIR_CUSHIONED_LG_FRONT", - "label": "Large Cushioned Chair", - "category": "chairs", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false, - "backgroundTiles": 0, - "canPlaceOnSurfaces": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR_LG", - "orientation": "front" - }, - { - "id": "ASSET_NEW_109", - "paddedX": 48, - "paddedY": 608, - "paddedWidth": 32, - "paddedHeight": 16, - "name": "CHAIR_CUSHIONED_LG_BACK", - "label": "Large Cushioned Chair", - "category": "chairs", - "footprintW": 2, - "footprintH": 1, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false, - "backgroundTiles": 0, - "canPlaceOnSurfaces": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR_LG", - "orientation": "back" - }, - { - "id": "ASSET_NEW_110", - "paddedX": 16, - "paddedY": 624, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CHAIR_CUSHIONED_LG_RIGHT", - "label": "Large Cushioned Chair", - "category": "chairs", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false, - "backgroundTiles": 0, - "canPlaceOnSurfaces": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR_LG", - "orientation": "right" - }, - { - "id": "ASSET_NEW_111", - "paddedX": 32, - "paddedY": 624, - "paddedWidth": 16, - "paddedHeight": 32, - "name": "CHAIR_CUSHIONED_LG_LEFT", - "label": "Large Cushioned Chair", - "category": "chairs", - "footprintW": 1, - "footprintH": 2, - "isDesk": false, - "canPlaceOnWalls": false, - "discard": false, - "backgroundTiles": 0, - "canPlaceOnSurfaces": false, - "partOfGroup": true, - "groupId": "CUSHIONED_CHAIR_LG", - "orientation": "left" - }, - { - "id": "ASSET_NEW_112", - "paddedX": 48, - "paddedY": 624, - "paddedWidth": 32, - "paddedHeight": 32, - "name": "COFFEE_TABLE_LG", - "label": "Large Coffee Table", - "category": "desks", - "footprintW": 2, - "footprintH": 2, - "isDesk": true, - "canPlaceOnWalls": false, - "discard": false, - "backgroundTiles": 1, - "canPlaceOnSurfaces": false, - "partOfGroup": false, - "groupId": null - } - ] -} \ No newline at end of file diff --git a/scripts/0-import-tileset.ts b/scripts/0-import-tileset.ts deleted file mode 100644 index b8d349e6..00000000 --- a/scripts/0-import-tileset.ts +++ /dev/null @@ -1,425 +0,0 @@ -#!/usr/bin/env node -/** - * Pixel Agents Tileset Import Skill - Complete CLI wrapper for 7-stage asset extraction pipeline - * - * Usage: - * npx ts-node scripts/import-tileset-cli.ts - * - * This script guides you through the complete process of extracting furniture assets - * from a tileset PNG file and integrating them into the Pixel Agents extension. - */ - -import * as fs from 'fs' -import * as path from 'path' -import * as readline from 'readline' -import { execSync } from 'child_process' - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, -}) - -function question(prompt: string): Promise { - return new Promise((resolve) => { - rl.question(prompt, resolve) - }) -} - -interface StageStatus { - name: string - description: string - completed: boolean - script?: string -} - -const stages: StageStatus[] = [ - { - name: 'Stage 1: Asset Detection', - description: 'Automatically detect individual assets from tileset using flood-fill', - completed: false, - script: 'scripts/1-detect-assets.ts', - }, - { - name: 'Stage 2: Asset Editor', - description: 'Interactively edit asset positions, sizes, and erase unwanted pixels', - completed: false, - }, - { - name: 'Stage 3: Vision Inspection', - description: 'Use Claude vision to auto-generate metadata for each asset', - completed: false, - script: 'scripts/3-vision-inspect.ts', - }, - { - name: 'Stage 4: Metadata Review', - description: 'Review and edit all asset metadata (name, category, flags, etc)', - completed: false, - }, - { - name: 'Stage 5: Export Assets', - description: 'Export approved assets as PNG files + generate catalog.json', - completed: false, - script: 'scripts/5-export-assets.ts', - }, - { - name: 'Stage 6: Extension Integration', - description: 'Assets bundled with extension and loaded at runtime', - completed: false, - }, - { - name: 'Stage 7: You are here!', - description: 'Using this CLI to repeat the process for new tilesets', - completed: true, - }, -] - -function displayBanner() { - console.clear() - console.log('\n') - console.log('╔════════════════════════════════════════════════════════════════╗') - console.log('║ 🎨 ARCADIA TILESET IMPORT SKILL 🎨 ║') - console.log('║ Complete Asset Extraction Pipeline (7 Stages) ║') - console.log('╚════════════════════════════════════════════════════════════════╝') - console.log() -} - -function displayStages() { - console.log('\n📋 Pipeline Stages:\n') - stages.forEach((stage, idx) => { - const icon = stage.completed ? '✅' : '⬜' - const status = stage.completed ? ' COMPLETE' : '' - console.log(`${icon} ${stage.name}${status}`) - console.log(` ${stage.description}`) - console.log() - }) -} - -async function displayMenu() { - console.log('\n🎯 Main Menu:') - console.log(' 1. Start new tileset import') - console.log(' 2. View pipeline stages') - console.log(' 3. Run specific stage') - console.log(' 4. Exit') - console.log() - - const choice = await question('Select option (1-4): ') - return choice.trim() -} - -async function getFileInput(prompt: string, filter?: string): Promise { - let valid = false - let file = '' - - while (!valid) { - file = await question(prompt) - file = file.trim().replace(/^['"]|['"]$/g, '') // Remove quotes - - if (!fs.existsSync(file)) { - console.log(`❌ File not found: ${file}`) - continue - } - - if (filter && !file.endsWith(filter)) { - console.log(`❌ File must be a ${filter} file`) - continue - } - - valid = true - } - - return file -} - -async function runStage1(tilesetFile: string) { - console.log('\n📍 Stage 1: Asset Detection') - console.log('─'.repeat(60)) - console.log('Flood-fill algorithm will automatically detect all individual assets') - console.log(`Input: ${tilesetFile}`) - console.log('Output: tileset-detection-output.json') - console.log() - - // Copy tileset to root if needed - const tilesetDest = path.join(process.cwd(), 'assets', 'office_tileset_16x16.png') - if (!fs.existsSync(tilesetDest)) { - console.log(`📦 Copying tileset to ${tilesetDest}...`) - const destDir = path.dirname(tilesetDest) - if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }) - fs.copyFileSync(tilesetFile, tilesetDest) - } - - const confirm = await question('Run Stage 1? (y/n): ') - if (confirm.toLowerCase() === 'y') { - try { - console.log('\n🔄 Running detection...') - execSync('npx ts-node scripts/detect-tileset-assets.ts', { stdio: 'inherit' }) - console.log('\n✅ Stage 1 complete!') - return true - } catch (err) { - console.log('\n❌ Stage 1 failed') - return false - } - } - return false -} - -async function runStage2() { - console.log('\n📍 Stage 2: Asset Editor') - console.log('─'.repeat(60)) - console.log('Interactive editor for refining asset positions and erase unwanted pixels') - console.log('Input: tileset-detection-output.json') - console.log('Output: asset-editor-output.json') - console.log() - console.log('📝 Open scripts/asset-editor.html in a web browser to edit assets') - console.log(' 1. Adjust bounding boxes') - console.log(' 2. Split stuck/overlapped assets') - console.log(' 3. Erase unwanted pixels') - console.log(' 4. Export when done') - console.log() - - const confirm = await question('Open editor? (y/n): ') - if (confirm.toLowerCase() === 'y') { - try { - const editorPath = path.join(process.cwd(), 'scripts', '2-asset-editor.html') - console.log(`\n📂 Opening: ${editorPath}`) - if (process.platform === 'win32') { - execSync(`start "" "${editorPath}"`) - } else if (process.platform === 'darwin') { - execSync(`open "${editorPath}"`) - } else { - execSync(`xdg-open "${editorPath}"`) - } - - console.log('\n⏳ Editor opened in browser. When finished:') - const done = await question('Press Enter when done editing...') - console.log('✅ Stage 2 complete!') - return true - } catch (err) { - console.log('❌ Could not open editor') - return false - } - } - return false -} - -async function runStage3() { - console.log('\n📍 Stage 3: Vision Inspection') - console.log('─'.repeat(60)) - console.log('Claude vision API analyzes each asset and generates metadata') - console.log('Input: asset-editor-output.json + office_tileset_16x16.png') - console.log('Output: tileset-metadata-draft.json') - console.log() - console.log('⚠️ Requires ANTHROPIC_API_KEY in .env file') - console.log() - - const confirm = await question('Run Stage 3? (y/n): ') - if (confirm.toLowerCase() === 'y') { - try { - console.log('\n🔄 Running vision inspection...') - execSync('npx ts-node scripts/inspect-assets.ts', { stdio: 'inherit' }) - console.log('\n✅ Stage 3 complete!') - return true - } catch (err) { - console.log('\n❌ Stage 3 failed') - return false - } - } - return false -} - -async function runStage4() { - console.log('\n📍 Stage 4: Metadata Review') - console.log('─'.repeat(60)) - console.log('Interactive review and editing of all asset metadata') - console.log('Input: tileset-metadata-draft.json') - console.log('Output: tileset-metadata-final.json') - console.log() - console.log('📝 Open scripts/review-assets.html in a web browser to review') - console.log(' 1. View asset previews (4x zoom with grid)') - console.log(' 2. Edit metadata: name, label, category') - console.log(' 3. Set footprint dimensions (in tiles)') - console.log(' 4. Mark special flags: isDesk, canPlaceOnWalls, canPlaceOnSurfaces') - console.log(' 5. Mark assets to discard') - console.log(' 6. Auto-saves to localStorage every 2 seconds') - console.log() - - const confirm = await question('Open review editor? (y/n): ') - if (confirm.toLowerCase() === 'y') { - try { - const editorPath = path.join(process.cwd(), 'scripts', '4-review-metadata.html') - console.log(`\n📂 Opening: ${editorPath}`) - if (process.platform === 'win32') { - execSync(`start "" "${editorPath}"`) - } else if (process.platform === 'darwin') { - execSync(`open "${editorPath}"`) - } else { - execSync(`xdg-open "${editorPath}"`) - } - - console.log('\n⏳ Editor opened in browser. When finished:') - const done = await question('Press Enter when done reviewing...') - console.log('✅ Stage 4 complete!') - return true - } catch (err) { - console.log('❌ Could not open editor') - return false - } - } - return false -} - -async function runStage5() { - console.log('\n📍 Stage 5: Export Assets') - console.log('─'.repeat(60)) - console.log('Export approved assets as PNG files + generate furniture-catalog.json') - console.log('Input: tileset-metadata-final.json + office_tileset_16x16.png') - console.log('Output: assets/furniture/{category}/{id}.png + furniture-catalog.json') - console.log() - - const confirm = await question('Run Stage 5? (y/n): ') - if (confirm.toLowerCase() === 'y') { - try { - console.log('\n🔄 Exporting assets...') - execSync('npx ts-node scripts/export-tileset-assets.ts', { stdio: 'inherit' }) - console.log('\n✅ Stage 5 complete!') - return true - } catch (err) { - console.log('\n❌ Stage 5 failed') - return false - } - } - return false -} - -async function runStage6() { - console.log('\n📍 Stage 6: Extension Integration') - console.log('─'.repeat(60)) - console.log('Assets are automatically bundled with the extension and loaded at runtime') - console.log() - console.log('✅ Automatic! Just rebuild the extension:') - console.log(' npm run build') - console.log() - console.log('📦 The extension now:') - console.log(' • Bundles assets/furniture/* in dist/') - console.log(' • Loads assets from dist/assets/ at runtime') - console.log(' • Works in any directory (no workspace dependency)') - console.log(' • Shows ONLY your custom assets (hides hardcoded furniture)') - console.log() - - const confirm = await question('Rebuild extension now? (y/n): ') - if (confirm.toLowerCase() === 'y') { - try { - console.log('\n🔄 Building extension...') - execSync('npm run build', { stdio: 'inherit' }) - console.log('\n✅ Stage 6 complete! Extension ready to use.') - return true - } catch (err) { - console.log('\n❌ Build failed') - return false - } - } - return false -} - -async function runPipeline() { - displayBanner() - console.log('🎬 Starting new tileset import workflow...\n') - - const tilesetFile = await getFileInput( - '📁 Enter path to tileset PNG file: ', - '.png', - ) - - console.log('\n' + '═'.repeat(60)) - console.log('Running 6-stage asset extraction pipeline...') - console.log('═'.repeat(60)) - - const results: { [key: number]: boolean } = {} - - // Stage 1 - results[1] = await runStage1(tilesetFile) - if (!results[1]) return - - // Stage 2 - results[2] = await runStage2() - if (!results[2]) return - - // Stage 3 - results[3] = await runStage3() - if (!results[3]) return - - // Stage 4 - results[4] = await runStage4() - if (!results[4]) return - - // Stage 5 - results[5] = await runStage5() - if (!results[5]) return - - // Stage 6 - results[6] = await runStage6() - - // Final summary - console.log('\n' + '═'.repeat(60)) - console.log('🎉 PIPELINE COMPLETE!') - console.log('═'.repeat(60)) - console.log() - console.log('✨ Your tileset assets are now integrated into Pixel Agents!') - console.log() - console.log('📝 Summary:') - console.log(' Stage 1: ✅ Detection complete') - console.log(' Stage 2: ✅ Assets edited') - console.log(' Stage 3: ✅ Metadata generated') - console.log(' Stage 4: ✅ Metadata reviewed') - console.log(' Stage 5: ✅ Assets exported to assets/furniture/') - console.log(' Stage 6: ✅ Extension bundled') - console.log() - console.log('🚀 Next steps:') - console.log(' 1. Press F5 in VS Code to test the extension') - console.log(' 2. Your custom assets will be available in the editor') - console.log(' 3. Click "Edit" → "Place" to see all your furniture') - console.log() - console.log('To run another tileset, execute:') - console.log(' npx ts-node scripts/import-tileset-cli.ts') - console.log() -} - -async function main() { - displayBanner() - - let running = true - while (running) { - const choice = await displayMenu() - - switch (choice) { - case '1': - await runPipeline() - break - case '2': - displayStages() - break - case '3': - const stage = await question( - 'Enter stage number (1-6): ', - ) - console.log(`Stage ${stage} selected (not yet implemented)`) - break - case '4': - console.log('\n👋 Goodbye!\n') - running = false - break - default: - console.log('❌ Invalid option') - } - - if (running) { - const again = await question('\nContinue? (y/n): ') - if (again.toLowerCase() !== 'y') { - running = false - } - } - } - - rl.close() -} - -main() diff --git a/scripts/1-detect-assets.ts b/scripts/1-detect-assets.ts deleted file mode 100644 index fbf7e4da..00000000 --- a/scripts/1-detect-assets.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * Automatic Tileset Asset Detection - * - * Reads a tileset PNG, detects background color, and extracts individual assets - * using flood-fill algorithm. Auto-sizes each asset to nearest 16px multiple. - * - * Usage: - * npx ts-node scripts/detect-tileset-assets.ts assets/office_tileset_16x16.png - */ - -import { readFileSync, writeFileSync } from 'fs' -import { PNG } from 'pngjs' - -interface Pixel { - r: number - g: number - b: number - a: number -} - -interface DetectedAsset { - id: string - x: number - y: number - width: number - height: number - paddedX: number - paddedY: number - paddedWidth: number - paddedHeight: number -} - -interface DetectionOutput { - version: 1 - timestamp: string - sourceFile: string - tileset: { - width: number - height: number - } - backgroundColor: string - totalPixels: number - backgroundPixels: number - assets: DetectedAsset[] -} - -const args = process.argv.slice(2) -const pngPath = args[0] || './webview-ui/public/assets/office_tileset_16x16.png' - -console.log(`\n📷 Reading tileset: ${pngPath}`) - -// ───────────────────────────────────────────────────────────────────── -// Read PNG -// ───────────────────────────────────────────────────────────────────── - -const pngBuffer = readFileSync(pngPath) -const png = PNG.sync.read(pngBuffer) -const { width, height, data } = png - -console.log(` Dimensions: ${width}x${height} pixels`) - -// ───────────────────────────────────────────────────────────────────── -// Pixel access & color detection -// ───────────────────────────────────────────────────────────────────── - -function getPixel(x: number, y: number): Pixel { - if (x < 0 || x >= width || y < 0 || y >= height) { - return { r: 0, g: 0, b: 0, a: 0 } - } - const idx = (y * width + x) * 4 - return { r: data[idx], g: data[idx + 1], b: data[idx + 2], a: data[idx + 3] } -} - -function setPixel(x: number, y: number, p: Pixel): void { - const idx = (y * width + x) * 4 - data[idx] = p.r - data[idx + 1] = p.g - data[idx + 2] = p.b - data[idx + 3] = p.a -} - -function colorToHex(p: Pixel): string { - return `#${p.r.toString(16).padStart(2, '0')}${p.g.toString(16).padStart(2, '0')}${p.b.toString(16).padStart(2, '0')}${p.a.toString(16).padStart(2, '0')}` -} - -function pixelsEqual(p1: Pixel, p2: Pixel): boolean { - return p1.r === p2.r && p1.g === p2.g && p1.b === p2.b && p1.a === p2.a -} - -// ───────────────────────────────────────────────────────────────────── -// Detect background color -// ───────────────────────────────────────────────────────────────────── - -console.log(`\n🎨 Detecting background color...`) - -// Count color frequency -const colorMap = new Map() -let totalPixels = 0 -for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const p = getPixel(x, y) - const hex = colorToHex(p) - colorMap.set(hex, (colorMap.get(hex) || 0) + 1) - totalPixels++ - } -} - -// Most common color is likely background -let backgroundColor: Pixel -let bgHex: string -let bgCount: number -if (colorMap.has('00000000')) { - // If transparent exists, assume it's background - backgroundColor = { r: 0, g: 0, b: 0, a: 0 } - bgHex = '00000000' - bgCount = colorMap.get(bgHex) || 0 -} else { - // Otherwise use most frequent color - let maxCount = 0 - let maxHex = '' - for (const [hex, count] of colorMap) { - if (count > maxCount) { - maxCount = count - maxHex = hex - } - } - bgHex = maxHex - bgCount = maxCount - const r = parseInt(bgHex.slice(1, 3), 16) - const g = parseInt(bgHex.slice(3, 5), 16) - const b = parseInt(bgHex.slice(5, 7), 16) - const a = parseInt(bgHex.slice(7, 9), 16) - backgroundColor = { r, g, b, a } -} - -console.log(` Background: ${bgHex} (${bgCount}/${totalPixels} = ${((bgCount / totalPixels) * 100).toFixed(1)}%)`) - -// ───────────────────────────────────────────────────────────────────── -// Flood-fill to find all asset regions -// ───────────────────────────────────────────────────────────────────── - -console.log(`\n🔍 Extracting asset regions (flood-fill)...`) - -const visited = new Uint8Array(width * height) - -function floodFill(startX: number, startY: number): Set { - const region = new Set() - const queue: Array<[number, number]> = [[startX, startY]] - - while (queue.length > 0) { - const [x, y] = queue.shift()! - const idx = y * width + x - - if (x < 0 || x >= width || y < 0 || y >= height) continue - if (visited[idx]) continue - - const p = getPixel(x, y) - if (pixelsEqual(p, backgroundColor)) continue - - visited[idx] = 1 - region.add(idx) - - // Add neighbors (4-connected) - queue.push([x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]) - } - - return region -} - -const assets: DetectedAsset[] = [] -let assetId = 0 - -for (let y = 0; y < height; y++) { - for (let x = 0; x < width; x++) { - const idx = y * width + x - if (visited[idx]) continue - - const p = getPixel(x, y) - if (pixelsEqual(p, backgroundColor)) continue - - // Found start of new asset region - const region = floodFill(x, y) - if (region.size === 0) continue - - // Find bounding box - let minX = width, - maxX = -1, - minY = height, - maxY = -1 - for (const pixelIdx of region) { - const py = Math.floor(pixelIdx / width) - const px = pixelIdx % width - minX = Math.min(minX, px) - maxX = Math.max(maxX, px) - minY = Math.min(minY, py) - maxY = Math.max(maxY, py) - } - - const assetWidth = maxX - minX + 1 - const assetHeight = maxY - minY + 1 - - // Calculate padding to nearest 16px multiple - // Center horizontally, align to bottom vertically - const paddedWidth = Math.ceil(assetWidth / 16) * 16 - const paddedHeight = Math.ceil(assetHeight / 16) * 16 - - const paddedX = minX - Math.floor((paddedWidth - assetWidth) / 2) - const paddedY = minY - (paddedHeight - assetHeight) // bottom-aligned - - assets.push({ - id: `ASSET_${assetId++}`, - x: minX, - y: minY, - width: assetWidth, - height: assetHeight, - paddedX: Math.max(0, paddedX), - paddedY: Math.max(0, paddedY), - paddedWidth, - paddedHeight, - }) - } -} - -console.log(` Found ${assets.length} assets`) - -// ───────────────────────────────────────────────────────────────────── -// Sort by position (top-left to bottom-right) -// ───────────────────────────────────────────────────────────────────── - -assets.sort((a, b) => a.paddedY - b.paddedY || a.paddedX - b.paddedX) - -// ───────────────────────────────────────────────────────────────────── -// Output JSON -// ───────────────────────────────────────────────────────────────────── - -const output: DetectionOutput = { - version: 1, - timestamp: new Date().toISOString(), - sourceFile: pngPath, - tileset: { width, height }, - backgroundColor: bgHex, - totalPixels, - backgroundPixels: bgCount, - assets, -} - -const outputPath = './scripts/.tileset-working/tileset-detection-output.json' -writeFileSync(outputPath, JSON.stringify(output, null, 2)) - -console.log(`\n✅ Detection complete!`) -console.log(` Output: ${outputPath}`) -console.log(` Assets: ${assets.length}`) -console.log(` Background: ${bgHex} (${((bgCount / totalPixels) * 100).toFixed(1)}%)`) -console.log(`\n📋 Next step: Open scripts/asset-editor.html to review and edit assets\n`) diff --git a/scripts/2-asset-editor.html b/scripts/2-asset-editor.html deleted file mode 100644 index 62418887..00000000 --- a/scripts/2-asset-editor.html +++ /dev/null @@ -1,921 +0,0 @@ - - - - - Asset Editor - 3 Pane Resizable - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
- - - - - - - - - -
- -
- - - -
- -
- 0 splits -
-
- - -
- -
-

Assets (0)

-
-
- -
- - -
-

Preview

-
- -
-
- -
- - -
-

Tileset View

-
- -
-
-
- - -
Ready
- - - - - - - - diff --git a/scripts/3-vision-inspect.ts b/scripts/3-vision-inspect.ts deleted file mode 100644 index 7f461315..00000000 --- a/scripts/3-vision-inspect.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * Stage 3: Vision Inspection & Auto-Metadata Generation - * - * Uses Claude's vision API to analyze each asset and suggest: - * - Name, Label, Category - * - isDesk flag, canPlaceOnWalls flag - * - * Usage: - * npx ts-node scripts/inspect-assets.ts - * - * Requires: - * - asset-editor-output.json (approved assets) - * - assets/office_tileset_16x16.png (source tileset) - * - ANTHROPIC_API_KEY environment variable - */ - -import { readFileSync, writeFileSync } from 'fs' -import { join } from 'path' -import { PNG } from 'pngjs' -import Anthropic from '@anthropic-ai/sdk' - -// Load .env file -function loadEnv() { - try { - const envPath = join(__dirname, '..', '.env') - const envContent = readFileSync(envPath, 'utf-8') - const lines = envContent.split('\n') - for (const line of lines) { - const [key, ...valueParts] = line.split('=') - const value = valueParts.join('=').trim() - if (key && value) { - process.env[key.trim()] = value - } - } - } catch (err) { - // .env file not found, will rely on environment variable - } -} - -interface AssetWithMetadata { - id: string - paddedX: number - paddedY: number - paddedWidth: number - paddedHeight: number - erasedPixels?: Array<{ x: number; y: number }> - // Suggestions from vision - suggestedName?: string - suggestedLabel?: string - suggestedCategory?: string - suggestedIsDesk?: boolean - suggestedCanPlaceOnWalls?: boolean -} - -const pngPath = './webview-ui/public/assets/office_tileset_16x16.png' -const inputJsonPath = './scripts/.tileset-working/asset-editor-output.json' -const outputJsonPath = './scripts/.tileset-working/tileset-metadata-draft.json' - -console.log(`\n🔍 Stage 3: Vision Inspection & Auto-Metadata\n`) - -// ───────────────────────────────────────────────────────────────────── -// Load input data -// ───────────────────────────────────────────────────────────────────── - -console.log(`📖 Loading ${inputJsonPath}...`) -const inputData = JSON.parse(readFileSync(inputJsonPath, 'utf-8')) -const assets: AssetWithMetadata[] = inputData.assets - -console.log(`📷 Loading ${pngPath}...`) -const pngBuffer = readFileSync(pngPath) -const png = PNG.sync.read(pngBuffer) -const { width: pngWidth, height: pngHeight, data: pngData } = png - -console.log(` Found ${assets.length} assets to inspect\n`) - -// ───────────────────────────────────────────────────────────────────── -// Helper: Extract asset region as PNG buffer -// ───────────────────────────────────────────────────────────────────── - -function extractAssetPng(asset: AssetWithMetadata): Buffer { - const w = asset.paddedWidth - const h = asset.paddedHeight - - // Create new PNG for this asset - const assetPng = new PNG({ width: w, height: h }) - - // Copy pixels from tileset, handling out-of-bounds and erased pixels - const erasedSet = new Set( - (asset.erasedPixels || []).map((p) => `${p.x},${p.y}`), - ) - - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const sourceX = asset.paddedX + x - const sourceY = asset.paddedY + y - const isErased = erasedSet.has(`${x},${y}`) - - // Destination pixel index - const dstIdx = (y * w + x) << 2 - - // Check if pixel is out of bounds or erased - if ( - sourceX < 0 || - sourceX >= pngWidth || - sourceY < 0 || - sourceY >= pngHeight || - isErased - ) { - // Transparent (RGBA = 0,0,0,0) - assetPng.data[dstIdx] = 0 - assetPng.data[dstIdx + 1] = 0 - assetPng.data[dstIdx + 2] = 0 - assetPng.data[dstIdx + 3] = 0 - } else { - // Copy from source - const srcIdx = (sourceY * pngWidth + sourceX) << 2 - assetPng.data[dstIdx] = pngData[srcIdx] - assetPng.data[dstIdx + 1] = pngData[srcIdx + 1] - assetPng.data[dstIdx + 2] = pngData[srcIdx + 2] - assetPng.data[dstIdx + 3] = pngData[srcIdx + 3] - } - } - } - - return PNG.sync.write(assetPng) -} - -// ───────────────────────────────────────────────────────────────────── -// Vision analysis with Claude -// ───────────────────────────────────────────────────────────────────── - -async function analyzeAsset( - client: Anthropic, - asset: AssetWithMetadata, - pngBuffer: Buffer, - index: number, - total: number, -): Promise { - const base64 = pngBuffer.toString('base64') - - console.log(`[${index + 1}/${total}] Analyzing ${asset.id}...`) - - const prompt = `You are an expert at identifying pixel art furniture and objects. Analyze this pixel art image and provide metadata. - -Return ONLY valid JSON on a single line (no markdown, no explanation): -{ - "name": "UPPERCASE_SNAKE_CASE name (e.g., DESK_WOOD_SM, CHAIR_SPINNING, PLANT_POT)", - "label": "Human readable label (e.g., Wood Table Small, Spinning Chair)", - "category": "one of: desks, chairs, storage, decor, electronics, misc", - "isDesk": true/false, - "canPlaceOnWalls": true/false -} - -Guidelines: -- name: SCREAMING_SNAKE_CASE, descriptive, include size/style (SM/LG/WOOD/etc) -- label: Title Case, human friendly -- category: Pick the most specific category -- isDesk: true only if it's a desk/table where agents sit -- canPlaceOnWalls: true if item could be placed on wall tiles (e.g., wall art, shelves, clocks)` - - try { - const response = await client.messages.create({ - model: 'claude-opus-4-6', - max_tokens: 256, - messages: [ - { - role: 'user', - content: [ - { - type: 'image', - source: { - type: 'base64', - media_type: 'image/png', - data: base64, - }, - }, - { - type: 'text', - text: prompt, - }, - ], - }, - ], - }) - - // Extract JSON from response - const text = - response.content[0].type === 'text' ? response.content[0].text : '' - const jsonMatch = text.match(/\{[\s\S]*\}/) - if (!jsonMatch) { - console.warn(` ⚠️ No JSON found in response, skipping`) - return - } - - const data = JSON.parse(jsonMatch[0]) - asset.suggestedName = data.name - asset.suggestedLabel = data.label - asset.suggestedCategory = data.category - asset.suggestedIsDesk = data.isDesk - asset.suggestedCanPlaceOnWalls = data.canPlaceOnWalls - - console.log( - ` ✓ ${asset.suggestedName} | ${asset.suggestedLabel} | ${asset.suggestedCategory}`, - ) - } catch (err) { - console.warn(` ⚠️ Error: ${err instanceof Error ? err.message : err}`) - } -} - -// ───────────────────────────────────────────────────────────────────── -// Main -// ───────────────────────────────────────────────────────────────────── - -async function main() { - // Load .env file first - loadEnv() - - const apiKey = process.env.ANTHROPIC_API_KEY - if (!apiKey) { - console.error('❌ Error: ANTHROPIC_API_KEY not set') - console.error(' Add your key to .env file:') - console.error(' ANTHROPIC_API_KEY=sk-ant-...') - process.exit(1) - } - - const client = new Anthropic({ apiKey }) - - console.log(`🤖 Using Claude Opus 4.6 for vision analysis\n`) - - // Analyze each asset - for (let i = 0; i < assets.length; i++) { - const asset = assets[i] - const pngBuffer = extractAssetPng(asset) - await analyzeAsset(client, asset, pngBuffer, i, assets.length) - } - - console.log(`\n✅ Vision analysis complete!\n`) - - // Prepare output - const output = { - version: 1, - timestamp: new Date().toISOString(), - sourceFile: inputData.sourceFile, - tileset: inputData.tileset, - backgroundColor: inputData.backgroundColor, - assets: assets.map((a) => ({ - id: a.id, - paddedX: a.paddedX, - paddedY: a.paddedY, - paddedWidth: a.paddedWidth, - paddedHeight: a.paddedHeight, - erasedPixels: a.erasedPixels, - // Metadata suggestions (ready for user review) - name: a.suggestedName || a.id, - label: a.suggestedLabel || a.id, - category: a.suggestedCategory || 'misc', - footprintW: Math.max(1, Math.round(a.paddedWidth / 16)), - footprintH: Math.max(1, Math.round(a.paddedHeight / 16)), - isDesk: a.suggestedIsDesk || false, - canPlaceOnWalls: a.suggestedCanPlaceOnWalls || false, - discard: false, - })), - } - - // Write output - writeFileSync(outputJsonPath, JSON.stringify(output, null, 2)) - console.log(`📝 Metadata suggestions saved to: ${outputJsonPath}`) - - // Summary - const withSuggestions = assets.filter((a) => a.suggestedName).length - console.log(`\n📊 Summary:`) - console.log(` Total assets: ${assets.length}`) - console.log(` With metadata: ${withSuggestions}`) - console.log(` Success rate: ${((withSuggestions / assets.length) * 100).toFixed(1)}%`) - - console.log(`\n📋 Next step: Review metadata in Stage 4`) - console.log(` open scripts/metadata-editor.html\n`) -} - -main().catch(console.error) diff --git a/scripts/4-review-metadata.html b/scripts/4-review-metadata.html deleted file mode 100644 index f9bdf3af..00000000 --- a/scripts/4-review-metadata.html +++ /dev/null @@ -1,509 +0,0 @@ - - - - - Asset Review & Metadata Editor - - - - -
- - - - - - - - -
- -
- -
-

Assets (0)

- -
-
- - -
-
- - -
-

Preview

-
- -
-
- - -
-

Edit Metadata

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- - -
-
- -
- -
-
- - -
-
- - -
-
-
- -
- -
- - -
- -
- -
- -
- - -
-
- -
-
- - -
-
-
- -
- - -
-
-
- -
Ready
- - - - - - - diff --git a/scripts/5-export-assets.ts b/scripts/5-export-assets.ts deleted file mode 100644 index 044e65b9..00000000 --- a/scripts/5-export-assets.ts +++ /dev/null @@ -1,275 +0,0 @@ -/** - * Stage 5: Export Assets to Folder Structure + Catalog - * - * Reads final metadata and exports approved assets as PNG files - * organized by category, plus generates furniture-catalog.json - * - * Usage: - * npx ts-node scripts/export-tileset-assets.ts - * - * Requires: - * - tileset-metadata-final.json (approved metadata) - * - assets/office_tileset_16x16.png (source tileset) - */ - -import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs' -import { join } from 'path' -import { PNG } from 'pngjs' - -interface Asset { - id: string - paddedX: number - paddedY: number - paddedWidth: number - paddedHeight: number - erasedPixels?: Array<{ x: number; y: number }> - name: string - label: string - category: string - footprintW: number - footprintH: number - isDesk: boolean - canPlaceOnWalls: boolean - discard?: boolean - partOfGroup?: boolean - groupId?: string | null - orientation?: string - state?: string - canPlaceOnSurfaces?: boolean - backgroundTiles?: number -} - -interface CatalogEntry { - id: string - name: string - label: string - category: string - file: string - width: number - height: number - footprintW: number - footprintH: number - isDesk: boolean - canPlaceOnWalls?: boolean - groupId?: string - orientation?: string - state?: string - canPlaceOnSurfaces?: boolean - backgroundTiles?: number -} - -const metadataPath = './scripts/.tileset-working/tileset-metadata-final.json' -const tilesetPath = './webview-ui/public/assets/office_tileset_16x16.png' -const assetsDir = './webview-ui/public/assets/furniture' - -console.log(`\n📦 Stage 5: Export Assets to Folder Structure + Catalog\n`) - -// ───────────────────────────────────────────────────────────────────── -// Load input data -// ───────────────────────────────────────────────────────────────────── - -console.log(`📖 Loading ${metadataPath}...`) -const metadata = JSON.parse(readFileSync(metadataPath, 'utf-8')) -const assets: Asset[] = metadata.assets.filter((a: Asset) => !a.discard) - -console.log(`📷 Loading ${tilesetPath}...`) -const pngBuffer = readFileSync(tilesetPath) -const tileset = PNG.sync.read(pngBuffer) -const { width: tilesetWidth, height: tilesetHeight, data: tilesetData } = tileset - -console.log(` Found ${assets.length} assets to export\n`) - -// ───────────────────────────────────────────────────────────────────── -// Helper: Extract asset PNG with erased pixels as transparent -// ───────────────────────────────────────────────────────────────────── - -function extractAssetPng(asset: Asset): Buffer { - const w = asset.paddedWidth - const h = asset.paddedHeight - - const assetPng = new PNG({ width: w, height: h }) - const erasedSet = new Set( - (asset.erasedPixels || []).map((p) => `${p.x},${p.y}`), - ) - - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - const sourceX = asset.paddedX + x - const sourceY = asset.paddedY + y - const isErased = erasedSet.has(`${x},${y}`) - - const dstIdx = (y * w + x) << 2 - - // Out of bounds or erased = transparent - if ( - sourceX < 0 || - sourceX >= tilesetWidth || - sourceY < 0 || - sourceY >= tilesetHeight || - isErased - ) { - assetPng.data[dstIdx] = 0 - assetPng.data[dstIdx + 1] = 0 - assetPng.data[dstIdx + 2] = 0 - assetPng.data[dstIdx + 3] = 0 - } else { - // Copy from tileset - const srcIdx = (sourceY * tilesetWidth + sourceX) << 2 - assetPng.data[dstIdx] = tilesetData[srcIdx] - assetPng.data[dstIdx + 1] = tilesetData[srcIdx + 1] - assetPng.data[dstIdx + 2] = tilesetData[srcIdx + 2] - assetPng.data[dstIdx + 3] = tilesetData[srcIdx + 3] - } - } - } - - return PNG.sync.write(assetPng) -} - -// ───────────────────────────────────────────────────────────────────── -// Create folder structure -// ───────────────────────────────────────────────────────────────────── - -console.log(`🗑️ Cleaning old assets...`) - -if (existsSync(assetsDir)) { - rmSync(assetsDir, { recursive: true }) - console.log(` Removed ${assetsDir}`) -} - -console.log(`📁 Creating folder structure...`) - -mkdirSync(assetsDir, { recursive: true }) - -const categories = new Set(assets.map((a) => a.category)) -for (const category of categories) { - const categoryDir = join(assetsDir, category) - if (!existsSync(categoryDir)) { - mkdirSync(categoryDir, { recursive: true }) - } -} - -console.log(` Created ${categories.size} category folders\n`) - -// ───────────────────────────────────────────────────────────────────── -// Export assets and build catalog -// ───────────────────────────────────────────────────────────────────── - -console.log(`💾 Exporting assets...\n`) - -const catalog: CatalogEntry[] = [] -let exported = 0 - -for (const asset of assets) { - const categoryDir = join(assetsDir, asset.category) - const filename = `${asset.name}.png` - const filepath = join(categoryDir, filename) - const relativePath = `furniture/${asset.category}/${filename}` - - try { - const pngBuffer = extractAssetPng(asset) - writeFileSync(filepath, pngBuffer) - - const entry: CatalogEntry = { - id: asset.id, - name: asset.name, - label: asset.label, - category: asset.category, - file: relativePath, - width: asset.paddedWidth, - height: asset.paddedHeight, - footprintW: asset.footprintW, - footprintH: asset.footprintH, - isDesk: asset.isDesk, - } - - // Wall placement flag - if (asset.canPlaceOnWalls) { - entry.canPlaceOnWalls = true - } - - // Surface placement flag - if (asset.canPlaceOnSurfaces) { - entry.canPlaceOnSurfaces = true - } - - // Background tiles - if (asset.backgroundTiles && asset.backgroundTiles > 0) { - entry.backgroundTiles = asset.backgroundTiles - } - - // Rotation group: use explicit orientation if present, otherwise derive from name suffix - if (asset.groupId) { - entry.groupId = asset.groupId - if (asset.orientation) { - entry.orientation = asset.orientation - } else { - const suffix = asset.name.split('_').pop()?.toLowerCase() - if (suffix && ['front', 'back', 'left', 'right'].includes(suffix)) { - entry.orientation = suffix - } - } - } - - // State (on/off) - if (asset.state) { - entry.state = asset.state - } - - catalog.push(entry) - - console.log( - ` ✓ ${asset.category}/${filename.padEnd(30)} (${asset.paddedWidth}×${asset.paddedHeight}px)`, - ) - exported++ - } catch (err) { - console.warn( - ` ✗ ${asset.category}/${filename} - ${err instanceof Error ? err.message : err}`, - ) - } -} - -console.log(`\n✅ Exported ${exported} assets\n`) - -// ───────────────────────────────────────────────────────────────────── -// Generate furniture-catalog.json -// ───────────────────────────────────────────────────────────────────── - -const catalogPath = join(assetsDir, 'furniture-catalog.json') -const catalogOutput = { - version: 1, - timestamp: new Date().toISOString(), - totalAssets: catalog.length, - categories: Array.from(categories).sort(), - assets: catalog.sort((a, b) => a.id.localeCompare(b.id)), -} - -writeFileSync(catalogPath, JSON.stringify(catalogOutput, null, 2)) -console.log(`📋 Generated furniture-catalog.json`) -console.log(` Location: ${catalogPath}`) -console.log(` Assets: ${catalog.length}\n`) - -// ───────────────────────────────────────────────────────────────────── -// Summary by category -// ───────────────────────────────────────────────────────────────────── - -console.log(`📊 Summary by Category:`) -const byCat = new Map() -for (const cat of categories) { - const count = catalog.filter((a) => a.category === cat).length - byCat.set(cat, count) - console.log(` ${cat.padEnd(15)} ${count} assets`) -} - -console.log(`\n✅ Export complete!`) -console.log(`\n📂 Folder structure:`) -console.log(` assets/`) -console.log(` ├── furniture/`) -for (const cat of Array.from(categories).sort()) { - const count = byCat.get(cat) - console.log(` │ ├── ${cat}/ (${count} assets)`) -} -console.log(` │ └── furniture-catalog.json`) - -console.log(`\n📋 Next step: Stage 6 - Extension Integration`) -console.log(` The extension will load assets from assets/furniture-catalog.json\n`) diff --git a/scripts/asset-manager.html b/scripts/asset-manager.html index ada42ee6..a21d7fcc 100644 --- a/scripts/asset-manager.html +++ b/scripts/asset-manager.html @@ -46,10 +46,31 @@ .resize-handle { width: 4px; background: #333; cursor: col-resize; user-select: none; flex-shrink: 0; } .resize-handle:hover { background: #3498db; } - /* Center: Preview */ - #preview-panel { flex: 1; display: flex; flex-direction: column; background: #1a1a2e; border-right: 1px solid #333; min-width: 200px; } - #preview-header { padding: 8px 10px; font-size: 12px; font-weight: 600; border-bottom: 1px solid #333; background: #0f3460; display: flex; justify-content: space-between; align-items: center; } - #preview-header select { font-size: 11px; padding: 2px 6px; background: #2a2a4a; border: 1px solid #555; color: #e0e0e0; border-radius: 2px; cursor: pointer; } + /* Center: Split preview panel */ + #center-panel { flex: 1; display: flex; flex-direction: row; min-width: 400px; } + + /* Left half: Tileset */ + #tileset-pane { flex: 1; display: flex; flex-direction: column; background: #1a1a2e; border-right: 1px solid #333; min-width: 150px; } + #tileset-pane .pane-header { padding: 8px 10px; font-size: 12px; font-weight: 600; border-bottom: 1px solid #333; background: #0f3460; display: flex; justify-content: space-between; align-items: center; } + #tileset-pane .pane-header select { font-size: 11px; padding: 2px 6px; background: #2a2a4a; border: 1px solid #555; color: #e0e0e0; border-radius: 2px; cursor: pointer; } + #tileset-pane .pane-header label { font-size: 10px; color: #aaa; } + #tileset-wrap { flex: 1; overflow: auto; background: #111; position: relative; cursor: default; } + #tileset-canvas { display: block; image-rendering: pixelated; } + #select-rect { position: absolute; border: 2px solid #2ecc71; background: rgba(46,204,113,0.15); pointer-events: none; display: none; } + #tileset-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: #555; font-size: 12px; } + .add-mode-bar { padding: 5px 10px; background: #1a3a1a; border-bottom: 1px solid #2a5a2a; display: flex; gap: 10px; align-items: center; font-size: 11px; flex-shrink: 0; } + .add-mode-bar span { color: #8c8; } + .add-mode-bar button { font-size: 11px; padding: 3px 10px; background: #6b2020; border: 1px solid #8b3030; color: #e0e0e0; border-radius: 2px; cursor: pointer; } + .add-mode-bar button:hover { background: #8b3030; } + + /* Center resize handle */ + #handle-center { width: 4px; background: #333; cursor: col-resize; user-select: none; flex-shrink: 0; } + #handle-center:hover { background: #3498db; } + + /* Right half: Asset preview */ + #preview-pane { flex: 1; display: flex; flex-direction: column; background: #1a1a2e; min-width: 150px; } + #preview-pane .pane-header { padding: 8px 10px; font-size: 12px; font-weight: 600; border-bottom: 1px solid #333; background: #0f3460; display: flex; justify-content: space-between; align-items: center; } + #preview-pane .pane-header select { font-size: 11px; padding: 2px 6px; background: #2a2a4a; border: 1px solid #555; color: #e0e0e0; border-radius: 2px; cursor: pointer; } #preview-container { flex: 1; display: flex; align-items: center; justify-content: center; padding: 12px; overflow: auto; } #preview-canvas { image-rendering: pixelated; border: 2px solid #555; background: #000; } #preview-canvas.invalid { border-color: #e74c3c; } @@ -62,19 +83,8 @@ #eraser-info { font-size: 9px; color: #e74c3c; display: none; } #eraser-info.visible { display: inline; } - /* Tileset view (Add Asset mode) */ - #tileset-view { display: none; flex: 1; flex-direction: column; min-height: 0; } - #tileset-view.active { display: flex; } - #tileset-view .tv-bar { padding: 6px 10px; background: #1a3a1a; border-bottom: 1px solid #2a5a2a; display: flex; gap: 10px; align-items: center; font-size: 11px; flex-shrink: 0; } - #tileset-view .tv-bar span { color: #8c8; } - #tileset-view .tv-bar select, #tileset-view .tv-bar label { font-size: 11px; color: #aaa; } - #tileset-view .tv-bar select { padding: 2px 6px; background: #2a2a4a; border: 1px solid #555; color: #e0e0e0; border-radius: 2px; cursor: pointer; } - #tileset-view .tv-bar button { font-size: 11px; padding: 3px 10px; background: #6b2020; border: 1px solid #8b3030; color: #e0e0e0; border-radius: 2px; cursor: pointer; } - #tileset-view .tv-bar button:hover { background: #8b3030; } - #tileset-wrap { flex: 1; overflow: auto; background: #111; position: relative; cursor: crosshair; } - #tileset-canvas { display: block; image-rendering: pixelated; } - #select-rect { position: absolute; border: 2px solid #2ecc71; background: rgba(46,204,113,0.15); pointer-events: none; display: none; } - + .dropdown-item { display:block; width:100%; text-align:left; padding:6px 12px; font-size:12px; border:none; background:none; color:#e0e0e0; cursor:pointer; } + .dropdown-item:hover { background:#1a5276; } #toolbar button.add-btn { background: #1a5a2a; border-color: #2a8a3a; } #toolbar button.add-btn:hover { background: #2a8a3a; } #toolbar button.add-btn.active { background: #2ecc71; border-color: #27ae60; color: #000; font-weight: 600; } @@ -100,6 +110,8 @@ .geo-row input { flex: 1; min-width: 0; } .geo-row .step-btn { padding: 2px 6px; font-size: 10px; background: #2a2a4a; border: 1px solid #555; color: #e0e0e0; border-radius: 2px; cursor: pointer; flex-shrink: 0; line-height: 1; } .geo-row .step-btn:hover { background: #3a3a6a; } + .geo-row .step-spacer { width: 28px; flex-shrink: 0; } + .geo-row .fp-label { font-size: 9px; color: #6aa; width: 40px; text-align: right; flex-shrink: 0; } .field-row { display: flex; gap: 8px; } .field-row .field { flex: 1; } @@ -110,9 +122,50 @@ .checkbox-item input[type="checkbox"] { width: 15px; height: 15px; cursor: pointer; flex-shrink: 0; accent-color: #3498db; } .checkbox-item label { cursor: pointer; font-size: 11px; } - .rotation-fields { margin-top: 6px; display: none; } - .rotation-fields.visible { display: block; } - .rotation-fields .field { margin-bottom: 8px; } + .asset-item.multi-selected { border-color: #9b59b6; background: #2a1a3e; } + .badge-group { display: inline-block; padding: 1px 5px; border-radius: 2px; font-size: 8px; font-weight: 600; margin-left: 4px; vertical-align: middle; } + .badge-group-rotation { background: #1a4a6a; color: #5dade2; } + .badge-group-state { background: #4a3a0a; color: #f1c40f; } + .badge-group-animation { background: #1a4a2a; color: #2ecc71; } + .group-type-btn { font-size: 10px; padding: 4px 10px; border: 1px solid #555; background: #2a2a4a; color: #aaa; border-radius: 2px; cursor: pointer; flex: 1; text-align: center; } + .group-type-btn:hover { background: #3a3a6a; } + .group-type-btn.selected { background: #1a5276; border-color: #2980b9; color: #e0e0e0; font-weight: 600; } + .group-member-row { display: flex; gap: 6px; align-items: center; padding: 4px 0; border-bottom: 1px solid #2a2a4a; font-size: 11px; } + .group-member-row .gm-thumb { width: 24px; height: 24px; flex-shrink: 0; image-rendering: pixelated; background: #000; border: 1px solid #444; border-radius: 2px; } + .group-member-row .gm-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .group-member-row select { font-size: 10px; padding: 2px 4px; background: #2a2a4a; border: 1px solid #555; color: #e0e0e0; border-radius: 2px; width: 70px; flex-shrink: 0; } + + /* Group wrapper in asset list */ + .group-wrapper { + margin: 3px 4px; border: 1px solid #333; border-radius: 4px; background: #161630; overflow: hidden; + } + .group-wrapper.group-selected { border-color: #9b59b6; background: #1e1640; } + .group-wrapper .group-header { + padding: 4px 8px; font-size: 10px; font-weight: 600; color: #aaa; background: #1a1a3e; + border-bottom: 1px solid #2a2a4a; display: flex; justify-content: space-between; align-items: center; cursor: pointer; + } + .group-wrapper.group-selected .group-header { color: #c9a0dc; background: #231845; } + .group-wrapper .group-header .group-header-type { font-size: 8px; font-weight: 600; padding: 1px 5px; border-radius: 2px; } + .group-wrapper .group-header .group-header-type.rotation { background: #1a4a6a; color: #5dade2; } + .group-wrapper .group-header .group-header-type.state { background: #4a3a0a; color: #f1c40f; } + .group-wrapper .group-header .group-header-type.animation { background: #1a4a2a; color: #2ecc71; } + .group-wrapper .asset-item { margin: 0; border-radius: 0; border-left: none; border-right: none; border-top: none; border-bottom: 1px solid #2a2a4a; } + .group-wrapper .asset-item:last-child { border-bottom: none; } + + /* Compound group: member unit rows */ + .group-member-row.group-unit { background: #1a1a3e; border-radius: 2px; padding: 5px 6px; } + .group-member-row .gm-type-badge { font-size: 8px; font-weight: 600; padding: 1px 5px; border-radius: 2px; margin-right: 4px; flex-shrink: 0; } + .gm-type-badge.animation { background: #1a4a2a; color: #2ecc71; } + .gm-type-badge.state { background: #4a3a0a; color: #f1c40f; } + .gm-type-badge.rotation { background: #1a4a6a; color: #5dade2; } + /* Nested group wrappers */ + .group-wrapper .group-wrapper { margin: 0; border-left: 2px solid #444; border-top: none; border-right: none; border-bottom: none; border-radius: 0; background: #141428; } + .group-wrapper .group-wrapper.group-selected { border-left-color: #9b59b6; background: #1e1640; } + .group-wrapper .group-wrapper .group-header { background: #161630; font-size: 9px; padding: 3px 8px; } + .group-wrapper .group-wrapper.group-selected .group-header { color: #c9a0dc; background: #231845; } + .group-wrapper .group-wrapper .asset-item { background: #141428; } + .group-wrapper .group-wrapper .asset-item.selected { border-color: #3498db; background: #1a3a5e; } + .group-wrapper .group-wrapper .asset-item.multi-selected { border-color: #9b59b6; background: #2a1a3e; } .discard-section { padding: 8px 10px; margin-top: 6px; background: #4a1a1a; border: 1px solid #6b2020; border-radius: 3px; } .discard-section label { color: #ff9988; font-weight: 600; } @@ -139,8 +192,17 @@
- - +
+ + +
+ - - - - - + +
+
@@ -175,53 +231,65 @@ 0 / 0
- -
-
- Preview -
- - - click/drag to erase pixels - - + +
+ +
+
+ Tileset +
+ + + +
-
-
- -
- -
-
- Drag to select a region on the tileset - - - +
+
Load a PNG to view the tileset
+
+ +
+ + +
+
+ Asset Preview +
+ + + click/drag to erase pixels + + +
+
+
+ +
@@ -252,104 +320,139 @@
- - - + + + +
- - - + + + +
Metadata
-
- - -
-
- - -
- - + +
-
+
- - + +
-
- - + +
+
+ + +
Top rows where other items can be placed behind (shown blue in preview)
+
-
-
-
- - -
Top rows where other items can be placed behind (shown blue in preview)
+ +
Flags
+
+
+ + +
+
+ + +
- -
Flags
-
-
- - -
-
- - -
-
- - + + - -
Rotation Group
-
-
- - + + -
- - + +
- - + + + + + + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
@@ -365,6 +468,43 @@
+ +
+
+
+ + + @@ -386,7 +526,11 @@ let data = null // Full JSON (version, assets[], etc.) let tilesetImg = null // Loaded tileset Image -let selectedIdx = -1 // Index into data.assets +let selectedIdx = -1 // Index into data.assets (primary selection for form/preview) +let selectedIndices = new Set() // Multi-select set +let lastClickedIdx = -1 // Anchor for shift-click range select +let selectedGroupId = null // Currently group-selected group ID (first click selects group, second drills into item) +let selectedGroupUnits = new Set() // Set of groupIds selected as compound units (for Ctrl+click on group headers) let filtered = [] // Indices into data.assets matching current filter let previewZoom = 4 let savedSnapshot = null // Deep copy for Reset button @@ -421,6 +565,33 @@ // FILE LOADING // ════════════════════════════════════════════════════════════════════════ +// New Project — start with empty data +// File dropdown menu toggle +const fileMenuBtn = document.getElementById('btn-file-menu') +const fileDropdown = document.getElementById('file-dropdown') +fileMenuBtn.onclick = (e) => { + e.stopPropagation() + fileDropdown.style.display = fileDropdown.style.display === 'none' ? 'block' : 'none' +} +document.addEventListener('click', () => { fileDropdown.style.display = 'none' }) +fileDropdown.addEventListener('click', () => { fileDropdown.style.display = 'none' }) + +document.getElementById('btn-new-project').onclick = () => { + if (data && data.assets.length > 0) { + if (!confirm('Start a new project? Unsaved changes will be lost.')) return + } + jsonFileHandle = null + loadJsonData({ + version: 1, + timestamp: new Date().toISOString(), + sourceFile: '', + tileset: '', + backgroundColor: null, + assets: [] + }) + statusMsg.textContent = 'New project created — load a PNG and start adding assets' +} + document.getElementById('btn-load-json').onclick = async () => { // Try File System Access API first (Chromium) — gives us a handle for direct save-back if (window.showOpenFilePicker) { @@ -469,9 +640,22 @@ const img = new Image() img.onload = () => { tilesetImg = img + // Auto-initialize empty project if no data loaded yet + if (!data) { + loadJsonData({ + version: 1, + timestamp: new Date().toISOString(), + sourceFile: file.name, + tileset: file.name, + backgroundColor: null, + assets: [] + }) + } + updateTilesetVisibility() + renderTileset() renderList() renderPreview() - statusMsg.textContent = '\u2713 Tileset loaded: ' + img.width + 'x' + img.height + 'px' + statusMsg.textContent = '\u2713 Tileset loaded: ' + img.width + 'x' + img.height + 'px' + (!data || data.assets.length === 0 ? ' — click "+ Add Asset" to start defining assets' : '') } img.src = ev.target.result } @@ -481,13 +665,21 @@ function loadJsonData(parsed) { data = parsed + migrateGroupsFromAssets() + selectedGroupId = null + selectedGroupUnits.clear() selectedIdx = data.assets.length > 0 ? 0 : -1 + selectedIndices.clear() + if (selectedIdx >= 0) selectedIndices.add(selectedIdx) + lastClickedIdx = selectedIdx savedSnapshot = JSON.parse(JSON.stringify(data)) undoStack.length = 0 redoStack.length = 0 updateFilter() if (selectedIdx >= 0) loadForm(selectedIdx) + updateEditorForSelection() renderPreview() + renderTileset() updateCounts() scheduleSave() updateSaveButton() @@ -498,7 +690,7 @@ if (!data) { btn.style.display = 'none' } else { - btn.style.display = '' + btn.style.display = 'block' if (jsonFileHandle) { btn.textContent = 'Save' btn.title = 'Write directly to ' + jsonFileHandle.name @@ -511,11 +703,14 @@ // Auto-load from localStorage window.addEventListener('load', () => { + updateTilesetVisibility() try { const saved = localStorage.getItem('asset-manager-data') if (saved) { loadJsonData(JSON.parse(saved)) - statusMsg.textContent = 'Restored from localStorage' + statusMsg.textContent = 'Restored from localStorage (' + data.assets.length + ' assets)' + } else { + statusMsg.textContent = 'Click "New" to start a new project, or "Load JSON" to open an existing one' } } catch {} }) @@ -538,31 +733,41 @@ // ════════════════════════════════════════════════════════════════════════ function pushUndo() { - undoStack.push(JSON.stringify(data.assets)) + undoStack.push(JSON.stringify({ assets: data.assets, groups: data.groups || [] })) if (undoStack.length > MAX_UNDO) undoStack.shift() redoStack.length = 0 } function undo() { if (undoStack.length === 0) return - redoStack.push(JSON.stringify(data.assets)) - data.assets = JSON.parse(undoStack.pop()) + redoStack.push(JSON.stringify({ assets: data.assets, groups: data.groups || [] })) + const restored = JSON.parse(undoStack.pop()) + data.assets = restored.assets + data.groups = restored.groups refreshAfterUndoRedo() } function redo() { if (redoStack.length === 0) return - undoStack.push(JSON.stringify(data.assets)) - data.assets = JSON.parse(redoStack.pop()) + undoStack.push(JSON.stringify({ assets: data.assets, groups: data.groups || [] })) + const restored = JSON.parse(redoStack.pop()) + data.assets = restored.assets + data.groups = restored.groups refreshAfterUndoRedo() } function refreshAfterUndoRedo() { // Clamp selectedIdx + selectedGroupId = null + selectedGroupUnits.clear() if (selectedIdx >= data.assets.length) selectedIdx = data.assets.length - 1 + selectedIndices.clear() + if (selectedIdx >= 0) selectedIndices.add(selectedIdx) updateFilter() if (selectedIdx >= 0) loadForm(selectedIdx) + updateEditorForSelection() renderPreview() + renderTileset() updateCounts() scheduleSave() statusMsg.textContent = 'Undo/Redo applied' @@ -573,25 +778,19 @@ // ════════════════════════════════════════════════════════════════════════ document.getElementById('filter-cat').onchange = () => updateFilter() -document.getElementById('filter-show').onchange = () => updateFilter() document.getElementById('search').oninput = () => updateFilter() function updateFilter() { if (!data) return const search = document.getElementById('search').value.toLowerCase() const cat = document.getElementById('filter-cat').value - const show = document.getElementById('filter-show').value - filtered = [] data.assets.forEach((asset, idx) => { if (cat && asset.category !== cat) return - if (show === 'approved' && asset.discard) return - if (show === 'discarded' && !asset.discard) return if (search) { - const name = (asset.name || '').toLowerCase() const label = (asset.label || '').toLowerCase() const id = (asset.id || '').toLowerCase() - if (!name.includes(search) && !label.includes(search) && !id.includes(search)) return + if (!label.includes(search) && !id.includes(search)) return } filtered.push(idx) }) @@ -612,65 +811,361 @@ // ASSET LIST RENDERING // ════════════════════════════════════════════════════════════════════════ +function renderAssetItem(idx) { + const asset = data.assets[idx] + const isSel = idx === selectedIdx + const isMulti = selectedIndices.has(idx) && selectedIndices.size > 1 + const div = document.createElement('div') + div.className = 'asset-item' + (isSel ? ' selected' : '') + (isMulti && !isSel ? ' multi-selected' : '') + (asset.discard ? ' discarded' : '') + div.dataset.idx = idx + div.onclick = (e) => { e.stopPropagation(); handleListClick(idx, e) } + + // Thumbnail + const thumb = document.createElement('canvas') + thumb.width = 40; thumb.height = 40 + thumb.className = 'asset-thumb' + if (tilesetImg && asset.paddedWidth > 0 && asset.paddedHeight > 0) { + const tctx = thumb.getContext('2d') + tctx.imageSmoothingEnabled = false + const srcX = Math.max(0, asset.paddedX) + const srcY = Math.max(0, asset.paddedY) + const offX = Math.max(0, -asset.paddedX) + const offY = Math.max(0, -asset.paddedY) + const srcW = asset.paddedWidth - offX + const srcH = asset.paddedHeight - offY + if (srcW > 0 && srcH > 0 && srcX < tilesetImg.width && srcY < tilesetImg.height) { + const s = Math.min(40 / asset.paddedWidth, 40 / asset.paddedHeight) + const dw = asset.paddedWidth * s + const dh = asset.paddedHeight * s + const dx = (40 - dw) / 2 + offX * s + const dy = (40 - dh) / 2 + offY * s + tctx.drawImage(tilesetImg, + srcX, srcY, Math.min(srcW, tilesetImg.width - srcX), Math.min(srcH, tilesetImg.height - srcY), + dx, dy, Math.min(srcW, tilesetImg.width - srcX) * s, Math.min(srcH, tilesetImg.height - srcY) * s + ) + } + } + div.appendChild(thumb) + + // Info + const meta = document.createElement('div') + meta.className = 'asset-meta' + const group = findGroupForAsset(asset) + let nameHtml = '
' + escHtml(asset.id || asset.label) + '
' + nameHtml += '
' + asset.paddedWidth + '\u00d7' + asset.paddedHeight + 'px' + const listFpW = Math.max(1, Math.round(asset.paddedWidth / 16)) + const listFpH = Math.max(1, Math.round(asset.paddedHeight / 16)) + nameHtml += ' \u2022 ' + listFpW + '\u00d7' + listFpH + ' tiles' + nameHtml += '
' + // Show role within group (no need for full group badge since we have the wrapper) + if (group) { + let role = '' + if (group.memberRoles && group.memberRoles[asset.id]) { + role = group.memberRoles[asset.id] + } else if (group.type === 'rotation') { + role = asset.orientation || '?' + } else if (group.type === 'state') { + role = asset.state || '?' + } else { + role = 'Frame ' + (group.members.indexOf(asset.id) + 1) + } + nameHtml += '
' + escHtml(role) + '
' + } else { + nameHtml += '
' + escHtml(asset.category || '') + if (asset.discard) nameHtml += ' DISCARDED' + nameHtml += '
' + } + meta.innerHTML = nameHtml + div.appendChild(meta) + + return div +} + function renderList() { if (!data) return assetListEl.innerHTML = '' - filtered.forEach(idx => { + // Build set of asset indices that belong to sub-groups (whose parent is a compound group) + // These should be rendered inside their parent group wrapper, not standalone + const subGroupAssetIndices = new Set() + const topLevelGroupIds = new Set() + if (data.groups) { + for (const g of data.groups) { + if (g.members.some(m => m.startsWith('@'))) { + // This is a compound group — collect all leaf assets from sub-groups + for (const mid of g.members) { + if (mid.startsWith('@')) { + const leafIds = resolveGroupAssets(mid) + for (const lid of leafIds) { + const idx = data.assets.findIndex(a => a.id === lid) + if (idx >= 0) subGroupAssetIndices.add(idx) + } + } + } + } + } + // Identify top-level groups (not sub-groups of another) + for (const g of data.groups) { + if (!findParentGroup(g.id)) topLevelGroupIds.add(g.id) + } + } + + // Pre-collect filtered indices per top-level group + const groupFilteredMap = {} // groupId → [filtered indices] + const ungroupedOrdered = [] // { type: 'item', idx } | { type: 'group', groupId } + const seenGroups = new Set() + + for (const idx of filtered) { const asset = data.assets[idx] - const div = document.createElement('div') - div.className = 'asset-item' + (idx === selectedIdx ? ' selected' : '') + (asset.discard ? ' discarded' : '') - div.dataset.idx = idx - div.onclick = () => selectAsset(idx) - - // Thumbnail - const thumb = document.createElement('canvas') - thumb.width = 40; thumb.height = 40 - thumb.className = 'asset-thumb' - if (tilesetImg && asset.paddedWidth > 0 && asset.paddedHeight > 0) { - const tctx = thumb.getContext('2d') - tctx.imageSmoothingEnabled = false - const srcX = Math.max(0, asset.paddedX) - const srcY = Math.max(0, asset.paddedY) - const offX = Math.max(0, -asset.paddedX) - const offY = Math.max(0, -asset.paddedY) - const srcW = asset.paddedWidth - offX - const srcH = asset.paddedHeight - offY - if (srcW > 0 && srcH > 0 && srcX < tilesetImg.width && srcY < tilesetImg.height) { - const s = Math.min(40 / asset.paddedWidth, 40 / asset.paddedHeight) - const dw = asset.paddedWidth * s - const dh = asset.paddedHeight * s - const dx = (40 - dw) / 2 + offX * s - const dy = (40 - dh) / 2 + offY * s - tctx.drawImage(tilesetImg, - srcX, srcY, Math.min(srcW, tilesetImg.width - srcX), Math.min(srcH, tilesetImg.height - srcY), - dx, dy, Math.min(srcW, tilesetImg.width - srcX) * s, Math.min(srcH, tilesetImg.height - srcY) * s - ) + const group = findGroupForAsset(asset) + if (group) { + // Find the top-level group for this asset + let topGroup = group + let parent = findParentGroup(topGroup.id) + while (parent) { topGroup = parent; parent = findParentGroup(topGroup.id) } + + if (!seenGroups.has(topGroup.id)) { + seenGroups.add(topGroup.id) + groupFilteredMap[topGroup.id] = [] + ungroupedOrdered.push({ type: 'group', groupId: topGroup.id }) + } + groupFilteredMap[topGroup.id].push(idx) + } else { + ungroupedOrdered.push({ type: 'item', idx }) + } + } + + // Helper to render a group wrapper (possibly with nested sub-group wrappers) + function renderGroupWrapper(groupId, filteredIndices) { + const group = data.groups.find(g => g.id === groupId) + if (!group) return null + + const wrapper = document.createElement('div') + const isSelected = selectedGroupId === groupId || selectedGroupUnits.has(groupId) + wrapper.className = 'group-wrapper' + (isSelected ? ' group-selected' : '') + wrapper.dataset.groupId = groupId + + // Group header + const header = document.createElement('div') + header.className = 'group-header' + let typeLabel = group.type === 'rotation' ? 'Rotation' : group.type === 'state' ? 'State' : 'Animation' + if (group.type === 'rotation' && group.rotationScheme && group.rotationScheme !== '4-way') { + typeLabel += ' (' + (group.rotationScheme === '3-way-mirror' ? '3+M' : '2') + ')' + } + const memberCount = filteredIndices.length + header.innerHTML = '' + escHtml(group.name || group.id) + ' (' + memberCount + ')' + + '' + escHtml(typeLabel) + '' + header.onclick = (e) => { e.stopPropagation(); handleGroupHeaderClick(groupId, e) } + wrapper.appendChild(header) + + // Check if this is a compound group + const hasSubGroups = group.members.some(m => m.startsWith('@')) + + if (hasSubGroups) { + // Render members in order, with sub-group wrappers for @refs + for (const mid of group.members) { + if (mid.startsWith('@')) { + const subGroupId = mid.slice(1) + // Collect filtered indices that belong to this sub-group + const subLeafIds = new Set(resolveGroupAssets(mid)) + const subIndices = filteredIndices.filter(idx => subLeafIds.has(data.assets[idx].id)) + if (subIndices.length > 0) { + const subWrapper = renderGroupWrapper(subGroupId, subIndices) + if (subWrapper) wrapper.appendChild(subWrapper) + } + } else { + // Direct asset member + const idx = data.assets.findIndex(a => a.id === mid) + if (idx >= 0 && filteredIndices.includes(idx)) { + wrapper.appendChild(renderAssetItem(idx)) + } + } + } + } else { + // Simple group: render all items directly + for (const gIdx of filteredIndices) { + wrapper.appendChild(renderAssetItem(gIdx)) } } - div.appendChild(thumb) - // Info - const meta = document.createElement('div') - meta.className = 'asset-meta' - let nameHtml = '
' + escHtml(asset.name || asset.id) + '
' - nameHtml += '
' + asset.paddedWidth + '\u00d7' + asset.paddedHeight + 'px' - if (asset.footprintW && asset.footprintH) nameHtml += ' \u2022 ' + asset.footprintW + '\u00d7' + asset.footprintH + ' tiles' - nameHtml += '
' - nameHtml += '
' + escHtml(asset.category || '') - if (asset.discard) nameHtml += ' DISCARDED' - nameHtml += '
' - meta.innerHTML = nameHtml - div.appendChild(meta) + return wrapper + } - assetListEl.appendChild(div) - }) + // Render in order + for (const entry of ungroupedOrdered) { + if (entry.type === 'item') { + assetListEl.appendChild(renderAssetItem(entry.idx)) + } else { + const groupId = entry.groupId + const groupIndices = groupFilteredMap[groupId] + if (!groupIndices || groupIndices.length === 0) continue + const wrapper = renderGroupWrapper(groupId, groupIndices) + if (wrapper) assetListEl.appendChild(wrapper) + } + } // Scroll selected into view const sel = assetListEl.querySelector('.selected') if (sel) sel.scrollIntoView({ block: 'nearest' }) } +// Click on group header — select all group members (multi-select) +// With Ctrl: add group as a compound unit for compound group creation +function handleGroupHeaderClick(groupId, e) { + if (!data || !data.groups) return + const group = data.groups.find(g => g.id === groupId) + if (!group) return + + // Resolve all leaf asset indices for this group (handles @sub-group refs) + function resolveGroupIndices(g) { + const indices = [] + for (const mid of g.members) { + if (mid.startsWith('@')) { + const subGroup = data.groups.find(sg => sg.id === mid.slice(1)) + if (subGroup) indices.push(...resolveGroupIndices(subGroup)) + } else { + const idx = data.assets.findIndex(a => a.id === mid) + if (idx >= 0) indices.push(idx) + } + } + return indices + } + + if (e && e.ctrlKey) { + // Ctrl+click: toggle this group as a compound unit + selectedGroupId = null + if (selectedGroupUnits.has(groupId)) { + // Toggle off: remove group and its member indices + selectedGroupUnits.delete(groupId) + const groupIndices = resolveGroupIndices(group) + groupIndices.forEach(i => selectedIndices.delete(i)) + } else { + // Toggle on: add group and its member indices + selectedGroupUnits.add(groupId) + const groupIndices = resolveGroupIndices(group) + groupIndices.forEach(i => selectedIndices.add(i)) + } + selectedIdx = selectedIndices.size === 1 ? [...selectedIndices][0] : -1 + lastClickedIdx = [...selectedIndices][0] || -1 + } else { + // Normal click: select just this group + selectedGroupId = groupId + selectedGroupUnits = new Set([groupId]) + selectedIndices.clear() + const groupIndices = resolveGroupIndices(group) + groupIndices.forEach(i => selectedIndices.add(i)) + selectedIdx = -1 + lastClickedIdx = [...selectedIndices][0] || -1 + } + + deactivateEraser() + updateEditorForSelection() + renderList() + renderPreview() + renderTileset() +} + +function handleListClick(idx, e) { + const asset = data.assets[idx] + const group = findGroupForAsset(asset) + + if (e.shiftKey && lastClickedIdx >= 0) { + // Range select — keep existing group units, add range to selectedIndices + selectedGroupId = null + const posA = filtered.indexOf(lastClickedIdx) + const posB = filtered.indexOf(idx) + if (posA >= 0 && posB >= 0) { + const lo = Math.min(posA, posB), hi = Math.max(posA, posB) + if (!e.ctrlKey) { + // Clear only non-group-unit indices, keep group unit indices + const groupUnitIndices = new Set() + for (const gid of selectedGroupUnits) { + const g = data.groups.find(gr => gr.id === gid) + if (g) { + for (const mid of g.members) { + for (const aid of resolveGroupAssets(mid)) { + const ai = data.assets.findIndex(a => a.id === aid) + if (ai >= 0) groupUnitIndices.add(ai) + } + } + } + } + selectedIndices.clear() + groupUnitIndices.forEach(i => selectedIndices.add(i)) + } + for (let i = lo; i <= hi; i++) selectedIndices.add(filtered[i]) + } + } else if (e.ctrlKey) { + // Toggle individual asset — preserve existing group units + selectedGroupId = null + if (selectedIndices.has(idx)) selectedIndices.delete(idx) + else selectedIndices.add(idx) + lastClickedIdx = idx + } else if (group) { + // Grouped item: drill-down logic + // Build the full ancestor chain for this asset's group: [directGroup, parent, grandparent, ...] + const ancestorChain = [] + let cur = group + while (cur) { + ancestorChain.push(cur) + cur = findParentGroup(cur.id) + if (cur) cur = data.groups.find(g => g.id === cur.id) + } + // ancestorChain[0] = direct group, ancestorChain[last] = top-level group + + if (!selectedGroupId) { + // No group selected yet: select the top-level group + const topGroup = ancestorChain[ancestorChain.length - 1] + handleGroupHeaderClick(topGroup.id) + return + } + + // Find where selectedGroupId sits in the chain + const selectedLevel = ancestorChain.findIndex(g => g.id === selectedGroupId) + if (selectedLevel < 0) { + // selectedGroupId is not an ancestor of this asset — select its top-level group + const topGroup = ancestorChain[ancestorChain.length - 1] + handleGroupHeaderClick(topGroup.id) + return + } + + if (selectedLevel > 0) { + // Drill down one level: select the next sub-group + const nextGroup = ancestorChain[selectedLevel - 1] + handleGroupHeaderClick(nextGroup.id) + return + } + + // selectedLevel === 0: we're at the direct group already, drill into individual asset + selectedGroupId = null + selectedGroupUnits.clear() + selectedIndices.clear() + selectedIndices.add(idx) + lastClickedIdx = idx + } else { + // Normal click (ungrouped item) + selectedGroupId = null + selectedGroupUnits.clear() + selectedIndices.clear() + selectedIndices.add(idx) + lastClickedIdx = idx + } + + // Update selectedIdx: if exactly one selected, use it; otherwise -1 + if (selectedIndices.size === 1) { + selectedIdx = [...selectedIndices][0] + } else { + selectedIdx = -1 + } + + deactivateEraser() + if (selectedIdx >= 0) loadForm(selectedIdx) + updateEditorForSelection() + renderList() + renderPreview() + renderTileset() +} + function escHtml(s) { return s.replace(/&/g, '&').replace(//g, '>') } @@ -681,11 +1176,18 @@ function selectAsset(idx) { if (!data || idx < 0 || idx >= data.assets.length) return + selectedGroupId = null + selectedGroupUnits.clear() + selectedIndices.clear() + selectedIndices.add(idx) + lastClickedIdx = idx selectedIdx = idx deactivateEraser() loadForm(idx) + updateEditorForSelection() renderList() renderPreview() + renderTileset() } function deactivateEraser() { @@ -697,47 +1199,117 @@ previewCanvas.classList.remove('eraser-active') } +// Convert a label string to an ID: uppercase, spaces/hyphens → underscores, strip non-alphanumeric +function labelToId(label) { + return label.trim().toUpperCase().replace(/[\s\-]+/g, '_').replace(/[^A-Z0-9_]/g, '') +} + +// Deduplicate IDs: when multiple assets share the same ID, append _2, _3, etc. +// Preserves manually-set and group-assigned IDs; only deduplicates collisions. +function deduplicateIds() { + if (!data) return + const seen = {} // id → count seen so far + for (const asset of data.assets) { + // Use the asset's current ID (which may have been manually set or group-assigned) + const baseId = asset.id || labelToId(asset.label) || 'UNNAMED' + if (!seen[baseId]) { + seen[baseId] = 1 + asset.id = baseId + } else { + seen[baseId]++ + asset.id = baseId + '_' + seen[baseId] + } + asset.name = asset.id + } +} + +// Track whether user has manually edited the ID field +let idManuallyEdited = false + +// Live-update ID preview as label is typed (unless user manually edited ID) +document.getElementById('f-label').oninput = () => { + if (idManuallyEdited) return + const label = document.getElementById('f-label').value + document.getElementById('f-id').value = labelToId(label) || '' +} + +// Mark ID as manually edited when user types in it +document.getElementById('f-id').oninput = () => { + idManuallyEdited = true +} + +// Track whether user has manually edited the group ID field +let groupIdManuallyEdited = false + +// Live-update group ID from group name +document.getElementById('group-name-input').oninput = () => { + if (groupIdManuallyEdited) return + const name = document.getElementById('group-name-input').value + document.getElementById('group-id-input').value = labelToId(name) || '' +} + +// Mark group ID as manually edited when user types in it +document.getElementById('group-id-input').oninput = () => { + groupIdManuallyEdited = true +} + function loadForm(idx) { const a = data.assets[idx] document.getElementById('geo-x').value = a.paddedX document.getElementById('geo-y').value = a.paddedY document.getElementById('geo-w').value = a.paddedWidth document.getElementById('geo-h').value = a.paddedHeight - document.getElementById('f-id').value = a.id - document.getElementById('f-name').value = a.name || '' document.getElementById('f-label').value = a.label || '' + document.getElementById('f-id').value = a.id || labelToId(a.label || '') + // Reset manual edit tracking — if the current ID matches labelToId, it wasn't manually edited + idManuallyEdited = !!(a.id && a.id !== labelToId(a.label || '')) document.getElementById('f-category').value = a.category || 'misc' - document.getElementById('f-fpw').value = a.footprintW || 1 - document.getElementById('f-fph').value = a.footprintH || 1 + updateFootprintDisplay() document.getElementById('f-bgtiles').value = a.backgroundTiles || 0 - document.getElementById('f-isdesk').checked = !!a.isDesk document.getElementById('f-walls').checked = !!a.canPlaceOnWalls document.getElementById('f-ontop').checked = !!a.canPlaceOnSurfaces - document.getElementById('f-grouped').checked = !!a.partOfGroup - document.getElementById('f-group-id').value = a.groupId || '' - document.getElementById('f-orientation').value = a.orientation || 'front' document.getElementById('f-discard').checked = !!a.discard - updateRotationVisibility() + updateGroupMembershipDisplay(a) } -function updateRotationVisibility() { - const visible = document.getElementById('f-grouped').checked - document.getElementById('rotation-fields').classList.toggle('visible', visible) +// Compute footprint from W/H and update the display labels +function updateFootprintDisplay() { + const w = +document.getElementById('geo-w').value || 0 + const h = +document.getElementById('geo-h').value || 0 + const fpW = Math.max(1, Math.round(w / 16)) + const fpH = Math.max(1, Math.round(h / 16)) + document.getElementById('fp-w').textContent = fpW + ' tile' + (fpW > 1 ? 's' : '') + document.getElementById('fp-h').textContent = fpH + ' tile' + (fpH > 1 ? 's' : '') } -document.getElementById('f-grouped').onchange = updateRotationVisibility - -// Footprint + background tiles live preview -;['f-fpw', 'f-fph', 'f-bgtiles'].forEach(id => { - document.getElementById(id).oninput = () => { - renderPreview() - } -}) +// Background tiles live preview +document.getElementById('f-bgtiles').oninput = () => { + renderPreview() +} // ════════════════════════════════════════════════════════════════════════ // GEOMETRY INPUTS (live preview) // ════════════════════════════════════════════════════════════════════════ +// Snap W/H to multiples of 16 on blur +;['geo-w', 'geo-h'].forEach(id => { + document.getElementById(id).onblur = () => { + const el = document.getElementById(id) + const raw = +el.value + const snapped = Math.max(16, Math.round(raw / 16) * 16) + if (snapped !== raw) el.value = snapped + // Push to asset + if (selectedIdx >= 0 && data) { + const a = data.assets[selectedIdx] + a.paddedWidth = +document.getElementById('geo-w').value || 16 + a.paddedHeight = +document.getElementById('geo-h').value || 16 + updateFootprintDisplay() + renderPreview() + renderTileset() + } + } +}) + // Number input live update ;['geo-x', 'geo-y', 'geo-w', 'geo-h'].forEach(id => { document.getElementById(id).oninput = () => { @@ -745,9 +1317,11 @@ const a = data.assets[selectedIdx] a.paddedX = +document.getElementById('geo-x').value a.paddedY = +document.getElementById('geo-y').value - a.paddedWidth = +document.getElementById('geo-w').value || 1 - a.paddedHeight = +document.getElementById('geo-h').value || 1 + a.paddedWidth = +document.getElementById('geo-w').value || 16 + a.paddedHeight = +document.getElementById('geo-h').value || 16 + updateFootprintDisplay() renderPreview() + renderTileset() } }) @@ -760,10 +1334,11 @@ const a = data.assets[selectedIdx] pushUndo() a[field] = (a[field] || 0) + delta - if (field === 'paddedWidth' && a.paddedWidth < 1) a.paddedWidth = 1 - if (field === 'paddedHeight' && a.paddedHeight < 1) a.paddedHeight = 1 + if (field === 'paddedWidth' && a.paddedWidth < 16) a.paddedWidth = 16 + if (field === 'paddedHeight' && a.paddedHeight < 16) a.paddedHeight = 16 loadForm(selectedIdx) renderPreview() + renderTileset() scheduleSave() } }) @@ -777,35 +1352,54 @@ pushUndo() const a = data.assets[selectedIdx] + const oldId = a.id // capture before rename // Geometry a.paddedX = +document.getElementById('geo-x').value a.paddedY = +document.getElementById('geo-y').value - a.paddedWidth = +document.getElementById('geo-w').value || a.paddedWidth - a.paddedHeight = +document.getElementById('geo-h').value || a.paddedHeight - // Metadata - a.name = document.getElementById('f-name').value.trim() || a.name + a.paddedWidth = Math.max(16, Math.round((+document.getElementById('geo-w').value || a.paddedWidth) / 16) * 16) + a.paddedHeight = Math.max(16, Math.round((+document.getElementById('geo-h').value || a.paddedHeight) / 16) * 16) + // Metadata — ID from field (may be manually edited or auto-generated from label) a.label = document.getElementById('f-label').value.trim() || a.label + const fieldId = document.getElementById('f-id').value.trim() + a.id = fieldId || labelToId(a.label) || a.id + a.name = a.id + + // Sync ID change into groups + if (oldId !== a.id && data.groups) { + for (const g of data.groups) { + const memberIdx = g.members.indexOf(oldId) + if (memberIdx >= 0) { + g.members[memberIdx] = a.id + } + if (g.memberRoles && g.memberRoles[oldId] !== undefined) { + g.memberRoles[a.id] = g.memberRoles[oldId] + delete g.memberRoles[oldId] + } + } + } + a.category = document.getElementById('f-category').value - a.footprintW = +document.getElementById('f-fpw').value || a.footprintW - a.footprintH = +document.getElementById('f-fph').value || a.footprintH + a.footprintW = Math.max(1, a.paddedWidth / 16) + a.footprintH = Math.max(1, a.paddedHeight / 16) a.backgroundTiles = +document.getElementById('f-bgtiles').value || 0 - // Flags - a.isDesk = document.getElementById('f-isdesk').checked + // Flags — isDesk derived from category + a.isDesk = a.category === 'desks' a.canPlaceOnWalls = document.getElementById('f-walls').checked a.canPlaceOnSurfaces = document.getElementById('f-ontop').checked - // Rotation - a.partOfGroup = document.getElementById('f-grouped').checked - a.groupId = document.getElementById('f-group-id').value.trim() || null - a.orientation = document.getElementById('f-grouped').checked ? document.getElementById('f-orientation').value : undefined // Discard a.discard = document.getElementById('f-discard').checked + // Deduplicate IDs across all assets + deduplicateIds() + updateFilter() + loadForm(selectedIdx) renderList() renderPreview() + renderTileset() updateCounts() scheduleSave() - statusMsg.textContent = '\u2713 Saved ' + a.name + statusMsg.textContent = '\u2713 Saved ' + a.label } document.getElementById('btn-reset').onclick = () => { @@ -815,6 +1409,45 @@ statusMsg.textContent = 'Reset to last saved' } +document.getElementById('btn-delete').onclick = () => { + if (selectedIdx < 0 || !data) return + const asset = data.assets[selectedIdx] + if (!confirm('Delete "' + (asset.label || asset.id) + '"? This cannot be undone.')) return + pushUndo() + removeAssetFromGroup(asset) + data.assets.splice(selectedIdx, 1) + if (selectedIdx >= data.assets.length) selectedIdx = data.assets.length - 1 + selectedIndices.clear() + if (selectedIdx >= 0) selectedIndices.add(selectedIdx) + updateFilter() + if (selectedIdx >= 0) loadForm(selectedIdx) + updateEditorForSelection() + renderPreview() + renderTileset() + updateCounts() + scheduleSave() + statusMsg.textContent = '\u2713 Deleted ' + (asset.label || asset.id) +} + +document.getElementById('btn-duplicate').onclick = () => { + if (selectedIdx < 0 || !data) return + pushUndo() + const clone = JSON.parse(JSON.stringify(data.assets[selectedIdx])) + data.assets.splice(selectedIdx + 1, 0, clone) + deduplicateIds() + selectedIdx = selectedIdx + 1 + selectedIndices.clear() + selectedIndices.add(selectedIdx) + updateFilter() + loadForm(selectedIdx) + updateEditorForSelection() + renderPreview() + renderTileset() + updateCounts() + scheduleSave() + statusMsg.textContent = '\u2713 Duplicated as ' + data.assets[selectedIdx].id +} + function buildOutputJson() { return JSON.stringify({ version: data.version || 1, @@ -822,7 +1455,8 @@ sourceFile: data.sourceFile, tileset: data.tileset, backgroundColor: data.backgroundColor, - assets: data.assets + assets: data.assets, + groups: data.groups || [] }, null, 2) } @@ -920,6 +1554,11 @@ function renderPreview() { if (!data || selectedIdx < 0 || selectedIdx >= data.assets.length) { + // Multi-select / group preview: show all selected assets side by side + if (data && selectedIndices.size > 1 && tilesetImg) { + renderGroupPreview() + return + } previewCanvas.width = 100; previewCanvas.height = 100 previewCtx.fillStyle = '#111'; previewCtx.fillRect(0, 0, 100, 100) previewCtx.fillStyle = '#444'; previewCtx.font = '12px sans-serif'; previewCtx.textAlign = 'center' @@ -988,8 +1627,8 @@ } // Footprint overlay — shows blocked rows (orange) vs background rows (blue) - const fpW = +document.getElementById('f-fpw').value || 0 - const fpH = +document.getElementById('f-fph').value || 0 + const fpW = Math.max(1, Math.round(asset.paddedWidth / 16)) + const fpH = Math.max(1, Math.round(asset.paddedHeight / 16)) const bgTiles = +document.getElementById('f-bgtiles').value || 0 if (fpW > 0 && fpH > 0) { const tileZoom = 16 * previewZoom @@ -1028,55 +1667,1197 @@ previewCtx.strokeRect(0, 0, w, h) } -// ════════════════════════════════════════════════════════════════════════ -// PIXEL ERASER -// ════════════════════════════════════════════════════════════════════════ +function renderGroupPreview() { + const indices = [...selectedIndices] + const assets = indices.map(i => data.assets[i]).filter(a => a.paddedWidth > 0 && a.paddedHeight > 0) + if (assets.length === 0) return -document.getElementById('btn-eraser').onclick = () => { - eraserActive = !eraserActive - document.getElementById('btn-eraser').classList.toggle('active', eraserActive) - document.getElementById('eraser-info').classList.toggle('visible', eraserActive) - previewCanvas.classList.toggle('eraser-active', eraserActive) -} + previewCanvas.classList.remove('invalid') -document.getElementById('btn-clear-erased').onclick = () => { - if (selectedIdx < 0 || !data) return - const asset = data.assets[selectedIdx] - if (!asset.erasedPixels || asset.erasedPixels.length === 0) return - pushUndo() - asset.erasedPixels = [] - renderPreview() - scheduleSave() - statusMsg.textContent = 'Cleared erased pixels for ' + asset.name -} + const gap = 4 // px gap between assets (in sprite pixels) + const maxH = Math.max(...assets.map(a => a.paddedHeight)) + const totalW = assets.reduce((sum, a) => sum + a.paddedWidth, 0) + gap * (assets.length - 1) -// Bresenham line: all integer pixels between two points -function bresenham(x0, y0, x1, y1) { - const pts = [] - const dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0) - const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1 - let err = dx - dy - let x = x0, y = y0 - while (true) { - pts.push({ x, y }) - if (x === x1 && y === y1) break - const e2 = 2 * err - if (e2 > -dy) { err -= dy; x += sx } - if (e2 < dx) { err += dx; y += sy } - } - return pts -} + const cw = totalW * previewZoom + const ch = maxH * previewZoom + previewCanvas.width = cw + previewCanvas.height = ch + previewCtx.imageSmoothingEnabled = false + previewCtx.clearRect(0, 0, cw, ch) -function previewPixelAt(e) { - const rect = previewCanvas.getBoundingClientRect() - return { - x: Math.floor((e.clientX - rect.left) / previewZoom), - y: Math.floor((e.clientY - rect.top) / previewZoom) - } -} + let curX = 0 + for (const asset of assets) { + const aw = asset.paddedWidth * previewZoom + const ah = asset.paddedHeight * previewZoom + // Bottom-align each asset + const dy = ch - ah -function erasePixels(pts) { - if (selectedIdx < 0 || !data) return + // Draw tileset region + const srcX = Math.max(0, asset.paddedX) + const srcY = Math.max(0, asset.paddedY) + const offX = Math.max(0, -asset.paddedX) + const offY = Math.max(0, -asset.paddedY) + const srcW = asset.paddedWidth - offX + const srcH = asset.paddedHeight - offY + + if (srcW > 0 && srcH > 0 && srcX < tilesetImg.width && srcY < tilesetImg.height) { + const clampW = Math.min(srcW, tilesetImg.width - srcX) + const clampH = Math.min(srcH, tilesetImg.height - srcY) + previewCtx.drawImage(tilesetImg, + srcX, srcY, clampW, clampH, + curX + offX * previewZoom, dy + offY * previewZoom, + clampW * previewZoom, clampH * previewZoom + ) + } + + // Erased pixels + if (asset.erasedPixels && asset.erasedPixels.length > 0) { + previewCtx.fillStyle = 'rgba(255, 0, 0, 0.35)' + for (const p of asset.erasedPixels) { + previewCtx.fillRect(curX + p.x * previewZoom, dy + p.y * previewZoom, previewZoom, previewZoom) + } + } + + // Border per asset + previewCtx.strokeStyle = 'rgba(52, 152, 219, 0.6)' + previewCtx.lineWidth = 1 + previewCtx.strokeRect(curX + 0.5, dy + 0.5, aw - 1, ah - 1) + + // Label below top + const label = asset.id || asset.label || '' + if (label) { + const fontSize = Math.max(8, previewZoom * 3) + previewCtx.font = fontSize + 'px sans-serif' + previewCtx.fillStyle = 'rgba(255,255,255,0.7)' + previewCtx.textAlign = 'center' + previewCtx.textBaseline = 'top' + previewCtx.fillText(label, curX + aw / 2, dy + 2, aw - 4) + } + + curX += aw + gap * previewZoom + } +} + +// ════════════════════════════════════════════════════════════════════════ +// GROUPS SYSTEM +// ════════════════════════════════════════════════════════════════════════ + +let groupCreatorType = 'rotation' +let rotationScheme = '4-way' // '4-way' | '3-way-mirror' | '2-way' +let editingGroupId = null // non-null when editing an existing group + +// Ensure data.groups exists +function ensureGroups() { + if (data && !data.groups) data.groups = [] +} + +// Find the group that contains a given asset +function findGroupForAsset(asset) { + if (!data || !data.groups || !asset.groupId) return null + return data.groups.find(g => g.id === asset.groupId) || null +} + +// Find the parent group that contains a sub-group reference +function findParentGroup(groupId) { + if (!data || !data.groups) return null + return data.groups.find(g => g.members.includes('@' + groupId)) || null +} + +// Resolve all leaf asset IDs from a member reference +// If starts with '@' → find the group, recursively resolve its members +// Otherwise → return [memberId] (it's a leaf asset) +function resolveGroupAssets(memberId) { + if (memberId.startsWith('@')) { + const gid = memberId.slice(1) + const group = data.groups.find(g => g.id === gid) + if (!group) return [] + const results = [] + for (const mid of group.members) { + results.push(...resolveGroupAssets(mid)) + } + return results + } + return [memberId] +} + +// Resolve full metadata for an asset by walking up the group hierarchy +function resolveAssetMetadata(assetId) { + const meta = {} + const asset = data.assets.find(a => a.id === assetId) + if (!asset || !asset.groupId) return meta + + // Find direct group + const directGroup = data.groups.find(g => g.id === asset.groupId) + if (!directGroup) return meta + + // Get role from direct group's memberRoles + if (directGroup.memberRoles) { + const role = directGroup.memberRoles[assetId] + if (directGroup.type === 'rotation') meta.orientation = role + else if (directGroup.type === 'state') meta.state = role + else if (directGroup.type === 'animation') meta.animationFrame = directGroup.members.indexOf(assetId) + } + + // Walk up to parent + const parent = findParentGroup(directGroup.id) + if (parent && parent.memberRoles) { + const parentRole = parent.memberRoles['@' + directGroup.id] + if (parent.type === 'rotation') meta.orientation = parentRole + else if (parent.type === 'state') meta.state = parentRole + + // Walk up one more level + const grandparent = findParentGroup(parent.id) + if (grandparent && grandparent.memberRoles) { + const gpRole = grandparent.memberRoles['@' + parent.id] + if (grandparent.type === 'rotation') meta.orientation = gpRole + else if (grandparent.type === 'state') meta.state = gpRole + } + } + + return meta +} + +// Check if a group is a sub-group of any compound group +function isSubGroup(groupId) { + return !!findParentGroup(groupId) +} + +// Get all top-level groups (groups that are not sub-groups of another group) +function getTopLevelGroups() { + if (!data || !data.groups) return [] + return data.groups.filter(g => !findParentGroup(g.id)) +} + +// Migrate: rebuild data.groups from per-asset fields if groups[] is missing +function migrateGroupsFromAssets() { + if (!data || data.groups) return + data.groups = [] + const byGroupId = {} + for (const asset of data.assets) { + if (!asset.groupId) continue + if (!byGroupId[asset.groupId]) byGroupId[asset.groupId] = [] + byGroupId[asset.groupId].push(asset) + } + for (const [gid, members] of Object.entries(byGroupId)) { + const hasOrientation = members.some(a => a.orientation) + const hasState = members.some(a => a.state) + const type = hasState ? 'state' : hasOrientation ? 'rotation' : 'rotation' + data.groups.push({ id: gid, type, members: members.map(a => a.id) }) + } +} + +// Sync group definition -> per-asset fields +function syncGroupToAssets(group) { + if (!data) return + for (let i = 0; i < group.members.length; i++) { + const asset = data.assets.find(a => a.id === group.members[i]) + if (!asset) continue + asset.groupId = group.id + asset.partOfGroup = true + if (group.type === 'rotation') { + // Try to infer orientation from suffix, or use position-based default + const suffixes = ['front', 'right', 'back', 'left'] + const nameLower = (asset.id || '').toLowerCase() + const matched = suffixes.find(s => nameLower.endsWith('_' + s)) + asset.orientation = matched || asset.orientation || suffixes[i % 4] + } else if (group.type === 'state') { + asset.state = i === 0 ? 'off' : 'on' + } else if (group.type === 'animation') { + asset.animationFrame = i + } + } +} + +// Remove asset from its group +function removeAssetFromGroup(asset) { + if (!data || !asset.groupId) return + ensureGroups() + const group = data.groups.find(g => g.id === asset.groupId) + if (group) { + group.members = group.members.filter(id => id !== asset.id) + // Also remove from memberRoles + if (group.memberRoles) delete group.memberRoles[asset.id] + + if (group.members.length < 2) { + // Dissolve the group — remove per-asset fields from remaining member too + for (const mid of group.members) { + if (mid.startsWith('@')) continue + const m = data.assets.find(a => a.id === mid) + if (m) { m.groupId = undefined; m.partOfGroup = false; m.orientation = undefined; m.state = undefined; m.animationFrame = undefined } + } + // If this group is a sub-group of a compound group, remove the reference + const parent = findParentGroup(group.id) + if (parent) { + parent.members = parent.members.filter(m => m !== '@' + group.id) + if (parent.memberRoles) delete parent.memberRoles['@' + group.id] + // Dissolve parent if < 2 members + if (parent.members.length < 2) { + for (const mid of parent.members) { + if (mid.startsWith('@')) continue + const m = data.assets.find(a => a.id === mid) + if (m) { m.groupId = undefined; m.partOfGroup = false; m.orientation = undefined; m.state = undefined; m.animationFrame = undefined } + } + data.groups = data.groups.filter(g => g.id !== parent.id) + } + } + data.groups = data.groups.filter(g => g.id !== group.id) + } + } + asset.groupId = undefined + asset.partOfGroup = false + asset.orientation = undefined + asset.state = undefined + asset.animationFrame = undefined +} + +// Show group membership info for a single selected asset +function updateGroupMembershipDisplay(asset) { + const el = document.getElementById('group-membership') + const catFlags = document.getElementById('individual-category-flags') + const group = findGroupForAsset(asset) + if (!group) { + el.style.display = 'none' + catFlags.style.display = '' + return + } + el.style.display = '' + // Restore all buttons for individual asset view + document.getElementById('btn-edit-group').style.display = '' + document.getElementById('btn-dissolve-group').style.display = '' + document.getElementById('btn-remove-from-group').style.display = '' + // Hide individual category/flags — they're managed at the group level + catFlags.style.display = 'none' + const typeLabel = group.type === 'rotation' ? 'Rotation' : group.type === 'state' ? 'State' : 'Animation' + let role = '' + if (group.memberRoles && group.memberRoles[asset.id]) { + role = group.memberRoles[asset.id] + } else if (group.type === 'rotation') { + role = asset.orientation || '?' + } else if (group.type === 'state') { + role = asset.state || '?' + } else { + role = 'Frame ' + (group.members.indexOf(asset.id) + 1) + } + const catLabel = (asset.category || 'misc').charAt(0).toUpperCase() + (asset.category || 'misc').slice(1) + let flagsStr = '' + if (asset.canPlaceOnWalls) flagsStr += 'Walls ' + if (asset.canPlaceOnSurfaces) flagsStr += 'Surfaces ' + if (asset.backgroundTiles) flagsStr += 'BG:' + asset.backgroundTiles + const schemeLabel = group.type === 'rotation' ? (group.rotationScheme === '3-way-mirror' ? '3-way + Mirror' : group.rotationScheme === '2-way' ? '2-way' : '4-way') : '' + + // Show compound group hierarchy + let hierarchyHtml = '' + const parent = findParentGroup(group.id) + if (parent) { + const parentType = parent.type === 'rotation' ? 'Rotation' : parent.type === 'state' ? 'State' : 'Animation' + const parentRole = parent.memberRoles ? (parent.memberRoles['@' + group.id] || '?') : '?' + hierarchyHtml = '
' + + 'Parent: ' + escHtml(parent.name || parent.id) + ' ' + parentType + '' + + ' (role: ' + escHtml(parentRole) + ')' + + '
' + + // Check for grandparent + const grandparent = findParentGroup(parent.id) + if (grandparent) { + const gpType = grandparent.type === 'rotation' ? 'Rotation' : grandparent.type === 'state' ? 'State' : 'Animation' + const gpRole = grandparent.memberRoles ? (grandparent.memberRoles['@' + parent.id] || '?') : '?' + hierarchyHtml += '
' + + 'Root: ' + escHtml(grandparent.name || grandparent.id) + ' ' + gpType + '' + + ' (role: ' + escHtml(gpRole) + ')' + + '
' + } + } + + document.getElementById('group-info').innerHTML = + '
Group: ' + escHtml(group.name || group.id) + '
' + + '
Group ID: ' + escHtml(group.id) + '
' + + '
Type: ' + typeLabel + '
' + + (schemeLabel ? '
Scheme: ' + schemeLabel + '
' : '') + + '
Role: ' + role + '
' + + '
Category: ' + escHtml(catLabel) + '
' + + (flagsStr ? '
Flags: ' + escHtml(flagsStr.trim()) + '
' : '') + + '
Members: ' + group.members.length + '
' + + hierarchyHtml +} + +// Update editor panel based on selection count +function updateEditorForSelection() { + const form = document.getElementById('editor-form') + const creator = document.getElementById('group-creator') + const header = document.getElementById('editor-header') + + // Detect if exactly one group is selected with no extra items + let singleSelectedGroup = null + if (selectedIndices.size > 1 && selectedGroupId && data.groups) { + const group = data.groups.find(g => g.id === selectedGroupId) + if (group) { + // Check that selected indices exactly match this group's leaf assets + const leafIds = new Set() + for (const mid of group.members) { + for (const aid of resolveGroupAssets(mid)) leafIds.add(aid) + } + const selectedAssetIds = new Set([...selectedIndices].map(i => data.assets[i].id)) + if (leafIds.size === selectedAssetIds.size && [...leafIds].every(id => selectedAssetIds.has(id))) { + singleSelectedGroup = group + } + } + } + + if (singleSelectedGroup) { + // Single group selected: show group info panel (not the creator) + for (const child of form.children) { + if (child.id !== 'group-membership') child.style.display = 'none' + } + creator.style.display = 'none' + const group = singleSelectedGroup + const typeLabel = group.type === 'rotation' ? 'Rotation' : group.type === 'state' ? 'State' : 'Animation' + header.textContent = escHtml(group.name || group.id) + + // Resolve first leaf asset for category/flags display + const firstLeafId = resolveGroupAssets(group.members[0])[0] + const firstMember = firstLeafId ? data.assets.find(a => a.id === firstLeafId) : null + const catLabel = ((group.category || (firstMember && firstMember.category) || 'misc')) + const catDisplay = catLabel.charAt(0).toUpperCase() + catLabel.slice(1) + let flagsStr = '' + const walls = group.canPlaceOnWalls != null ? group.canPlaceOnWalls : (firstMember && firstMember.canPlaceOnWalls) + const surfaces = group.canPlaceOnSurfaces != null ? group.canPlaceOnSurfaces : (firstMember && firstMember.canPlaceOnSurfaces) + const bgTiles = group.backgroundTiles != null ? group.backgroundTiles : (firstMember && firstMember.backgroundTiles) + if (walls) flagsStr += 'Walls ' + if (surfaces) flagsStr += 'Surfaces ' + if (bgTiles) flagsStr += 'BG:' + bgTiles + const schemeLabel = group.type === 'rotation' ? (group.rotationScheme === '3-way-mirror' ? '3-way + Mirror' : group.rotationScheme === '2-way' ? '2-way' : '4-way') : '' + + // List members with roles + let membersHtml = '' + for (const mid of group.members) { + const role = group.memberRoles ? (group.memberRoles[mid] || '') : '' + const roleStr = role ? ' (' + escHtml(role) + ')' : '' + if (mid.startsWith('@')) { + const subGroup = data.groups.find(g => g.id === mid.slice(1)) + const subType = subGroup ? (subGroup.type === 'rotation' ? 'Rotation' : subGroup.type === 'state' ? 'State' : 'Animation') : '?' + const subTypeClass = subGroup ? subGroup.type : '' + membersHtml += '
' + escHtml(subType) + ' ' + escHtml(subGroup ? (subGroup.name || subGroup.id) : mid.slice(1)) + roleStr + '
' + } else { + membersHtml += '
' + escHtml(mid) + roleStr + '
' + } + } + + const el = document.getElementById('group-membership') + el.style.display = '' + // Show Edit Group + Dissolve, hide Remove from Group (doesn't apply to group view) + document.getElementById('btn-edit-group').style.display = '' + document.getElementById('btn-dissolve-group').style.display = '' + document.getElementById('btn-remove-from-group').style.display = 'none' + document.getElementById('group-info').innerHTML = + '
Group: ' + escHtml(group.name || group.id) + '
' + + '
Group ID: ' + escHtml(group.id) + '
' + + '
Type: ' + typeLabel + '
' + + (schemeLabel ? '
Scheme: ' + schemeLabel + '
' : '') + + '
Category: ' + escHtml(catDisplay) + '
' + + (flagsStr ? '
Flags: ' + escHtml(flagsStr.trim()) + '
' : '') + + '
Members (' + group.members.length + '):
' + + membersHtml + } else if (selectedIndices.size > 1) { + // Multi-select: hide form fields, show group creator + for (const child of form.children) { + if (child.id !== 'group-creator') child.style.display = 'none' + } + creator.style.display = '' + if (selectedGroupUnits.size > 0) { + const units = getSelectedMemberUnits() + const gCount = units.filter(u => u.type === 'group').length + const aCount = units.filter(u => u.type === 'asset').length + const parts = [] + if (gCount > 0) parts.push(gCount + ' group' + (gCount > 1 ? 's' : '')) + if (aCount > 0) parts.push(aCount + ' asset' + (aCount > 1 ? 's' : '')) + header.textContent = units.length + ' items selected (' + parts.join(' + ') + ')' + document.getElementById('group-creator-count').textContent = '(' + units.length + ' items)' + } else { + header.textContent = selectedIndices.size + ' assets selected' + document.getElementById('group-creator-count').textContent = '(' + selectedIndices.size + ' assets)' + } + editingGroupId = null + initGroupCreator() + } else { + // Single or no selection: show form fields, hide group creator + for (const child of form.children) { + child.style.display = '' + } + creator.style.display = 'none' + header.textContent = 'Editor' + // Check if all selected are in same group — if editing from "Edit Group" button + if (selectedIdx >= 0) { + updateGroupMembershipDisplay(data.assets[selectedIdx]) + } else { + document.getElementById('group-membership').style.display = 'none' + } + } +} + +// Detect common prefix of asset IDs +function commonPrefix(ids) { + if (ids.length === 0) return '' + let prefix = ids[0] + for (let i = 1; i < ids.length; i++) { + while (ids[i].indexOf(prefix) !== 0 && prefix.length > 0) { + prefix = prefix.substring(0, prefix.length - 1) + } + } + // Trim trailing underscore + if (prefix.endsWith('_')) prefix = prefix.slice(0, -1) + return prefix +} + +// Auto-detect group type from name suffixes +function detectGroupType(assets) { + const ids = assets.map(a => (a.id || '').toUpperCase()) + const orientations = ['FRONT', 'BACK', 'LEFT', 'RIGHT', 'SIDE'] + const states = ['ON', 'OFF', 'OPEN', 'CLOSED'] + if (ids.every(id => orientations.some(o => id.endsWith('_' + o)))) return 'rotation' + if (ids.every(id => states.some(s => id.endsWith('_' + s)))) return 'state' + return 'rotation' +} + +// Auto-detect rotation scheme from assets in the group +function detectRotationScheme(assets) { + const suffixes = new Set() + for (const asset of assets) { + const o = asset.orientation || inferOrientation(asset.id) + if (o) suffixes.add(o) + } + if (suffixes.size <= 2 && !suffixes.has('back') && !suffixes.has('left')) return '2-way' + if (!suffixes.has('left') && suffixes.has('right') && suffixes.has('front') && suffixes.has('back')) return '3-way-mirror' + if (suffixes.size === 3 && suffixes.has('front') && suffixes.has('back') && (suffixes.has('right') || suffixes.has('side'))) return '3-way-mirror' + return '4-way' +} + +// Initialize the group creator panel for current multi-selection +function initGroupCreator() { + const indices = [...selectedIndices] + const assets = indices.map(i => data.assets[i]) + const units = getSelectedMemberUnits() + const ids = units.map(u => u.type === 'group' ? u.groupId : u.id) + + // Reset manual edit tracking for group ID + groupIdManuallyEdited = false + + // Check if all selected units are already members of the same parent group (compound edit) + if (units.length >= 2 && data.groups) { + // For compound groups: check if all units (group refs + assets) belong to the same parent + let candidateParent = null + for (const g of data.groups) { + const memberSet = new Set(g.members) + const allMatch = units.every(u => { + const ref = u.type === 'group' ? '@' + u.groupId : u.id + return memberSet.has(ref) + }) + if (allMatch && g.members.length === units.length) { + candidateParent = g + break + } + } + if (candidateParent) { + editingGroupId = candidateParent.id + groupCreatorType = candidateParent.type + if (candidateParent.type === 'rotation') { + rotationScheme = candidateParent.rotationScheme || '4-way' + document.getElementById('rotation-scheme').value = rotationScheme + } + document.getElementById('group-name-input').value = candidateParent.name || '' + document.getElementById('group-id-input').value = candidateParent.id + groupIdManuallyEdited = !!(candidateParent.id && candidateParent.name && candidateParent.id !== labelToId(candidateParent.name)) + // Resolve first leaf asset for defaults + const firstLeafId = resolveGroupAssets(candidateParent.members[0])[0] + const firstMember = firstLeafId ? data.assets.find(a => a.id === firstLeafId) : null + document.getElementById('group-category').value = candidateParent.category || (firstMember && firstMember.category) || 'misc' + document.getElementById('group-bgtiles').value = candidateParent.backgroundTiles != null ? candidateParent.backgroundTiles : (firstMember && firstMember.backgroundTiles) || 0 + document.getElementById('group-walls').checked = candidateParent.canPlaceOnWalls != null ? candidateParent.canPlaceOnWalls : !!(firstMember && firstMember.canPlaceOnWalls) + document.getElementById('group-ontop').checked = candidateParent.canPlaceOnSurfaces != null ? candidateParent.canPlaceOnSurfaces : !!(firstMember && firstMember.canPlaceOnSurfaces) + document.getElementById('btn-create-group').textContent = 'Update Group' + selectGroupTypeBtn(candidateParent.type) + renderGroupMembers() + return + } + } + + // Check if all assets are already in the same simple group (legacy / non-compound) + const groups = assets.map(a => a.groupId).filter(Boolean) + const uniqueGroups = [...new Set(groups)] + if (selectedGroupUnits.size === 0 && uniqueGroups.length === 1 && data.groups) { + const existing = data.groups.find(g => g.id === uniqueGroups[0]) + if (existing && !existing.members.some(m => m.startsWith('@'))) { + editingGroupId = existing.id + groupCreatorType = existing.type + if (existing.type === 'rotation') { + const memberAssets = existing.members.map(mid => data.assets.find(a => a.id === mid)).filter(Boolean) + rotationScheme = existing.rotationScheme || detectRotationScheme(memberAssets) + document.getElementById('rotation-scheme').value = rotationScheme + } + document.getElementById('group-name-input').value = existing.name || '' + document.getElementById('group-id-input').value = existing.id + groupIdManuallyEdited = !!(existing.id && existing.name && existing.id !== labelToId(existing.name)) + const firstMember = data.assets.find(a => a.id === existing.members[0]) + document.getElementById('group-category').value = existing.category || (firstMember && firstMember.category) || 'misc' + document.getElementById('group-bgtiles').value = existing.backgroundTiles != null ? existing.backgroundTiles : (firstMember && firstMember.backgroundTiles) || 0 + document.getElementById('group-walls').checked = existing.canPlaceOnWalls != null ? existing.canPlaceOnWalls : !!(firstMember && firstMember.canPlaceOnWalls) + document.getElementById('group-ontop').checked = existing.canPlaceOnSurfaces != null ? existing.canPlaceOnSurfaces : !!(firstMember && firstMember.canPlaceOnSurfaces) + document.getElementById('btn-create-group').textContent = 'Update Group' + selectGroupTypeBtn(existing.type) + renderGroupMembers() + return + } + } + + editingGroupId = null + + // Auto-detect type based on selected units + let detected = 'rotation' + const hasGroupUnits = units.some(u => u.type === 'group') + if (hasGroupUnits) { + // Suggest "next level up" type based on sub-group types + const subTypes = units.filter(u => u.type === 'group').map(u => u.groupType) + if (subTypes.some(t => t === 'state')) detected = 'rotation' + else if (subTypes.some(t => t === 'animation')) detected = 'state' + else detected = 'rotation' + } else { + detected = detectGroupType(assets) + } + + groupCreatorType = detected + if (detected === 'rotation') { + if (!hasGroupUnits) { + rotationScheme = detectRotationScheme(assets) + } else { + rotationScheme = '4-way' + } + document.getElementById('rotation-scheme').value = rotationScheme + } else { + rotationScheme = '4-way' + } + selectGroupTypeBtn(detected) + document.getElementById('group-name-input').value = '' + document.getElementById('group-id-input').value = commonPrefix(ids) || '' + // Default category/flags from first selected asset + const first = assets[0] + document.getElementById('group-category').value = first.category || 'misc' + document.getElementById('group-bgtiles').value = first.backgroundTiles || 0 + document.getElementById('group-walls').checked = !!first.canPlaceOnWalls + document.getElementById('group-ontop').checked = !!first.canPlaceOnSurfaces + document.getElementById('btn-create-group').textContent = 'Create Group' + renderGroupMembers() +} + +function selectGroupTypeBtn(type) { + groupCreatorType = type + document.querySelectorAll('.group-type-btn').forEach(btn => { + btn.classList.toggle('selected', btn.dataset.type === type) + }) + // Show/hide rotation scheme selector + const schemeField = document.getElementById('rotation-scheme-field') + schemeField.style.display = type === 'rotation' ? '' : 'none' + updateRotationSchemeHint() + renderGroupMembers() +} + +function updateRotationSchemeHint() { + const hint = document.getElementById('rotation-scheme-hint') + if (rotationScheme === '3-way-mirror') { + hint.textContent = 'Side sprite = Right. Left will be auto-mirrored at runtime.' + hint.style.display = '' + } else if (rotationScheme === '2-way') { + hint.textContent = 'Side sprite = Right. No back orientation.' + hint.style.display = '' + } else { + hint.textContent = '' + hint.style.display = 'none' + } +} + +function getOrientationsForScheme(scheme) { + if (scheme === '3-way-mirror') return ['front', 'back', 'right'] + if (scheme === '2-way') return ['front', 'right'] + return ['front', 'right', 'back', 'left'] // 4-way +} + +document.querySelectorAll('.group-type-btn').forEach(btn => { + btn.onclick = () => selectGroupTypeBtn(btn.dataset.type) +}) + +document.getElementById('rotation-scheme').onchange = (e) => { + rotationScheme = e.target.value + updateRotationSchemeHint() + renderGroupMembers() +} + +// Build member units from selection: groups (from selectedGroupUnits) + individual assets +function getSelectedMemberUnits() { + const units = [] // { type: 'group'|'asset', id, groupId?, indices, label, groupType? } + const coveredIndices = new Set() + + // First: group units + for (const gid of selectedGroupUnits) { + const group = data.groups.find(g => g.id === gid) + if (!group) continue + // Resolve all leaf indices for this group + const leafAssetIds = [] + for (const mid of group.members) { + leafAssetIds.push(...resolveGroupAssets(mid)) + } + const indices = leafAssetIds + .map(aid => data.assets.findIndex(a => a.id === aid)) + .filter(i => i >= 0 && selectedIndices.has(i)) + if (indices.length === 0) continue + indices.forEach(i => coveredIndices.add(i)) + units.push({ type: 'group', id: '@' + gid, groupId: gid, indices, label: group.name || gid, groupType: group.type }) + } + + // Then: individual assets not in any selected group unit + for (const idx of selectedIndices) { + if (coveredIndices.has(idx)) continue + const asset = data.assets[idx] + units.push({ type: 'asset', id: asset.id, indices: [idx], label: asset.label || asset.id }) + } + return units +} + +function renderGroupMembers() { + const list = document.getElementById('group-members-list') + const units = getSelectedMemberUnits() + const orientations = getOrientationsForScheme(rotationScheme) + + // When editing an existing compound group, restore roles from memberRoles + let existingRoles = null + if (editingGroupId) { + const existing = data.groups.find(g => g.id === editingGroupId) + if (existing && existing.memberRoles) existingRoles = existing.memberRoles + } + + let html = '' + units.forEach((unit, i) => { + const isGroup = unit.type === 'group' + html += '
' + + if (isGroup) { + const typeLabel = unit.groupType === 'rotation' ? 'Rotation' : unit.groupType === 'state' ? 'State' : 'Animation' + html += '' + escHtml(typeLabel) + '' + } + + html += '' + escHtml(unit.label) + '' + + // Determine current role + const ref = isGroup ? '@' + unit.groupId : unit.id + let cur = existingRoles ? existingRoles[ref] : null + + if (groupCreatorType === 'rotation') { + if (!cur) { + if (isGroup) { + // Try to infer from group name + cur = inferOrientation(unit.groupId) || orientations[i % orientations.length] + } else { + const asset = data.assets[unit.indices[0]] + const inferred = asset.orientation || inferOrientation(asset.id) + cur = inferred + if (cur === 'left' && !orientations.includes('left')) cur = 'right' + if (cur === 'side') cur = 'right' + cur = cur || orientations[i % orientations.length] + } + } + html += '' + } else if (groupCreatorType === 'state') { + if (!cur) { + if (isGroup) { + cur = inferState(unit.groupId) || (i === 0 ? 'off' : 'on') + } else { + const asset = data.assets[unit.indices[0]] + cur = asset.state || inferState(asset.id) || (i === 0 ? 'off' : 'on') + } + } + html += '' + } else { + html += 'Frame ' + (i + 1) + '' + } + html += '
' + }) + list.innerHTML = html +} + +function inferOrientation(id) { + const lower = (id || '').toLowerCase() + for (const o of ['front', 'back', 'left', 'right', 'side']) { + if (lower.endsWith('_' + o)) return o === 'side' ? 'right' : o + } + return null +} + +function inferState(id) { + const lower = (id || '').toLowerCase() + for (const s of ['on', 'off', 'open', 'closed']) { + if (lower.endsWith('_' + s)) return s + } + return null +} + +// Validate and create/update group +document.getElementById('btn-create-group').onclick = () => { + if (!data || selectedIndices.size < 2) return + const units = getSelectedMemberUnits() + const groupName = document.getElementById('group-name-input').value.trim() + const groupId = document.getElementById('group-id-input').value.trim() + + if (!groupName) { + showGroupValidation('Group Name is required') + return + } + + if (!groupId) { + showGroupValidation('Group ID is required (enter a Group Name to auto-generate)') + return + } + + if (units.length < 2) { + showGroupValidation('At least 2 members required') + return + } + + if (groupCreatorType === 'state' && units.length < 2) { + showGroupValidation('State groups require at least 2 members') + return + } + + // Validate: animation groups cannot contain sub-groups + if (groupCreatorType === 'animation' && units.some(u => u.type === 'group')) { + showGroupValidation('Animation groups can only contain individual assets') + return + } + + // Validate: no circular references + if (units.some(u => u.type === 'group' && u.groupId === groupId)) { + showGroupValidation('A group cannot contain itself') + return + } + + // Validate hierarchy: state groups cannot contain rotation groups, etc. + const groupUnits = units.filter(u => u.type === 'group') + for (const gu of groupUnits) { + if (groupCreatorType === 'state' && gu.groupType === 'rotation') { + showGroupValidation('State groups cannot contain rotation groups') + return + } + if (groupCreatorType === 'animation') { + showGroupValidation('Animation groups can only contain individual assets') + return + } + } + + // Check for duplicate orientations in rotation groups + if (groupCreatorType === 'rotation') { + const selects = document.querySelectorAll('#group-members-list .gm-role-select') + const orientations = [...selects].map(s => s.value) + const unique = new Set(orientations) + if (unique.size < orientations.length) { + showGroupValidation('Each orientation can only be assigned once') + return + } + const schemeOrients = getOrientationsForScheme(rotationScheme) + if (units.length > schemeOrients.length) { + showGroupValidation(rotationScheme + ' scheme only supports ' + schemeOrients.length + ' members (got ' + units.length + ')') + return + } + } + + // Check for duplicate or empty states in state groups + if (groupCreatorType === 'state') { + const stateInputs = document.querySelectorAll('#group-members-list .gm-role-input') + const stateValues = [...stateInputs].map(s => s.value.trim().toLowerCase()) + if (stateValues.some(s => !s)) { + showGroupValidation('Each state must have a non-empty value') + return + } + const unique = new Set(stateValues) + if (unique.size < stateValues.length) { + showGroupValidation('Each state value must be unique') + return + } + } + + pushUndo() + ensureGroups() + + const isEditing = !!editingGroupId + + // Remove old group if editing + if (editingGroupId) { + const oldGroup = data.groups.find(g => g.id === editingGroupId) + if (oldGroup) { + // Clear old simple asset members that are no longer in the group + for (const mid of oldGroup.members) { + if (mid.startsWith('@')) continue // sub-group refs don't need per-asset cleanup + const m = data.assets.find(a => a.id === mid) + const stillIncluded = units.some(u => u.type === 'asset' && u.indices.some(idx => data.assets[idx] === m)) + if (m && !stillIncluded) { + m.groupId = undefined; m.partOfGroup = false; m.orientation = undefined; m.state = undefined; m.animationFrame = undefined + } + } + data.groups = data.groups.filter(g => g.id !== editingGroupId) + } + } + + // Read group-level category/flags + const groupCategory = document.getElementById('group-category').value + const groupBgTiles = +document.getElementById('group-bgtiles').value || 0 + const groupWalls = document.getElementById('group-walls').checked + const groupOnTop = document.getElementById('group-ontop').checked + + // Collect roles from dropdowns or text inputs + const selects = document.querySelectorAll('#group-members-list .gm-role-select') + const stateInputs = document.querySelectorAll('#group-members-list .gm-role-input') + + // Build member refs and memberRoles + const memberRefs = [] + const memberRoles = {} + const hasCompoundMembers = units.some(u => u.type === 'group') + + units.forEach((unit, i) => { + let role + if (groupCreatorType === 'state') { + role = stateInputs[i] ? stateInputs[i].value.trim() || (i === 0 ? 'off' : 'on') : (i === 0 ? 'off' : 'on') + } else { + role = selects[i] ? selects[i].value : (groupCreatorType === 'animation' ? String(i) : undefined) + } + const ref = unit.type === 'group' ? '@' + unit.groupId : null // asset refs will be updated after rename + + if (unit.type === 'group') { + memberRefs.push(ref) + if (role) memberRoles[ref] = role + + // Rename leaf assets within the sub-group based on compound hierarchy + const subGroup = data.groups.find(g => g.id === unit.groupId) + if (subGroup) { + // Build the role suffix for this sub-group in the parent + const roleSuffix = role ? role.toUpperCase() : '' + const sideLabel = (rotationScheme !== '4-way' && role === 'right') ? 'SIDE' : roleSuffix + + // Walk sub-group members and rename + renameSubGroupAssets(subGroup, groupId, groupCreatorType === 'rotation' ? sideLabel : roleSuffix, groupCreatorType, groupName, groupCategory, groupBgTiles, groupWalls, groupOnTop) + } + } else { + // Individual asset member + const asset = data.assets[unit.indices[0]] + + // Remove from any existing group first + if (asset.groupId && asset.groupId !== groupId) removeAssetFromGroup(asset) + + asset.groupId = groupId + asset.partOfGroup = true + asset.label = groupName + asset.category = groupCategory + asset.isDesk = groupCategory === 'desks' + asset.backgroundTiles = groupBgTiles + asset.canPlaceOnWalls = groupWalls + asset.canPlaceOnSurfaces = groupOnTop + + if (groupCreatorType === 'rotation') { + const orientation = role || 'front' + asset.orientation = orientation + asset.state = undefined + asset.animationFrame = undefined + const suffix = (rotationScheme !== '4-way' && orientation === 'right') ? 'SIDE' : orientation.toUpperCase() + asset.id = groupId + '_' + suffix + asset.name = asset.id + } else if (groupCreatorType === 'state') { + const state = role || (i === 0 ? 'off' : 'on') + asset.state = state + asset.orientation = undefined + asset.animationFrame = undefined + asset.id = groupId + '_' + state.toUpperCase() + asset.name = asset.id + } else { + asset.animationFrame = i + asset.orientation = undefined + asset.state = undefined + asset.id = groupId + '_' + (i + 1) + asset.name = asset.id + } + + // For non-compound groups, use asset id directly + const assetRef = asset.id + memberRefs.push(assetRef) + if (role) memberRoles[assetRef] = role + } + }) + + // Create group entry + const newGroup = { + id: groupId, name: groupName, type: groupCreatorType, members: memberRefs, + ...(hasCompoundMembers ? { memberRoles } : {}), + category: groupCategory, backgroundTiles: groupBgTiles, + canPlaceOnWalls: groupWalls, canPlaceOnSurfaces: groupOnTop, + ...(groupCreatorType === 'rotation' && rotationScheme !== '4-way' ? { rotationScheme } : {}) + } + data.groups.push(newGroup) + + // Select first member asset + const firstIdx = units[0].indices[0] + selectAsset(firstIdx) + updateFilter() + renderList() + renderTileset() + scheduleSave() + statusMsg.textContent = '\u2713 ' + (isEditing ? 'Updated' : 'Created') + ' ' + groupCreatorType + ' group: ' + groupName + ' (' + groupId + ')' + editingGroupId = null +} + +// Rename assets within a sub-group based on their position in a compound hierarchy +function renameSubGroupAssets(subGroup, parentId, parentRoleSuffix, parentType, groupName, groupCategory, groupBgTiles, groupWalls, groupOnTop) { + for (let i = 0; i < subGroup.members.length; i++) { + const mid = subGroup.members[i] + if (mid.startsWith('@')) { + // Nested sub-group: recurse + const nestedGroup = data.groups.find(g => g.id === mid.slice(1)) + if (nestedGroup) { + // Build suffix for this level + let subRole = '' + if (subGroup.memberRoles && subGroup.memberRoles[mid]) { + subRole = subGroup.memberRoles[mid].toUpperCase() + } + const combinedSuffix = parentRoleSuffix ? parentRoleSuffix + '_' + subRole : subRole + renameSubGroupAssets(nestedGroup, parentId, combinedSuffix, parentType, groupName, groupCategory, groupBgTiles, groupWalls, groupOnTop) + } + } else { + const asset = data.assets.find(a => a.id === mid) + if (!asset) continue + + // Apply group-level properties + asset.label = groupName + asset.category = groupCategory + asset.isDesk = groupCategory === 'desks' + asset.backgroundTiles = groupBgTiles + asset.canPlaceOnWalls = groupWalls + asset.canPlaceOnSurfaces = groupOnTop + + // Build the full ID based on hierarchy + let suffix = parentRoleSuffix + + // Add sub-group role (state or animation frame) + if (subGroup.type === 'state') { + const role = (subGroup.memberRoles && subGroup.memberRoles[mid]) || asset.state || (i === 0 ? 'off' : 'on') + suffix = suffix ? suffix + '_' + role.toUpperCase() : role.toUpperCase() + } else if (subGroup.type === 'animation') { + suffix = suffix ? suffix + '_' + (i + 1) : String(i + 1) + } else if (subGroup.type === 'rotation') { + const role = (subGroup.memberRoles && subGroup.memberRoles[mid]) || asset.orientation || 'front' + suffix = suffix ? suffix + '_' + role.toUpperCase() : role.toUpperCase() + } + + const newId = parentId + (suffix ? '_' + suffix : '') + // Update the sub-group's member list to reflect the new ID + subGroup.members[i] = newId + // Update memberRoles keys if needed + if (subGroup.memberRoles && subGroup.memberRoles[mid] && mid !== newId) { + subGroup.memberRoles[newId] = subGroup.memberRoles[mid] + delete subGroup.memberRoles[mid] + } + asset.id = newId + asset.name = newId + } + } +} + +document.getElementById('btn-cancel-group').onclick = () => { + if (selectedIndices.size > 0) { + selectAsset([...selectedIndices][0]) + } +} + +function showGroupValidation(msg) { + const el = document.getElementById('group-validation') + el.textContent = msg + el.style.display = '' + setTimeout(() => { el.style.display = 'none' }, 3000) +} + +// Edit Group button — select all members of the group +// For compound groups, selects the parent group with sub-group units +document.getElementById('btn-edit-group').onclick = () => { + if (!data) return + + // Find the group to edit: either from selectedGroupId (group view) or from selected asset + let group = null + if (selectedGroupId && data.groups) { + group = data.groups.find(g => g.id === selectedGroupId) + } + if (!group && selectedIdx >= 0) { + const asset = data.assets[selectedIdx] + group = findGroupForAsset(asset) + } + if (!group) return + + // Set up selection to match this group's members for the group creator + selectedGroupId = null + selectedGroupUnits = new Set() + selectedIndices.clear() + + for (const mid of group.members) { + if (mid.startsWith('@')) { + const subGroupId = mid.slice(1) + selectedGroupUnits.add(subGroupId) + const leafIds = resolveGroupAssets(mid) + for (const lid of leafIds) { + const idx = data.assets.findIndex(a => a.id === lid) + if (idx >= 0) selectedIndices.add(idx) + } + } else { + const idx = data.assets.findIndex(a => a.id === mid) + if (idx >= 0) selectedIndices.add(idx) + } + } + + selectedIdx = -1 + lastClickedIdx = [...selectedIndices][0] + editingGroupId = group.id + updateEditorForSelection() + renderList() +} + +// Dissolve Group button — removes the group, leaving assets ungrouped +document.getElementById('btn-dissolve-group').onclick = () => { + if (!data || !data.groups) return + + // Find the group: from group view or from selected asset + let group = null + if (selectedGroupId) { + group = data.groups.find(g => g.id === selectedGroupId) + } + if (!group && selectedIdx >= 0) { + group = findGroupForAsset(data.assets[selectedIdx]) + } + if (!group) return + + if (!confirm('Dissolve group "' + (group.name || group.id) + '"? Assets will be ungrouped.')) return + + pushUndo() + + // Clear per-asset fields for all leaf assets + for (const mid of group.members) { + if (mid.startsWith('@')) { + // Sub-group: leave the sub-group intact, just detach from parent + // (the sub-group becomes a standalone group) + } else { + const asset = data.assets.find(a => a.id === mid) + if (asset) { + asset.groupId = undefined + asset.partOfGroup = false + asset.orientation = undefined + asset.state = undefined + asset.animationFrame = undefined + } + } + } + + // Remove sub-group references from parent if this group is a sub-group + const parent = findParentGroup(group.id) + if (parent) { + parent.members = parent.members.filter(m => m !== '@' + group.id) + if (parent.memberRoles) delete parent.memberRoles['@' + group.id] + if (parent.members.length < 2) { + // Dissolve parent too + for (const mid of parent.members) { + if (mid.startsWith('@')) continue + const m = data.assets.find(a => a.id === mid) + if (m) { m.groupId = undefined; m.partOfGroup = false; m.orientation = undefined; m.state = undefined; m.animationFrame = undefined } + } + data.groups = data.groups.filter(g => g.id !== parent.id) + } + } + + // Remove the group itself + data.groups = data.groups.filter(g => g.id !== group.id) + + // Reset selection + selectedGroupId = null + selectedGroupUnits.clear() + if (selectedIdx < 0 && selectedIndices.size > 0) { + selectedIdx = [...selectedIndices][0] + selectedIndices.clear() + selectedIndices.add(selectedIdx) + } + + if (selectedIdx >= 0) loadForm(selectedIdx) + updateEditorForSelection() + updateFilter() + renderList() + renderTileset() + scheduleSave() + statusMsg.textContent = '\u2713 Dissolved group: ' + (group.name || group.id) +} + +// Remove from Group button +document.getElementById('btn-remove-from-group').onclick = () => { + if (selectedIdx < 0 || !data) return + pushUndo() + removeAssetFromGroup(data.assets[selectedIdx]) + loadForm(selectedIdx) + updateEditorForSelection() + renderList() + renderTileset() + scheduleSave() + statusMsg.textContent = '\u2713 Removed from group' +} + +// ════════════════════════════════════════════════════════════════════════ +// PIXEL ERASER +// ════════════════════════════════════════════════════════════════════════ + +document.getElementById('btn-eraser').onclick = () => { + eraserActive = !eraserActive + document.getElementById('btn-eraser').classList.toggle('active', eraserActive) + document.getElementById('eraser-info').classList.toggle('visible', eraserActive) + previewCanvas.classList.toggle('eraser-active', eraserActive) +} + +document.getElementById('btn-clear-erased').onclick = () => { + if (selectedIdx < 0 || !data) return + const asset = data.assets[selectedIdx] + if (!asset.erasedPixels || asset.erasedPixels.length === 0) return + pushUndo() + asset.erasedPixels = [] + renderPreview() + scheduleSave() + statusMsg.textContent = 'Cleared erased pixels for ' + asset.label +} + +// Bresenham line: all integer pixels between two points +function bresenham(x0, y0, x1, y1) { + const pts = [] + const dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0) + const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1 + let err = dx - dy + let x = x0, y = y0 + while (true) { + pts.push({ x, y }) + if (x === x1 && y === y1) break + const e2 = 2 * err + if (e2 > -dy) { err -= dy; x += sx } + if (e2 < dx) { err += dx; y += sy } + } + return pts +} + +function previewPixelAt(e) { + const rect = previewCanvas.getBoundingClientRect() + return { + x: Math.floor((e.clientX - rect.left) / previewZoom), + y: Math.floor((e.clientY - rect.top) / previewZoom) + } +} + +function erasePixels(pts) { + if (selectedIdx < 0 || !data) return const asset = data.assets[selectedIdx] if (!asset.erasedPixels) asset.erasedPixels = [] const existing = new Set(asset.erasedPixels.map(p => p.x + ',' + p.y)) @@ -1128,17 +2909,35 @@ } // ════════════════════════════════════════════════════════════════════════ -// ADD ASSET MODE +// TILESET PANE (always visible) // ════════════════════════════════════════════════════════════════════════ const tilesetCanvas = document.getElementById('tileset-canvas') const tilesetCtx = tilesetCanvas.getContext('2d') const tilesetWrap = document.getElementById('tileset-wrap') const selectRect = document.getElementById('select-rect') +const tilesetEmpty = document.getElementById('tileset-empty') +const addModeBar = document.getElementById('add-mode-bar') + +function updateTilesetVisibility() { + const hasTileset = !!tilesetImg + tilesetWrap.style.display = hasTileset ? '' : 'none' + tilesetEmpty.style.display = hasTileset ? 'none' : '' +} document.getElementById('btn-add-asset').onclick = () => { if (!tilesetImg) { alert('Load a tileset PNG first'); return } - if (!data) { alert('Load a JSON file first'); return } + // Auto-initialize empty project if needed + if (!data) { + loadJsonData({ + version: 1, + timestamp: new Date().toISOString(), + sourceFile: '', + tileset: '', + backgroundColor: null, + assets: [] + }) + } redrawMode = false toggleAddMode(!addMode) } @@ -1154,7 +2953,7 @@ document.getElementById('tileset-zoom').onchange = (e) => { tilesetZoom = +e.target.value - if (addMode) renderTileset() + renderTileset() } function toggleAddMode(on) { @@ -1167,15 +2966,14 @@ if (!on) redrawMode = false document.getElementById('btn-add-asset').classList.toggle('active', on && !redrawMode) - document.getElementById('preview-container').style.display = on ? 'none' : '' - const headerText = on ? (redrawMode ? 'Redraw: ' + (data.assets[selectedIdx]?.name || 'Asset') : 'Add Asset') : 'Preview' - document.getElementById('preview-header').querySelector('span').textContent = headerText - // Update instruction text - const tvBarSpan = document.querySelector('#tileset-view .tv-bar span') - if (tvBarSpan) tvBarSpan.textContent = on && redrawMode ? 'Drag to redefine the asset region' : 'Drag to select a region on the tileset' - document.getElementById('tileset-view').classList.toggle('active', on) + tilesetWrap.style.cursor = on ? 'crosshair' : 'default' - if (on) renderTileset() + // Show/hide the add mode instruction bar + addModeBar.style.display = on ? '' : 'none' + document.getElementById('add-mode-label').textContent = on && redrawMode + ? 'Drag to redefine the asset region' : 'Drag to select a region on the tileset' + + renderTileset() } function renderTileset() { @@ -1193,14 +2991,42 @@ const y = asset.paddedY * tilesetZoom const w = asset.paddedWidth * tilesetZoom const h = asset.paddedHeight * tilesetZoom - // Highlight the asset being redrawn + if (redrawMode && i === selectedIdx) { + // Redraw target: dashed red tilesetCtx.strokeStyle = 'rgba(231, 76, 60, 0.8)' tilesetCtx.lineWidth = 2 tilesetCtx.setLineDash([4, 4]) tilesetCtx.strokeRect(x + 0.5, y + 0.5, w, h) tilesetCtx.setLineDash([]) + } else if (i === selectedIdx) { + // Selected asset: bright green highlight + tilesetCtx.fillStyle = 'rgba(46, 204, 113, 0.12)' + tilesetCtx.fillRect(x, y, w, h) + tilesetCtx.strokeStyle = 'rgba(46, 204, 113, 0.9)' + tilesetCtx.lineWidth = 2 + tilesetCtx.strokeRect(x + 0.5, y + 0.5, w, h) + // Label + tilesetCtx.font = `${Math.max(9, tilesetZoom * 4)}px sans-serif` + tilesetCtx.fillStyle = 'rgba(46, 204, 113, 0.95)' + tilesetCtx.textAlign = 'left' + tilesetCtx.textBaseline = 'bottom' + tilesetCtx.fillText(asset.id, x + 2, y - 2) + } else if (i === tilesetHoverIdx) { + // Hovered asset: yellow highlight + tilesetCtx.fillStyle = 'rgba(241, 196, 15, 0.1)' + tilesetCtx.fillRect(x, y, w, h) + tilesetCtx.strokeStyle = 'rgba(241, 196, 15, 0.8)' + tilesetCtx.lineWidth = 2 + tilesetCtx.strokeRect(x + 0.5, y + 0.5, w, h) + // Label + tilesetCtx.font = `${Math.max(9, tilesetZoom * 4)}px sans-serif` + tilesetCtx.fillStyle = 'rgba(241, 196, 15, 0.9)' + tilesetCtx.textAlign = 'left' + tilesetCtx.textBaseline = 'bottom' + tilesetCtx.fillText(asset.id, x + 2, y - 2) } else { + // Other assets: subtle blue tilesetCtx.strokeStyle = 'rgba(52, 152, 219, 0.5)' tilesetCtx.lineWidth = 1 tilesetCtx.strokeRect(x + 0.5, y + 0.5, w, h) @@ -1209,17 +3035,67 @@ } } -// Tileset mouse handlers for rectangle selection +// Tileset mouse handlers — click to select existing asset OR drag to create/redraw +let clickStartPos = null // track initial mousedown position for click vs drag detection +let tilesetHoverIdx = -1 // index of asset hovered on tileset + +// Find the asset at a given tileset pixel coordinate (smallest bbox wins) +function assetAtTilesetPos(px, py) { + if (!data) return -1 + let bestIdx = -1 + let bestArea = Infinity + data.assets.forEach((asset, i) => { + if (asset.discard) return + if (px >= asset.paddedX && px < asset.paddedX + asset.paddedWidth && + py >= asset.paddedY && py < asset.paddedY + asset.paddedHeight) { + const area = asset.paddedWidth * asset.paddedHeight + if (area < bestArea) { + bestArea = area + bestIdx = i + } + } + }) + return bestIdx +} + +tilesetCanvas.onmousemove = (e) => { + if (addMode && isDragging) return // handled by tilesetWrap.onmousemove + if (addMode) return + const rect = tilesetCanvas.getBoundingClientRect() + const px = (e.clientX - rect.left) / tilesetZoom + const py = (e.clientY - rect.top) / tilesetZoom + const idx = assetAtTilesetPos(px, py) + if (idx !== tilesetHoverIdx) { + tilesetHoverIdx = idx + tilesetCanvas.style.cursor = idx >= 0 ? 'pointer' : 'default' + tilesetCanvas.title = idx >= 0 ? data.assets[idx].label + ' (' + data.assets[idx].id + ')' : '' + renderTileset() + } +} + +tilesetCanvas.onmouseleave = () => { + if (tilesetHoverIdx >= 0) { + tilesetHoverIdx = -1 + tilesetCanvas.title = '' + renderTileset() + } +} + tilesetWrap.onmousedown = (e) => { if (e.target !== tilesetCanvas) return const rect = tilesetCanvas.getBoundingClientRect() const px = (e.clientX - rect.left) / tilesetZoom const py = (e.clientY - rect.top) / tilesetZoom - dragStart = snapCoord(px, py) - dragEnd = { ...dragStart } - isDragging = true - updateSelectRect() - selectRect.style.display = 'block' + + clickStartPos = { x: e.clientX, y: e.clientY } + + if (addMode) { + dragStart = snapCoord(px, py) + dragEnd = { ...dragStart } + isDragging = true + updateSelectRect() + selectRect.style.display = 'block' + } e.preventDefault() } @@ -1233,31 +3109,52 @@ } tilesetWrap.onmouseup = (e) => { - if (!isDragging) return - isDragging = false + const startPos = clickStartPos + clickStartPos = null - const rect = tilesetCanvas.getBoundingClientRect() - const px = (e.clientX - rect.left) / tilesetZoom - const py = (e.clientY - rect.top) / tilesetZoom - dragEnd = snapCoord(px, py) - - // Compute selected region - const x1 = Math.min(dragStart.x, dragEnd.x) - const y1 = Math.min(dragStart.y, dragEnd.y) - const x2 = Math.max(dragStart.x, dragEnd.x) - const y2 = Math.max(dragStart.y, dragEnd.y) - const w = x2 - x1 - const h = y2 - y1 + if (isDragging) { + isDragging = false - if (w < 1 || h < 1) { - selectRect.style.display = 'none' - return + const rect = tilesetCanvas.getBoundingClientRect() + const px = (e.clientX - rect.left) / tilesetZoom + const py = (e.clientY - rect.top) / tilesetZoom + dragEnd = snapCoord(px, py) + + // Compute selected region + const x1 = Math.min(dragStart.x, dragEnd.x) + const y1 = Math.min(dragStart.y, dragEnd.y) + const x2 = Math.max(dragStart.x, dragEnd.x) + const y2 = Math.max(dragStart.y, dragEnd.y) + const w = x2 - x1 + const h = y2 - y1 + + if (w < 1 || h < 1) { + selectRect.style.display = 'none' + // Tiny drag in add mode = treat as click-to-select + if (addMode) return + } else { + if (redrawMode) { + redrawAsset(x1, y1, w, h) + } else { + createNewAsset(x1, y1, w, h) + } + return + } } - if (redrawMode) { - redrawAsset(x1, y1, w, h) - } else { - createNewAsset(x1, y1, w, h) + // Click-to-select: find the asset whose bounding box contains the click point + if (!addMode && startPos && data && e.target === tilesetCanvas) { + const dx = Math.abs(e.clientX - startPos.x) + const dy = Math.abs(e.clientY - startPos.y) + if (dx < 5 && dy < 5) { + const rect = tilesetCanvas.getBoundingClientRect() + const px = (e.clientX - rect.left) / tilesetZoom + const py = (e.clientY - rect.top) / tilesetZoom + const bestIdx = assetAtTilesetPos(px, py) + if (bestIdx >= 0) { + selectAsset(bestIdx) + } + } } } @@ -1282,64 +3179,76 @@ } function createNewAsset(x, y, w, h) { - // Generate a unique ID - const existingIds = new Set(data.assets.map(a => a.id)) - let idx = data.assets.length - let newId - do { - newId = 'ASSET_NEW_' + idx - idx++ - } while (existingIds.has(newId)) + // Snap W/H to multiples of 16 + w = Math.max(16, Math.round(w / 16) * 16) + h = Math.max(16, Math.round(h / 16) * 16) - const fpW = Math.max(1, Math.round(w / 16)) - const fpH = Math.max(1, Math.round(h / 16)) + const label = 'New Asset' + const baseId = labelToId(label) const newAsset = { - id: newId, + id: baseId, paddedX: x, paddedY: y, paddedWidth: w, paddedHeight: h, - name: newId, - label: 'New Asset', + name: baseId, + label: label, category: 'misc', - footprintW: fpW, - footprintH: fpH, + footprintW: w / 16, + footprintH: h / 16, + backgroundTiles: 0, isDesk: false, canPlaceOnWalls: false, + canPlaceOnSurfaces: false, discard: false } pushUndo() data.assets.push(newAsset) + deduplicateIds() const newIdx = data.assets.length - 1 - // Exit add mode, select the new asset - toggleAddMode(false) + // Stay in add mode — just reset drag state so user can draw another + dragStart = null + dragEnd = null + selectRect.style.display = 'none' + selectedIdx = newIdx + selectedIndices.clear() + selectedIndices.add(newIdx) + lastClickedIdx = newIdx updateFilter() loadForm(newIdx) + updateEditorForSelection() renderPreview() + renderTileset() updateCounts() scheduleSave() - statusMsg.textContent = '\u2713 Created ' + newId + ' (' + w + '\u00d7' + h + 'px)' + statusMsg.textContent = '\u2713 Created ' + data.assets[newIdx].id + ' (' + w + '\u00d7' + h + 'px)' } function redrawAsset(x, y, w, h) { if (selectedIdx < 0 || !data) return + // Snap W/H to multiples of 16 + w = Math.max(16, Math.round(w / 16) * 16) + h = Math.max(16, Math.round(h / 16) * 16) pushUndo() const a = data.assets[selectedIdx] a.paddedX = x a.paddedY = y a.paddedWidth = w a.paddedHeight = h + a.footprintW = w / 16 + a.footprintH = h / 16 toggleAddMode(false) loadForm(selectedIdx) renderList() renderPreview() + renderTileset() scheduleSave() - statusMsg.textContent = '\u2713 Redrawn ' + a.name + ' (' + w + '\u00d7' + h + 'px)' + statusMsg.textContent = '\u2713 Redrawn ' + a.label + ' (' + w + '\u00d7' + h + 'px)' } // ════════════════════════════════════════════════════════════════════════ @@ -1350,6 +3259,7 @@ document.getElementById('handle-left').onmousedown = (e) => { resizing = 'left'; e.preventDefault() } document.getElementById('handle-right').onmousedown = (e) => { resizing = 'right'; e.preventDefault() } +document.getElementById('handle-center').onmousedown = (e) => { resizing = 'center'; e.preventDefault() } document.onmousemove = (e) => { if (resizing) { @@ -1363,6 +3273,15 @@ const editorW = mainRect.width - x const w = Math.max(250, Math.min(editorW, mainRect.width - 400)) document.getElementById('editor-panel').style.width = w + 'px' + } else if (resizing === 'center') { + // Resize tileset vs preview pane within center-panel + const centerPanel = document.getElementById('center-panel') + const centerRect = centerPanel.getBoundingClientRect() + const relX = e.clientX - centerRect.left + const tilesetW = Math.max(100, Math.min(relX, centerRect.width - 100)) + document.getElementById('tileset-pane').style.flex = 'none' + document.getElementById('tileset-pane').style.width = tilesetW + 'px' + document.getElementById('preview-pane').style.flex = '1' } } } @@ -1407,8 +3326,11 @@ updateFilter() } else { selectedIdx = -1 + selectedIndices.clear() + updateEditorForSelection() renderList() renderPreview() + renderTileset() } } return @@ -1430,6 +3352,315 @@ } } } + +// ════════════════════════════════════════════════════════════════════════ +// EXPORT ASSETS +// ════════════════════════════════════════════════════════════════════════ + +function showExportModal() { + if (!data || !tilesetImg) { + statusMsg.textContent = 'Load a tileset PNG before exporting' + return + } + const items = collectExportItems() + const groups = items.filter(i => i.leafAssets.length > 1) + const ungrouped = items.filter(i => i.leafAssets.length === 1) + const totalPngs = items.reduce((sum, i) => sum + i.leafAssets.length, 0) + + document.getElementById('export-summary').innerHTML = + '' + items.length + ' folders total: ' + + '' + groups.length + ' groups, ' + + '' + ungrouped.length + ' ungrouped assets, ' + + '' + totalPngs + ' PNGs' + + document.getElementById('export-progress').style.display = 'none' + document.getElementById('export-result').style.display = 'none' + document.getElementById('export-start-btn').disabled = false + + const modal = document.getElementById('export-modal') + modal.style.display = 'flex' +} + +function hideExportModal() { + document.getElementById('export-modal').style.display = 'none' +} + +document.getElementById('btn-export').onclick = showExportModal +document.getElementById('export-modal-close').onclick = hideExportModal +document.getElementById('export-cancel-btn').onclick = hideExportModal +document.getElementById('export-modal').onclick = (e) => { + if (e.target === document.getElementById('export-modal')) hideExportModal() +} + +// Collect all export units: { folderId, manifest, leafAssets, category } +function collectExportItems() { + if (!data) return [] + const items = [] + const groupedAssetIds = new Set() + + // Process top-level groups + const topGroups = getTopLevelGroups() + for (const group of topGroups) { + const leafIds = [] + for (const mid of group.members) { + leafIds.push(...resolveGroupAssets(mid)) + } + const leafAssets = leafIds.map(id => data.assets.find(a => a.id === id)).filter(Boolean) + if (leafAssets.length === 0) continue + + for (const id of leafIds) groupedAssetIds.add(id) + + const manifest = buildManifest(group, leafAssets) + items.push({ + folderId: group.id, + manifest, + leafAssets, + category: group.category || leafAssets[0].category || 'misc' + }) + } + + // Ungrouped, non-discarded assets + for (const asset of data.assets) { + if (asset.discard || groupedAssetIds.has(asset.id)) continue + if (asset.groupId && !groupedAssetIds.has(asset.id)) { + // Part of a sub-group already handled + const directGroup = data.groups ? data.groups.find(g => g.members.includes(asset.id)) : null + if (directGroup && isSubGroup(directGroup.id)) continue + } + // Skip if already in a group we processed + if (asset.partOfGroup) continue + + const manifest = buildManifestUngrouped(asset) + items.push({ + folderId: asset.name || asset.id, + manifest, + leafAssets: [asset], + category: asset.category || 'misc' + }) + } + + return items +} + +function buildManifest(group, leafAssets) { + const category = group.category || leafAssets[0].category || 'misc' + const scheme = group.rotationScheme || '4-way' + + const manifest = { + id: group.id, + name: group.name || group.id, + category, + type: 'group', + groupType: group.type, + ...(group.type === 'rotation' ? { rotationScheme: scheme } : {}), + canPlaceOnWalls: !!group.canPlaceOnWalls, + canPlaceOnSurfaces: !!group.canPlaceOnSurfaces, + backgroundTiles: group.backgroundTiles || 0, + members: buildMembersList(group, group.type, scheme), + } + + return manifest +} + +// Recursively build the members array for a group +// Each entry is either { type: "asset", ... } or { type: "group", ... } +function buildMembersList(group, parentType, rootScheme) { + const members = [] + for (let i = 0; i < group.members.length; i++) { + const mid = group.members[i] + if (mid.startsWith('@')) { + const subGroupId = mid.slice(1) + const subGroup = data.groups.find(g => g.id === subGroupId) + if (!subGroup) continue + + const parentRole = group.memberRoles ? group.memberRoles[mid] : undefined + const subEntry = { type: 'group', groupType: subGroup.type } + // Assign the role from the parent group + if (parentType === 'rotation' && parentRole) subEntry.orientation = resolveOrientation(parentRole, rootScheme) + if (parentType === 'state' && parentRole) subEntry.state = parentRole + if (parentType === 'animation') subEntry.frame = i + // Recurse into sub-group + subEntry.members = buildMembersList(subGroup, subGroup.type, rootScheme) + members.push(subEntry) + } else { + const asset = data.assets.find(a => a.id === mid) + if (!asset) continue + const role = group.memberRoles ? group.memberRoles[mid] : undefined + const entry = buildAssetMember(asset) + applyRoleToEntry(entry, parentType, role, asset, rootScheme, i) + members.push(entry) + } + } + return members +} + +// Apply role fields to an asset entry based on group type +function applyRoleToEntry(entry, groupType, role, asset, scheme, index) { + if (groupType === 'rotation') { + const orientation = role || asset.orientation || 'front' + entry.orientation = resolveOrientation(orientation, scheme) + if (scheme === '3-way-mirror' && (orientation === 'right' || orientation === 'side')) { + entry.mirrorSide = true + } + } + if (groupType === 'state') entry.state = role || asset.state || 'off' + if (groupType === 'animation') entry.frame = index +} + +// Resolve orientation label: 2-way and 3-way-mirror use "side" instead of "right" +function resolveOrientation(orientation, scheme) { + if ((scheme === '2-way' || scheme === '3-way-mirror') && orientation === 'right') return 'side' + return orientation +} + +function buildManifestUngrouped(asset) { + return { + id: asset.name || asset.id, + name: asset.label || asset.name || asset.id, + category: asset.category || 'misc', + type: 'asset', + canPlaceOnWalls: !!asset.canPlaceOnWalls, + canPlaceOnSurfaces: !!asset.canPlaceOnSurfaces, + backgroundTiles: asset.backgroundTiles || 0, + width: asset.paddedWidth, + height: asset.paddedHeight, + footprintW: asset.footprintW || Math.ceil(asset.paddedWidth / 16), + footprintH: asset.footprintH || 1, + } +} + +function buildAssetMember(asset) { + return { + type: 'asset', + id: asset.name || asset.id, + file: (asset.name || asset.id) + '.png', + width: asset.paddedWidth, + height: asset.paddedHeight, + footprintW: asset.footprintW || Math.ceil(asset.paddedWidth / 16), + footprintH: asset.footprintH || 1, + } +} + +// Extract a single asset as a PNG Blob from the tileset canvas +function extractAssetPngBlob(asset) { + return new Promise((resolve) => { + const w = asset.paddedWidth + const h = asset.paddedHeight + const canvas = document.createElement('canvas') + canvas.width = w + canvas.height = h + const ctx = canvas.getContext('2d') + + // Draw the tileset region + const srcX = Math.max(0, asset.paddedX) + const srcY = Math.max(0, asset.paddedY) + const offX = Math.max(0, -asset.paddedX) + const offY = Math.max(0, -asset.paddedY) + const srcW = Math.min(w - offX, tilesetImg.width - srcX) + const srcH = Math.min(h - offY, tilesetImg.height - srcY) + + if (srcW > 0 && srcH > 0) { + ctx.drawImage(tilesetImg, srcX, srcY, srcW, srcH, offX, offY, srcW, srcH) + } + + // Apply erased pixels + if (asset.erasedPixels && asset.erasedPixels.length > 0) { + const imgData = ctx.getImageData(0, 0, w, h) + for (const p of asset.erasedPixels) { + if (p.x >= 0 && p.x < w && p.y >= 0 && p.y < h) { + const idx = (p.y * w + p.x) * 4 + imgData.data[idx] = 0 + imgData.data[idx + 1] = 0 + imgData.data[idx + 2] = 0 + imgData.data[idx + 3] = 0 + } + } + ctx.putImageData(imgData, 0, 0) + } + + canvas.toBlob(resolve, 'image/png') + }) +} + +async function exportAssets() { + if (!window.showDirectoryPicker) { + document.getElementById('export-result').style.display = 'block' + document.getElementById('export-result').style.background = '#4a1a1a' + document.getElementById('export-result').style.border = '1px solid #6b2020' + document.getElementById('export-result').textContent = 'Export requires a Chromium browser (Chrome/Edge) with File System Access API support.' + return + } + + let rootDir + try { + rootDir = await window.showDirectoryPicker({ mode: 'readwrite' }) + } catch (err) { + if (err.name === 'AbortError') return + throw err + } + + const useCategoryFolders = document.querySelector('input[name="export-org"]:checked').value === 'category' + const items = collectExportItems() + const totalPngs = items.reduce((sum, i) => sum + i.leafAssets.length, 0) + let exportedPngs = 0 + + document.getElementById('export-progress').style.display = 'block' + document.getElementById('export-result').style.display = 'none' + document.getElementById('export-start-btn').disabled = true + + const furnitureDir = await rootDir.getDirectoryHandle('furniture', { create: true }) + + try { + for (const item of items) { + let parentDir = furnitureDir + if (useCategoryFolders) { + parentDir = await furnitureDir.getDirectoryHandle(item.category, { create: true }) + } + const groupDir = await parentDir.getDirectoryHandle(item.folderId, { create: true }) + + // Write manifest.json + const mHandle = await groupDir.getFileHandle('manifest.json', { create: true }) + const mWritable = await mHandle.createWritable() + await mWritable.write(JSON.stringify(item.manifest, null, 2)) + await mWritable.close() + + // Write PNGs + for (const asset of item.leafAssets) { + const blob = await extractAssetPngBlob(asset) + const filename = (asset.name || asset.id) + '.png' + const pHandle = await groupDir.getFileHandle(filename, { create: true }) + const pWritable = await pHandle.createWritable() + await pWritable.write(blob) + await pWritable.close() + + exportedPngs++ + const pct = Math.round((exportedPngs / totalPngs) * 100) + document.getElementById('export-progress-bar').style.width = pct + '%' + document.getElementById('export-progress-text').textContent = + 'Exporting... ' + exportedPngs + '/' + totalPngs + ' PNGs (' + pct + '%)' + } + } + + document.getElementById('export-progress-text').textContent = + 'Done! ' + exportedPngs + ' PNGs in ' + items.length + ' folders' + document.getElementById('export-result').style.display = 'block' + document.getElementById('export-result').style.background = '#1a4a2a' + document.getElementById('export-result').style.border = '1px solid #2a8a3a' + document.getElementById('export-result').innerHTML = + 'Exported ' + exportedPngs + ' PNGs across ' + items.length + ' folders to furniture/' + statusMsg.textContent = 'Export complete: ' + exportedPngs + ' PNGs in ' + items.length + ' folders' + } catch (err) { + document.getElementById('export-result').style.display = 'block' + document.getElementById('export-result').style.background = '#4a1a1a' + document.getElementById('export-result').style.border = '1px solid #6b2020' + document.getElementById('export-result').textContent = 'Export failed: ' + (err.message || err) + statusMsg.textContent = 'Export failed: ' + (err.message || err) + } + + document.getElementById('export-start-btn').disabled = false +} + +document.getElementById('export-start-btn').onclick = exportAssets diff --git a/scripts/export-characters.ts b/scripts/export-characters.ts deleted file mode 100644 index 7443c62a..00000000 --- a/scripts/export-characters.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Export pre-colored character sprites to PNG files. - * - * Generates 6 PNG files (one per palette) in - * webview-ui/public/assets/characters/. Each PNG is 112×96: - * - 7 frames horizontally (16px each) = 112px wide - * - 3 direction rows vertically (32px each) = 96px tall - * Row 0 = down, Row 1 = up, Row 2 = right - * - * Frame order per row: walk1, walk2, walk3, type1, type2, read1, read2 - * - * Colors are baked in from CHARACTER_PALETTES — no template swapping at runtime. - * Left-facing sprites are still generated by flipping right at runtime. - * - * Run: npx tsx scripts/export-characters.ts - */ - -import * as fs from 'fs' -import * as path from 'path' -import { PNG } from 'pngjs' -import { CHARACTER_TEMPLATES, CHARACTER_PALETTES } from '../webview-ui/src/office/sprites/spriteData.js' - -const FRAME_W = 16 -const FRAME_H = 32 -const SPRITE_H = 24 // actual template height — bottom-aligned in frame -const FRAMES_PER_ROW = 7 -const DIRECTIONS = ['down', 'up', 'right'] as const - -/** Template cell → palette key mapping */ -const TEMPLATE_CELL_MAP: Record = { - 'hair': 'hair', - 'skin': 'skin', - 'shirt': 'shirt', - 'pants': 'pants', - 'shoes': 'shoes', -} - -/** Parse a hex color string like '#RRGGBB' into [r, g, b] */ -function hexToRgb(hex: string): [number, number, number] { - const r = parseInt(hex.slice(1, 3), 16) - const g = parseInt(hex.slice(3, 5), 16) - const b = parseInt(hex.slice(5, 7), 16) - return [r, g, b] -} - -function buildCharacterPng( - paletteIndex: number, -): Buffer { - const pal = CHARACTER_PALETTES[paletteIndex] - const width = FRAME_W * FRAMES_PER_ROW // 144 - const height = FRAME_H * DIRECTIONS.length // 96 - const png = new PNG({ width, height }) - - const padTop = FRAME_H - SPRITE_H // 8px transparent padding at top of each frame - - for (let dirIdx = 0; dirIdx < DIRECTIONS.length; dirIdx++) { - const dir = DIRECTIONS[dirIdx] - const frames = CHARACTER_TEMPLATES[dir] - const rowOffsetY = dirIdx * FRAME_H - - for (let f = 0; f < frames.length; f++) { - const frame = frames[f] - const frameOffsetX = f * FRAME_W - - for (let y = 0; y < SPRITE_H; y++) { - const row = frame[y] - if (!row) continue - for (let x = 0; x < FRAME_W; x++) { - const cell = row[x] - const idx = (((rowOffsetY + padTop + y) * width) + (frameOffsetX + x)) * 4 - - if (!cell || cell === '') { - // Transparent - png.data[idx] = 0 - png.data[idx + 1] = 0 - png.data[idx + 2] = 0 - png.data[idx + 3] = 0 - } else { - const paletteKey = TEMPLATE_CELL_MAP[cell] - if (paletteKey) { - // Resolve palette color - const [r, g, b] = hexToRgb(pal[paletteKey]) - png.data[idx] = r - png.data[idx + 1] = g - png.data[idx + 2] = b - png.data[idx + 3] = 0xFF - } else { - // Direct color (eyes = #FFFFFF, etc.) - const [r, g, b] = hexToRgb(cell) - png.data[idx] = r - png.data[idx + 1] = g - png.data[idx + 2] = b - png.data[idx + 3] = 0xFF - } - } - } - } - } - } - - return PNG.sync.write(png) -} - -const outDir = path.join(__dirname, '..', 'webview-ui', 'public', 'assets', 'characters') -fs.mkdirSync(outDir, { recursive: true }) - -for (let i = 0; i < CHARACTER_PALETTES.length; i++) { - const buffer = buildCharacterPng(i) - const outPath = path.join(outDir, `char_${i}.png`) - fs.writeFileSync(outPath, buffer) - console.log(`✓ Wrote ${outPath} (palette ${i}, ${FRAME_W * FRAMES_PER_ROW}×${FRAME_H * DIRECTIONS.length})`) -} - -console.log(`\nGenerated ${CHARACTER_PALETTES.length} character PNGs`) -console.log('\nPNG layout:') -console.log(' 112×96 (7 frames × 16px wide, 3 rows × 32px tall)') -console.log(' Row 0: down, Row 1: up, Row 2: right') -console.log(' Sprite data is 24px tall, bottom-aligned in 32px frame (8px padding at top)') -console.log('\nFrame order (per row):') -console.log(' 0: walk_1, 1: walk_2, 2: walk_3, 3: type_1, 4: type_2, 5: read_1, 6: read_2') diff --git a/scripts/generate-walls.js b/scripts/generate-walls.js deleted file mode 100644 index ad43901c..00000000 --- a/scripts/generate-walls.js +++ /dev/null @@ -1,253 +0,0 @@ -/** - * Generate walls.png — a complete auto-tile wall set with all 16 bitmask configs. - * - * Layout: 4×4 grid, each cell is 16×32 pixels. - * Piece at mask M: col = M % 4, row = floor(M / 4) - * Image size: 64×128 - * - * Bitmask: N=1, E=2, S=4, W=8 - * - * Each piece shows: - * - Tile area (bottom 16 rows): wall plan/cap view (top surface) - * - Above tile (top 16 rows): 3D face extending upward - * - * Run: node scripts/generate-walls.js - */ - -const { PNG } = require('pngjs'); -const fs = require('fs'); -const path = require('path'); - -// ── Dimensions ─────────────────────────────────────────── -const TILE = 16; -const SPRITE_H = 32; -const GRID_COLS = 4; -const GRID_ROWS = 4; -const IMG_W = GRID_COLS * TILE; -const IMG_H = GRID_ROWS * SPRITE_H; - -// ── Colors (RGBA) ──────────────────────────────────────── -const TRANSPARENT = [0, 0, 0, 0]; -const BORDER = [0x30, 0x2A, 0x28, 255]; // #302A28 -const CAP = [0xFF, 0xFF, 0xFF, 255]; // #FFFFFF -const FACE = [0xEB, 0xE8, 0xE0, 255]; // #EBE8E0 - -// ── Wall geometry ──────────────────────────────────────── -const WALL_BAND = 8; // wall thickness in pixels -const BAND_START = (TILE - WALL_BAND) / 2; // = 4 -const BAND_END = BAND_START + WALL_BAND; // = 12 -const FACE_HEIGHT = 10; // face extends this many px above plan -const CAP_THICKNESS = 2; // cap highlight at top of wall - -/** - * Get the plan-view wall footprint (16×16 boolean grid) for a given mask. - * - * Center block always present. Arms extend in connected directions. - * Wall band is 8px wide, centered at positions 4-11. - */ -function getPlanFootprint(mask) { - const plan = Array.from({ length: 16 }, () => Array(16).fill(false)); - - // Center block (always) - for (let r = BAND_START; r < BAND_END; r++) - for (let c = BAND_START; c < BAND_END; c++) - plan[r][c] = true; - - // N arm (bit 0) - if (mask & 1) - for (let r = 0; r < BAND_START; r++) - for (let c = BAND_START; c < BAND_END; c++) - plan[r][c] = true; - - // E arm (bit 1) - if (mask & 2) - for (let r = BAND_START; r < BAND_END; r++) - for (let c = BAND_END; c < TILE; c++) - plan[r][c] = true; - - // S arm (bit 2) - if (mask & 4) - for (let r = BAND_END; r < TILE; r++) - for (let c = BAND_START; c < BAND_END; c++) - plan[r][c] = true; - - // W arm (bit 3) - if (mask & 8) - for (let r = BAND_START; r < BAND_END; r++) - for (let c = 0; c < BAND_START; c++) - plan[r][c] = true; - - return plan; -} - -/** - * Generate a single wall piece (16×32 RGBA pixel grid). - */ -function generatePiece(mask) { - // 1. Build the full wall shape in 16×32 sprite space - const shape = Array.from({ length: SPRITE_H }, () => Array(TILE).fill(false)); - - const plan = getPlanFootprint(mask); - - // Copy plan to tile area (sprite rows 16-31 = tile rows 0-15) - for (let r = 0; r < 16; r++) - for (let c = 0; c < 16; c++) - if (plan[r][c]) shape[16 + r][c] = true; - - // Extend face upward from the northernmost plan pixel per column. - // When N is set and the column is in the vertical band, extend face all the - // way to sprite row 0 so it seamlessly fills the overlap region with the - // northern neighbor's plan area (sprites are bottom-anchored, so the southern - // tile's rows 0-15 overlap with the northern tile's rows 16-31 in screen space). - for (let c = 0; c < 16; c++) { - let topRow = -1; - for (let r = 0; r < 16; r++) { - if (plan[r][c]) { topRow = r; break; } - } - if (topRow < 0) continue; - - const spriteNorth = 16 + topRow; - const needsFullExtension = (mask & 1) && c >= BAND_START && c < BAND_END && topRow === 0; - const faceTop = needsFullExtension ? 0 : Math.max(0, spriteNorth - FACE_HEIGHT); - for (let sr = faceTop; sr < spriteNorth; sr++) { - shape[sr][c] = true; - } - } - - // 2. Connection-aware neighbor check. - // When a pixel is at the sprite edge, check if the wall continues in that - // direction (via mask). If it does, treat the out-of-bounds neighbor as - // "shape" so no border is drawn at connecting edges. - function hasNeighbor(r, c) { - // In-bounds: just check shape - if (r >= 0 && r < SPRITE_H && c >= 0 && c < TILE) return shape[r][c]; - - // Out-of-bounds: check if wall continues via mask - if (r < 0) { - // Above sprite top — N connection (bit 0) - return !!(mask & 1) && c >= BAND_START && c < BAND_END; - } - if (r >= SPRITE_H) { - // Below sprite bottom — S connection (bit 2) - return !!(mask & 4) && c >= BAND_START && c < BAND_END; - } - if (c < 0) { - // Left of sprite — W connection (bit 3) - if (!(mask & 8)) return false; - const planRow = r - 16; - if (planRow >= BAND_START && planRow < BAND_END) return true; - const faceTop = 16 + BAND_START - FACE_HEIGHT; - if (r >= faceTop && r < 16 + BAND_START) return true; - return false; - } - if (c >= TILE) { - // Right of sprite — E connection (bit 1) - if (!(mask & 2)) return false; - const planRow = r - 16; - if (planRow >= BAND_START && planRow < BAND_END) return true; - const faceTop = 16 + BAND_START - FACE_HEIGHT; - if (r >= faceTop && r < 16 + BAND_START) return true; - return false; - } - return false; - } - - // Find outline pixels (shape pixel with at least one non-shape neighbor) - const isOutline = Array.from({ length: SPRITE_H }, () => Array(TILE).fill(false)); - for (let r = 0; r < SPRITE_H; r++) { - for (let c = 0; c < TILE; c++) { - if (!shape[r][c]) continue; - if (!hasNeighbor(r - 1, c) || - !hasNeighbor(r + 1, c) || - !hasNeighbor(r, c - 1) || - !hasNeighbor(r, c + 1)) { - isOutline[r][c] = true; - } - } - } - - // 3. Find cap pixels (topmost CAP_THICKNESS non-outline rows per column). - // Suppress caps for columns in the wall band when N is set — the wall - // continues above so there's no visible "top" edge. - const isCap = Array.from({ length: SPRITE_H }, () => Array(TILE).fill(false)); - for (let c = 0; c < TILE; c++) { - // If N connected and this column is in the vertical band, skip cap - if ((mask & 1) && c >= BAND_START && c < BAND_END) continue; - let count = 0; - for (let r = 0; r < SPRITE_H; r++) { - if (shape[r][c] && !isOutline[r][c]) { - isCap[r][c] = true; - count++; - if (count >= CAP_THICKNESS) break; - } - } - } - - // 4. Assemble pixel colors - const pixels = Array.from({ length: SPRITE_H }, () => - Array.from({ length: TILE }, () => [...TRANSPARENT]) - ); - - for (let r = 0; r < SPRITE_H; r++) { - for (let c = 0; c < TILE; c++) { - if (!shape[r][c]) continue; - if (isOutline[r][c]) { - pixels[r][c] = [...BORDER]; - } else if (isCap[r][c]) { - pixels[r][c] = [...CAP]; - } else { - pixels[r][c] = [...FACE]; - } - } - } - - return pixels; -} - -// ── Generate PNG ───────────────────────────────────────── -const png = new PNG({ width: IMG_W, height: IMG_H }); - -// Fill with transparent -for (let i = 0; i < png.data.length; i += 4) { - png.data[i] = 0; - png.data[i + 1] = 0; - png.data[i + 2] = 0; - png.data[i + 3] = 0; -} - -// Generate and place each piece -for (let mask = 0; mask < 16; mask++) { - const piece = generatePiece(mask); - const col = mask % GRID_COLS; - const row = Math.floor(mask / GRID_COLS); - const ox = col * TILE; - const oy = row * SPRITE_H; - - for (let r = 0; r < SPRITE_H; r++) { - for (let c = 0; c < TILE; c++) { - const idx = ((oy + r) * IMG_W + (ox + c)) * 4; - png.data[idx] = piece[r][c][0]; - png.data[idx + 1] = piece[r][c][1]; - png.data[idx + 2] = piece[r][c][2]; - png.data[idx + 3] = piece[r][c][3]; - } - } -} - -// Save -const outPath = path.join(__dirname, '..', 'webview-ui', 'public', 'assets', 'walls.png'); -const buffer = PNG.sync.write(png); -fs.writeFileSync(outPath, buffer); - -console.log(`✅ Generated walls.png (${IMG_W}×${IMG_H}) at ${outPath}`); -console.log('Layout: 4×4 grid, piece at mask M → col=M%4, row=floor(M/4)'); -console.log('Bitmask: N=1, E=2, S=4, W=8'); -console.log(''); -for (let m = 0; m < 16; m++) { - const dirs = []; - if (m & 1) dirs.push('N'); - if (m & 2) dirs.push('E'); - if (m & 4) dirs.push('S'); - if (m & 8) dirs.push('W'); - console.log(` mask ${m.toString().padStart(2)}: ${dirs.join('+') || '(isolated)'}`); -} diff --git a/src/PixelAgentsViewProvider.ts b/src/PixelAgentsViewProvider.ts index 3cc0c459..fe1cdd81 100644 --- a/src/PixelAgentsViewProvider.ts +++ b/src/PixelAgentsViewProvider.ts @@ -23,7 +23,11 @@ import { sendFloorTilesToWebview, sendWallTilesToWebview, } from './assetLoader.js'; -import { GLOBAL_KEY_SOUND_ENABLED, WORKSPACE_KEY_AGENT_SEATS } from './constants.js'; +import { + GLOBAL_KEY_SOUND_ENABLED, + LAYOUT_REVISION_KEY, + WORKSPACE_KEY_AGENT_SEATS, +} from './constants.js'; import { ensureProjectScan } from './fileWatcher.js'; import type { LayoutWatcher } from './layoutPersistence.js'; import { readLayoutFromFile, watchLayoutFile, writeLayoutToFile } from './layoutPersistence.js'; @@ -337,7 +341,7 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider { }); } - /** Export current saved layout to webview-ui/public/assets/default-layout.json (dev utility) */ + /** Export current saved layout as a versioned default-layout-{N}.json (dev utility) */ exportDefaultLayout(): void { const layout = readLayoutFromFile(); if (!layout) { @@ -349,16 +353,27 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider { vscode.window.showErrorMessage('Pixel Agents: No workspace folder found.'); return; } - const targetPath = path.join( - workspaceRoot, - 'webview-ui', - 'public', - 'assets', - 'default-layout.json', - ); + const assetsDir = path.join(workspaceRoot, 'webview-ui', 'public', 'assets'); + + // Find the next revision number + let maxRevision = 0; + if (fs.existsSync(assetsDir)) { + for (const file of fs.readdirSync(assetsDir)) { + const match = /^default-layout-(\d+)\.json$/.exec(file); + if (match) { + maxRevision = Math.max(maxRevision, parseInt(match[1], 10)); + } + } + } + const nextRevision = maxRevision + 1; + layout[LAYOUT_REVISION_KEY] = nextRevision; + + const targetPath = path.join(assetsDir, `default-layout-${nextRevision}.json`); const json = JSON.stringify(layout, null, 2); fs.writeFileSync(targetPath, json, 'utf-8'); - vscode.window.showInformationMessage(`Pixel Agents: Default layout exported to ${targetPath}`); + vscode.window.showInformationMessage( + `Pixel Agents: Default layout exported as revision ${nextRevision} to ${targetPath}`, + ); } private startLayoutWatcher(): void { diff --git a/src/agentManager.ts b/src/agentManager.ts index 5d012711..f7c17a9d 100644 --- a/src/agentManager.ts +++ b/src/agentManager.ts @@ -399,9 +399,10 @@ export function sendLayout( defaultLayout?: Record | null, ): void { if (!webview) return; - const layout = migrateAndLoadLayout(context, defaultLayout); + const result = migrateAndLoadLayout(context, defaultLayout); webview.postMessage({ type: 'layoutLoaded', - layout, + layout: result?.layout ?? null, + wasReset: result?.wasReset ?? false, }); } diff --git a/src/assetLoader.ts b/src/assetLoader.ts index 4ce2a253..03a1e96c 100644 --- a/src/assetLoader.ts +++ b/src/assetLoader.ts @@ -1,8 +1,8 @@ /** - * Asset Loader - Loads furniture assets from disk at startup + * Asset Loader - Loads furniture assets from per-folder manifests * - * Reads assets/furniture/furniture-catalog.json and loads all PNG files - * into SpriteData format for use in the webview. + * Scans assets/furniture/ subdirectories, reads each manifest.json, + * and loads all PNG files into SpriteData format for use in the webview. */ import * as fs from 'fs'; @@ -16,8 +16,8 @@ import { CHAR_FRAME_W, CHAR_FRAMES_PER_ROW, CHARACTER_DIRECTIONS, - FLOOR_PATTERN_COUNT, FLOOR_TILE_SIZE, + LAYOUT_REVISION_KEY, PNG_ALPHA_THRESHOLD, WALL_BITMASK_COUNT, WALL_GRID_COLS, @@ -37,12 +37,15 @@ export interface FurnitureAsset { footprintH: number; isDesk: boolean; canPlaceOnWalls: boolean; - partOfGroup?: boolean; groupId?: string; canPlaceOnSurfaces?: boolean; backgroundTiles?: number; orientation?: string; state?: string; + mirrorSide?: boolean; + rotationScheme?: string; + animationGroup?: string; + frame?: number; } export interface LoadedAssets { @@ -50,50 +53,259 @@ export interface LoadedAssets { sprites: Map; // assetId -> SpriteData } +// ── Manifest types ────────────────────────────────────────── + +interface ManifestAsset { + type: 'asset'; + id: string; + file: string; + width: number; + height: number; + footprintW: number; + footprintH: number; + orientation?: string; + state?: string; + frame?: number; + mirrorSide?: boolean; +} + +interface ManifestGroup { + type: 'group'; + groupType: 'rotation' | 'state' | 'animation'; + rotationScheme?: string; + orientation?: string; + state?: string; + members: ManifestNode[]; +} + +type ManifestNode = ManifestAsset | ManifestGroup; + +interface FurnitureManifest { + id: string; + name: string; + category: string; + canPlaceOnWalls: boolean; + canPlaceOnSurfaces: boolean; + backgroundTiles: number; + // If type is 'asset', these fields are present: + type: 'asset' | 'group'; + file?: string; + width?: number; + height?: number; + footprintW?: number; + footprintH?: number; + // If type is 'group': + groupType?: string; + rotationScheme?: string; + members?: ManifestNode[]; +} + +interface InheritedProps { + groupId: string; + name: string; + category: string; + canPlaceOnWalls: boolean; + canPlaceOnSurfaces: boolean; + backgroundTiles: number; + orientation?: string; + state?: string; + rotationScheme?: string; + animationGroup?: string; +} + +/** + * Recursively flatten a manifest node into FurnitureAsset[]. + * Inherited properties flow from root to all leaf assets. + */ +function flattenManifest(node: ManifestNode, inherited: InheritedProps): FurnitureAsset[] { + if (node.type === 'asset') { + const asset = node as ManifestAsset; + // Merge orientation: node-level takes priority, then inherited + const orientation = asset.orientation ?? inherited.orientation; + const state = asset.state ?? inherited.state; + return [ + { + id: asset.id, + name: inherited.name, + label: inherited.name, + category: inherited.category, + file: asset.file, + width: asset.width, + height: asset.height, + footprintW: asset.footprintW, + footprintH: asset.footprintH, + isDesk: inherited.category === 'desks', + canPlaceOnWalls: inherited.canPlaceOnWalls, + canPlaceOnSurfaces: inherited.canPlaceOnSurfaces, + backgroundTiles: inherited.backgroundTiles, + groupId: inherited.groupId, + ...(orientation ? { orientation } : {}), + ...(state ? { state } : {}), + ...(asset.mirrorSide ? { mirrorSide: true } : {}), + ...(inherited.rotationScheme ? { rotationScheme: inherited.rotationScheme } : {}), + ...(inherited.animationGroup ? { animationGroup: inherited.animationGroup } : {}), + ...(asset.frame !== undefined ? { frame: asset.frame } : {}), + }, + ]; + } + + // Group node + const group = node as ManifestGroup; + const results: FurnitureAsset[] = []; + + for (const member of group.members) { + // Build inherited props for children + const childProps: InheritedProps = { ...inherited }; + + if (group.groupType === 'rotation') { + // Rotation groups set groupId and pass rotationScheme + if (group.rotationScheme) { + childProps.rotationScheme = group.rotationScheme; + } + } + + if (group.groupType === 'state') { + // State groups propagate orientation from the group level + if (group.orientation) { + childProps.orientation = group.orientation; + } + // Propagate state from group level if set (for animation groups nested in state) + if (group.state) { + childProps.state = group.state; + } + } + + if (group.groupType === 'animation') { + // Animation groups: create animation group ID and propagate state + // Use the parent's orientation to build a unique animation group name + const orient = group.orientation ?? inherited.orientation ?? ''; + const state = group.state ?? inherited.state ?? ''; + childProps.animationGroup = `${inherited.groupId}_${orient}_${state}`.toUpperCase(); + if (group.state) { + childProps.state = group.state; + } + } + + // Propagate orientation from group to children (for state groups that have orientation) + if (group.orientation && !childProps.orientation) { + childProps.orientation = group.orientation; + } + + results.push(...flattenManifest(member, childProps)); + } + + return results; +} + /** - * Load furniture assets from disk + * Load furniture assets from per-folder manifests */ export async function loadFurnitureAssets(workspaceRoot: string): Promise { try { console.log(`[AssetLoader] workspaceRoot received: "${workspaceRoot}"`); - const catalogPath = path.join(workspaceRoot, 'assets', 'furniture', 'furniture-catalog.json'); - console.log(`[AssetLoader] Attempting to load from: ${catalogPath}`); + const furnitureDir = path.join(workspaceRoot, 'assets', 'furniture'); + console.log(`[AssetLoader] Scanning furniture directory: ${furnitureDir}`); - if (!fs.existsSync(catalogPath)) { - console.log('ℹ️ No furniture catalog found at:', catalogPath); + if (!fs.existsSync(furnitureDir)) { + console.log('ℹ️ No furniture directory found at:', furnitureDir); return null; } - console.log('📦 Loading furniture assets from:', catalogPath); + const entries = fs.readdirSync(furnitureDir, { withFileTypes: true }); + const dirs = entries.filter((e) => e.isDirectory()); - const catalogContent = fs.readFileSync(catalogPath, 'utf-8'); - const catalogData = JSON.parse(catalogContent); - const catalog: FurnitureAsset[] = catalogData.assets || []; + if (dirs.length === 0) { + console.log('ℹ️ No furniture subdirectories found'); + return null; + } + console.log(`📦 Found ${dirs.length} furniture folders`); + + const catalog: FurnitureAsset[] = []; const sprites = new Map(); - for (const asset of catalog) { + for (const dir of dirs) { + const itemDir = path.join(furnitureDir, dir.name); + const manifestPath = path.join(itemDir, 'manifest.json'); + + if (!fs.existsSync(manifestPath)) { + console.warn(` ⚠️ No manifest.json in ${dir.name}`); + continue; + } + try { - // Ensure file path includes 'assets/' prefix if not already present - let filePath = asset.file; - if (!filePath.startsWith('assets/')) { - filePath = `assets/${filePath}`; + const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); + const manifest = JSON.parse(manifestContent) as FurnitureManifest; + + // Build the inherited props from the root manifest + const inherited: InheritedProps = { + groupId: manifest.id, + name: manifest.name, + category: manifest.category, + canPlaceOnWalls: manifest.canPlaceOnWalls, + canPlaceOnSurfaces: manifest.canPlaceOnSurfaces, + backgroundTiles: manifest.backgroundTiles, + }; + + let assets: FurnitureAsset[]; + + if (manifest.type === 'asset') { + // Single asset manifest (no groups) — file defaults to {id}.png + assets = [ + { + id: manifest.id, + name: manifest.name, + label: manifest.name, + category: manifest.category, + file: manifest.file ?? `${manifest.id}.png`, + width: manifest.width!, + height: manifest.height!, + footprintW: manifest.footprintW!, + footprintH: manifest.footprintH!, + isDesk: manifest.category === 'desks', + canPlaceOnWalls: manifest.canPlaceOnWalls, + canPlaceOnSurfaces: manifest.canPlaceOnSurfaces, + backgroundTiles: manifest.backgroundTiles, + groupId: manifest.id, + }, + ]; + } else { + // Group manifest — flatten recursively + if (manifest.rotationScheme) { + inherited.rotationScheme = manifest.rotationScheme; + } + const rootGroup: ManifestGroup = { + type: 'group', + groupType: manifest.groupType as 'rotation' | 'state' | 'animation', + rotationScheme: manifest.rotationScheme, + members: manifest.members!, + }; + assets = flattenManifest(rootGroup, inherited); } - const assetPath = path.join(workspaceRoot, filePath); - if (!fs.existsSync(assetPath)) { - console.warn(` ⚠️ Asset file not found: ${asset.file}`); - continue; - } + // Load PNGs for each asset + for (const asset of assets) { + try { + const assetPath = path.join(itemDir, asset.file); + if (!fs.existsSync(assetPath)) { + console.warn(` ⚠️ Asset file not found: ${asset.file} in ${dir.name}`); + continue; + } - // Read PNG and convert to SpriteData - const pngBuffer = fs.readFileSync(assetPath); - const spriteData = pngToSpriteData(pngBuffer, asset.width, asset.height); + const pngBuffer = fs.readFileSync(assetPath); + const spriteData = pngToSpriteData(pngBuffer, asset.width, asset.height); + sprites.set(asset.id, spriteData); + } catch (err) { + console.warn( + ` ⚠️ Error loading ${asset.id}: ${err instanceof Error ? err.message : err}`, + ); + } + } - sprites.set(asset.id, spriteData); + catalog.push(...assets); } catch (err) { console.warn( - ` ⚠️ Error loading ${asset.id}: ${err instanceof Error ? err.message : err}`, + ` ⚠️ Error processing ${dir.name}: ${err instanceof Error ? err.message : err}`, ); } } @@ -114,8 +326,16 @@ export async function loadFurnitureAssets(workspaceRoot: string): Promise= 255) return rgb; + return `${rgb}${a.toString(16).padStart(2, '0').toUpperCase()}`; +} + function pngToSpriteData(pngBuffer: Buffer, width: number, height: number): string[][] { try { // Parse PNG using pngjs @@ -140,15 +360,7 @@ function pngToSpriteData(pngBuffer: Buffer, width: number, height: number): stri const b = data[pixelIndex + 2]; const a = data[pixelIndex + 3]; - // If alpha is near zero, treat as transparent - if (a < PNG_ALPHA_THRESHOLD) { - row.push(''); - } else { - // Convert RGB to hex color string - const hex = - `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase(); - row.push(hex); - } + row.push(rgbaToHex(r, g, b, a)); } sprite.push(row); } @@ -168,23 +380,57 @@ function pngToSpriteData(pngBuffer: Buffer, width: number, height: number): stri // ── Default layout loading ─────────────────────────────────── /** - * Load the bundled default layout from assets/default-layout.json. - * Returns the parsed layout object or null if not found. + * Load the bundled default layout with the highest revision. + * Scans for assets/default-layout-{N}.json files and picks the one + * with the largest N. Falls back to assets/default-layout.json for + * backward compatibility. */ export function loadDefaultLayout(assetsRoot: string): Record | null { + const assetsDir = path.join(assetsRoot, 'assets'); try { - const layoutPath = path.join(assetsRoot, 'assets', 'default-layout.json'); - if (!fs.existsSync(layoutPath)) { - console.log('[AssetLoader] No default-layout.json found at:', layoutPath); + // Scan for versioned default layouts: default-layout-{N}.json + let bestRevision = 0; + let bestPath: string | null = null; + + if (fs.existsSync(assetsDir)) { + for (const file of fs.readdirSync(assetsDir)) { + const match = /^default-layout-(\d+)\.json$/.exec(file); + if (match) { + const rev = parseInt(match[1], 10); + if (rev > bestRevision) { + bestRevision = rev; + bestPath = path.join(assetsDir, file); + } + } + } + } + + // Fall back to unversioned default-layout.json + if (!bestPath) { + const fallback = path.join(assetsDir, 'default-layout.json'); + if (fs.existsSync(fallback)) { + bestPath = fallback; + } + } + + if (!bestPath) { + console.log('[AssetLoader] No default layout found in:', assetsDir); return null; } - const content = fs.readFileSync(layoutPath, 'utf-8'); + + const content = fs.readFileSync(bestPath, 'utf-8'); const layout = JSON.parse(content) as Record; - console.log(`[AssetLoader] ✅ Loaded default layout (${layout.cols}×${layout.rows})`); + // Ensure layoutRevision matches the file's revision number + if (bestRevision > 0 && !layout[LAYOUT_REVISION_KEY]) { + layout[LAYOUT_REVISION_KEY] = bestRevision; + } + console.log( + `[AssetLoader] Loaded default layout (${layout.cols}×${layout.rows}, revision ${layout[LAYOUT_REVISION_KEY] ?? 0}) from ${path.basename(bestPath)}`, + ); return layout; } catch (err) { console.error( - `[AssetLoader] ❌ Error loading default layout: ${err instanceof Error ? err.message : err}`, + `[AssetLoader] Error loading default layout: ${err instanceof Error ? err.message : err}`, ); return null; } @@ -193,54 +439,82 @@ export function loadDefaultLayout(assetsRoot: string): Record | // ── Wall tile loading ──────────────────────────────────────── export interface LoadedWallTiles { - /** 16 sprites indexed by bitmask (N=1,E=2,S=4,W=8), each 16×32 SpriteData */ - sprites: string[][][]; + /** Array of wall sets, each containing 16 sprites indexed by bitmask (N=1,E=2,S=4,W=8) */ + sets: string[][][][]; } /** - * Load wall tiles from walls.png (64×128, 4×4 grid of 16×32 pieces). + * Parse a single wall PNG (64×128, 4×4 grid of 16×32 pieces) into 16 bitmask sprites. * Piece at bitmask M: col = M % 4, row = floor(M / 4). */ +function parseWallPng(pngBuffer: Buffer): string[][][] { + const png = PNG.sync.read(pngBuffer); + const sprites: string[][][] = []; + for (let mask = 0; mask < WALL_BITMASK_COUNT; mask++) { + const ox = (mask % WALL_GRID_COLS) * WALL_PIECE_WIDTH; + const oy = Math.floor(mask / WALL_GRID_COLS) * WALL_PIECE_HEIGHT; + const sprite: string[][] = []; + for (let r = 0; r < WALL_PIECE_HEIGHT; r++) { + const row: string[] = []; + for (let c = 0; c < WALL_PIECE_WIDTH; c++) { + const idx = ((oy + r) * png.width + (ox + c)) * 4; + const rv = png.data[idx]; + const gv = png.data[idx + 1]; + const bv = png.data[idx + 2]; + const av = png.data[idx + 3]; + row.push(rgbaToHex(rv, gv, bv, av)); + } + sprite.push(row); + } + sprites.push(sprite); + } + return sprites; +} + +/** + * Load wall tile sets from assets/walls/ folder. + * Each file is named wall_N.png (e.g. wall_0.png, wall_1.png, ...). + * Files are loaded in numeric order; each PNG is a 64×128 grid of 16 bitmask pieces. + */ export async function loadWallTiles(assetsRoot: string): Promise { try { - const wallPath = path.join(assetsRoot, 'assets', 'walls.png'); - if (!fs.existsSync(wallPath)) { - console.log('[AssetLoader] No walls.png found at:', wallPath); + const wallsDir = path.join(assetsRoot, 'assets', 'walls'); + if (!fs.existsSync(wallsDir)) { + console.log('[AssetLoader] No walls/ directory found at:', wallsDir); return null; } - console.log('[AssetLoader] Loading wall tiles from:', wallPath); - const pngBuffer = fs.readFileSync(wallPath); - const png = PNG.sync.read(pngBuffer); + console.log('[AssetLoader] Loading wall tiles from:', wallsDir); - const sprites: string[][][] = []; - for (let mask = 0; mask < WALL_BITMASK_COUNT; mask++) { - const ox = (mask % WALL_GRID_COLS) * WALL_PIECE_WIDTH; - const oy = Math.floor(mask / WALL_GRID_COLS) * WALL_PIECE_HEIGHT; - const sprite: string[][] = []; - for (let r = 0; r < WALL_PIECE_HEIGHT; r++) { - const row: string[] = []; - for (let c = 0; c < WALL_PIECE_WIDTH; c++) { - const idx = ((oy + r) * png.width + (ox + c)) * 4; - const rv = png.data[idx]; - const gv = png.data[idx + 1]; - const bv = png.data[idx + 2]; - const av = png.data[idx + 3]; - if (av < PNG_ALPHA_THRESHOLD) { - row.push(''); - } else { - row.push( - `#${rv.toString(16).padStart(2, '0')}${gv.toString(16).padStart(2, '0')}${bv.toString(16).padStart(2, '0')}`.toUpperCase(), - ); - } - } - sprite.push(row); + // Find all wall_N.png files and sort by index + const entries = fs.readdirSync(wallsDir); + const wallFiles: { index: number; filename: string }[] = []; + for (const entry of entries) { + const match = /^wall_(\d+)\.png$/i.exec(entry); + if (match) { + wallFiles.push({ index: parseInt(match[1], 10), filename: entry }); } - sprites.push(sprite); } - console.log(`[AssetLoader] ✅ Loaded ${sprites.length} wall tile pieces`); - return { sprites }; + if (wallFiles.length === 0) { + console.log('[AssetLoader] No wall_N.png files found in walls/'); + return null; + } + + wallFiles.sort((a, b) => a.index - b.index); + + const sets: string[][][][] = []; + for (const { filename } of wallFiles) { + const filePath = path.join(wallsDir, filename); + const pngBuffer = fs.readFileSync(filePath); + const sprites = parseWallPng(pngBuffer); + sets.push(sprites); + } + + console.log( + `[AssetLoader] ✅ Loaded ${sets.length} wall tile set(s) (${sets.length * WALL_BITMASK_COUNT} pieces total)`, + ); + return { sets }; } catch (err) { console.error( `[AssetLoader] ❌ Error loading wall tiles: ${err instanceof Error ? err.message : err}`, @@ -255,55 +529,56 @@ export async function loadWallTiles(assetsRoot: string): Promise { try { - const floorPath = path.join(assetsRoot, 'assets', 'floors.png'); - if (!fs.existsSync(floorPath)) { - console.log('[AssetLoader] No floors.png found at:', floorPath); + const floorsDir = path.join(assetsRoot, 'assets', 'floors'); + if (!fs.existsSync(floorsDir)) { + console.log('[AssetLoader] No floors/ directory found at:', floorsDir); return null; } - console.log('[AssetLoader] Loading floor tiles from:', floorPath); - const pngBuffer = fs.readFileSync(floorPath); - const png = PNG.sync.read(pngBuffer); - const sprites: string[][][] = []; - for (let t = 0; t < FLOOR_PATTERN_COUNT; t++) { - const sprite: string[][] = []; - for (let y = 0; y < FLOOR_TILE_SIZE; y++) { - const row: string[] = []; - for (let x = 0; x < FLOOR_TILE_SIZE; x++) { - const px = t * FLOOR_TILE_SIZE + x; - const idx = (y * png.width + px) * 4; - const r = png.data[idx]; - const g = png.data[idx + 1]; - const b = png.data[idx + 2]; - const a = png.data[idx + 3]; - if (a < PNG_ALPHA_THRESHOLD) { - row.push(''); - } else { - row.push( - `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase(), - ); - } - } - sprite.push(row); + console.log('[AssetLoader] Loading floor tiles from:', floorsDir); + + // Find all floor_N.png files and sort by index + const entries = fs.readdirSync(floorsDir); + const floorFiles: { index: number; filename: string }[] = []; + for (const entry of entries) { + const match = /^floor_(\d+)\.png$/i.exec(entry); + if (match) { + floorFiles.push({ index: parseInt(match[1], 10), filename: entry }); } + } + + if (floorFiles.length === 0) { + console.log('[AssetLoader] No floor_N.png files found in floors/'); + return null; + } + + floorFiles.sort((a, b) => a.index - b.index); + + const sprites: string[][][] = []; + for (const { filename } of floorFiles) { + const filePath = path.join(floorsDir, filename); + const pngBuffer = fs.readFileSync(filePath); + const sprite = pngToSpriteData(pngBuffer, FLOOR_TILE_SIZE, FLOOR_TILE_SIZE); sprites.push(sprite); } - console.log(`[AssetLoader] ✅ Loaded ${sprites.length} floor tile patterns`); + console.log(`[AssetLoader] ✅ Loaded ${sprites.length} floor tile patterns from floors/`); return { sprites }; } catch (err) { console.error( @@ -380,13 +655,7 @@ export async function loadCharacterSprites( const g = png.data[idx + 1]; const b = png.data[idx + 2]; const a = png.data[idx + 3]; - if (a < PNG_ALPHA_THRESHOLD) { - row.push(''); - } else { - row.push( - `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase(), - ); - } + row.push(rgbaToHex(r, g, b, a)); } sprite.push(row); } diff --git a/src/constants.ts b/src/constants.ts index 5e95c166..1373e168 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,12 +11,11 @@ export const BASH_COMMAND_DISPLAY_MAX_LENGTH = 30; export const TASK_DESCRIPTION_DISPLAY_MAX_LENGTH = 40; // ── PNG / Asset Parsing ───────────────────────────────────── -export const PNG_ALPHA_THRESHOLD = 128; +export const PNG_ALPHA_THRESHOLD = 2; export const WALL_PIECE_WIDTH = 16; export const WALL_PIECE_HEIGHT = 32; export const WALL_GRID_COLS = 4; export const WALL_BITMASK_COUNT = 16; -export const FLOOR_PATTERN_COUNT = 7; export const FLOOR_TILE_SIZE = 16; export const CHARACTER_DIRECTIONS = ['down', 'up', 'right'] as const; export const CHAR_FRAME_W = 16; @@ -28,6 +27,7 @@ export const CHAR_COUNT = 6; export const LAYOUT_FILE_DIR = '.pixel-agents'; export const LAYOUT_FILE_NAME = 'layout.json'; export const LAYOUT_FILE_POLL_INTERVAL_MS = 2000; +export const LAYOUT_REVISION_KEY = 'layoutRevision'; // ── Settings Persistence ──────────────────────────────────── export const GLOBAL_KEY_SOUND_ENABLED = 'pixel-agents.soundEnabled'; diff --git a/src/layoutPersistence.ts b/src/layoutPersistence.ts index 5a2de1c6..b0661fcc 100644 --- a/src/layoutPersistence.ts +++ b/src/layoutPersistence.ts @@ -7,6 +7,7 @@ import { LAYOUT_FILE_DIR, LAYOUT_FILE_NAME, LAYOUT_FILE_POLL_INTERVAL_MS, + LAYOUT_REVISION_KEY, WORKSPACE_KEY_LAYOUT, } from './constants.js'; @@ -47,9 +48,15 @@ export function writeLayoutToFile(layout: Record): void { } } +export interface LayoutLoadResult { + layout: Record; + /** True when the user's saved layout was replaced by a newer bundled default */ + wasReset: boolean; +} + /** * Load layout with migration from workspace state: - * 1. If file exists → return it + * 1. If file exists → return it (reset if bundled default has a newer revision) * 2. Else if workspace state has layout → write to file, clear workspace state, return it * 3. Else if defaultLayout provided → write to file, return it * 4. Else → return null @@ -57,12 +64,21 @@ export function writeLayoutToFile(layout: Record): void { export function migrateAndLoadLayout( context: ExtensionContext, defaultLayout?: Record | null, -): Record | null { - // 1. Try file +): LayoutLoadResult | null { + // 1. Try file — but reset if bundled default has a newer revision const fromFile = readLayoutFromFile(); if (fromFile) { + const fileRevision = (fromFile[LAYOUT_REVISION_KEY] as number) ?? 0; + const defaultRevision = (defaultLayout?.[LAYOUT_REVISION_KEY] as number) ?? 0; + if (defaultRevision > fileRevision) { + console.log( + `[Pixel Agents] Layout revision outdated (${fileRevision} < ${defaultRevision}), resetting to bundled default`, + ); + writeLayoutToFile(defaultLayout!); + return { layout: defaultLayout!, wasReset: true }; + } console.log('[Pixel Agents] Layout loaded from file'); - return fromFile; + return { layout: fromFile, wasReset: false }; } // 2. Migrate from workspace state @@ -71,14 +87,14 @@ export function migrateAndLoadLayout( console.log('[Pixel Agents] Migrating layout from workspace state to file'); writeLayoutToFile(fromState); context.workspaceState.update(WORKSPACE_KEY_LAYOUT, undefined); - return fromState; + return { layout: fromState, wasReset: false }; } // 3. Use bundled default if (defaultLayout) { console.log('[Pixel Agents] Writing bundled default layout to file'); writeLayoutToFile(defaultLayout); - return defaultLayout; + return { layout: defaultLayout, wasReset: false }; } // 4. Nothing diff --git a/webview-ui/.gitignore b/webview-ui/.gitignore index 33413be9..a547bf36 100644 --- a/webview-ui/.gitignore +++ b/webview-ui/.gitignore @@ -22,9 +22,3 @@ dist-ssr *.njsproj *.sln *.sw? - -# Privately licensed files -*/assets/* -!*/assets/characters/ -!*/assets/default-layout.json -!*/assets/walls.png \ No newline at end of file diff --git a/webview-ui/public/Screenshot_v1.1.jpg b/webview-ui/public/Screenshot_v1.1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c714422b1c063658277f861b6a5c9a88a43cb9f GIT binary patch literal 60810 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

wqk+lT3fCYzRUB#?`u#-W= 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 literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/default-layout-1.json b/webview-ui/public/assets/default-layout-1.json new file mode 100644 index 00000000..2954e54d --- /dev/null +++ b/webview-ui/public/assets/default-layout-1.json @@ -0,0 +1,92 @@ +{ + "version": 1, + "cols": 21, + "rows": 22, + "layoutRevision": 1, + "tiles": [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 255, + 0, 7, 7, 7, 7, 7, 7, 7, 7, 7, 0, 9, 9, 9, 9, 9, 9, 9, 9, 0, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 + ], + "tileColors": [ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":209,"s":39,"b":-25,"c":-80}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":214,"s":30,"b":-100,"c":-55}, null, + {"h":214,"s":30,"b":-100,"c":-55}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":25,"s":48,"b":-43,"c":-88}, {"h":214,"s":30,"b":-100,"c":-55}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":209,"s":0,"b":-16,"c":-8}, {"h":214,"s":30,"b":-100,"c":-55}, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null + ], + "furniture": [ + {"uid": "f-1773353910654-5cdg", "type": "TABLE_FRONT", "col": 4, "row": 16}, + {"uid": "f-1773354646615-jhxl", "type": "COFFEE_TABLE", "col": 14, "row": 14}, + {"uid": "f-1773354664329-hxsh", "type": "SOFA_SIDE", "col": 13, "row": 14}, + {"uid": "f-1773354665989-zgrw", "type": "SOFA_BACK", "col": 14, "row": 16}, + {"uid": "f-1773354668333-lo7w", "type": "SOFA_FRONT", "col": 14, "row": 13}, + {"uid": "f-1773354670818-r1q2", "type": "SOFA_SIDE:left", "col": 16, "row": 14}, + {"uid": "f-1773354686967-yiua", "type": "HANGING_PLANT", "col": 9, "row": 9}, + {"uid": "f-1773354687677-hn2k", "type": "HANGING_PLANT", "col": 1, "row": 9}, + {"uid": "f-1773354693077-f7aj", "type": "DOUBLE_BOOKSHELF", "col": 7, "row": 9}, + {"uid": "f-1773354700513-f1zs", "type": "DOUBLE_BOOKSHELF", "col": 2, "row": 9}, + {"uid": "f-1773354799984-j5ri", "type": "SMALL_PAINTING", "col": 12, "row": 9}, + {"uid": "f-1773354827151-yox2", "type": "CLOCK", "col": 5, "row": 9}, + {"uid": "f-1773354842615-f5md", "type": "PLANT", "col": 18, "row": 10}, + {"uid": "f-1773354861273-67uo", "type": "COFFEE", "col": 14, "row": 15}, + {"uid": "f-1773354877474-kt9s", "type": "WOODEN_CHAIR_SIDE", "col": 3, "row": 18}, + {"uid": "f-1773354879805-px9b", "type": "WOODEN_CHAIR_SIDE", "col": 3, "row": 16}, + {"uid": "f-1773354880309-yphd", "type": "WOODEN_CHAIR_SIDE:left", "col": 7, "row": 16}, + {"uid": "f-1773354881902-9m50", "type": "WOODEN_CHAIR_SIDE:left", "col": 7, "row": 18}, + {"uid": "f-1773354931010-8vvr", "type": "DESK_FRONT", "col": 2, "row": 12}, + {"uid": "f-1773354932396-5uus", "type": "DESK_FRONT", "col": 6, "row": 12}, + {"uid": "f-1773356768339-eo6u", "type": "CUSHIONED_BENCH", "col": 3, "row": 14}, + {"uid": "f-1773356769007-a8jm", "type": "CUSHIONED_BENCH", "col": 7, "row": 14}, + {"uid": "f-1773356781294-b69z", "type": "PC_FRONT_OFF", "col": 7, "row": 12}, + {"uid": "f-1773356782055-vp70", "type": "PC_FRONT_OFF", "col": 3, "row": 12}, + {"uid": "f-1773356784581-5jw9", "type": "PC_SIDE", "col": 4, "row": 16}, + {"uid": "f-1773356785458-pyjn", "type": "PC_SIDE", "col": 4, "row": 18}, + {"uid": "f-1773356787060-higb", "type": "PC_SIDE:left", "col": 6, "row": 16}, + {"uid": "f-1773356787744-ykrz", "type": "PC_SIDE:left", "col": 6, "row": 18}, + {"uid": "f-1773356878781-rncl", "type": "PLANT_2", "col": 11, "row": 10}, + {"uid": "f-1773356974812-apra", "type": "LARGE_PAINTING", "col": 14, "row": 9}, + {"uid": "f-1773357087399-3kfy", "type": "BIN", "col": 2, "row": 20}, + {"uid": "f-1773357989802-thws", "type": "SMALL_TABLE_FRONT", "col": 17, "row": 19}, + {"uid": "f-1773358001163-aqv4", "type": "SMALL_TABLE_SIDE", "col": 1, "row": 18}, + {"uid": "f-1773358458100-4wm2", "type": "COFFEE", "col": 1, "row": 19}, + {"uid": "f-1773358479734-biia", "type": "PLANT_2", "col": 1, "row": 17}, + {"uid": "f-1773358485454-id8j", "type": "SMALL_PAINTING_2", "col": 17, "row": 9} + ] +} diff --git a/webview-ui/public/assets/default-layout.json b/webview-ui/public/assets/default-layout.json deleted file mode 100644 index d2d44466..00000000 --- a/webview-ui/public/assets/default-layout.json +++ /dev/null @@ -1,2802 +0,0 @@ -{ - "version": 1, - "cols": 21, - "rows": 21, - "tiles": [ - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 0, - 0, - 2, - 2, - 0, - 0, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 2, - 2, - 0, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 8, - 0, - 0, - 0, - 0, - 0, - 2, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0, - 0, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0 - ], - "tileColors": [ - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 23, - "s": 21, - "b": 46, - "c": 0 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 28, - "s": 50, - "b": -50, - "c": -37 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 205, - "s": 37, - "b": -20, - "c": 12 - }, - { - "h": 214, - "s": 30, - "b": -100, - "c": -55 - } - ], - "furniture": [ - { - "uid": "f-1770720603403-9vfr", - "type": "ASSET_40", - "col": 11, - "row": 10 - }, - { - "uid": "f-1770720612017-f2f0", - "type": "ASSET_44", - "col": 16, - "row": 11 - }, - { - "uid": "f-1770738908507-t1xd", - "type": "ASSET_42", - "col": 13, - "row": 10 - }, - { - "uid": "f-1770738932007-u7ju", - "type": "ASSET_61", - "col": 11, - "row": 16 - }, - { - "uid": "f-1770825889942-ft1f", - "type": "ASSET_49", - "col": 8, - "row": 18 - }, - { - "uid": "f-1770829214421-uu44", - "type": "ASSET_41_0_1", - "col": 19, - "row": 10 - }, - { - "uid": "f-1770829235442-8cvj", - "type": "ASSET_7", - "col": 17, - "row": 10 - }, - { - "uid": "f-1770831748000-ksmj", - "type": "ASSET_18", - "col": 1, - "row": 10 - }, - { - "uid": "f-1770831767041-zaxr", - "type": "ASSET_18", - "col": 7, - "row": 10 - }, - { - "uid": "f-1770831768596-kx8j", - "type": "ASSET_17", - "col": 3, - "row": 10 - }, - { - "uid": "f-1770831788343-fbxl", - "type": "ASSET_143", - "col": 1, - "row": 11 - }, - { - "uid": "f-1770831791526-md8k", - "type": "ASSET_142", - "col": 1, - "row": 19 - }, - { - "uid": "f-1770831898859-bfdn", - "type": "ASSET_83", - "col": 14, - "row": 10 - }, - { - "uid": "f-1771117638479-ddcx", - "type": "ASSET_84", - "col": 15, - "row": 9, - "color": { - "h": -167, - "s": -52, - "b": -14, - "c": 8 - } - }, - { - "uid": "f-1771117697507-gcxt", - "type": "ASSET_101", - "col": 15, - "row": 14, - "color": { - "h": -9, - "s": 3, - "b": 2, - "c": 8 - } - }, - { - "uid": "f-1771119388928-dbrf", - "type": "ASSET_NEW_110", - "col": 14, - "row": 17 - }, - { - "uid": "f-1771119393112-vl9b", - "type": "ASSET_NEW_111", - "col": 17, - "row": 17 - }, - { - "uid": "f-1771253485902-4ipo", - "type": "ASSET_NEW_106", - "col": 2, - "row": 14, - "color": { - "h": -12, - "s": 51, - "b": -64, - "c": 0 - } - }, - { - "uid": "f-1771253487232-60st", - "type": "ASSET_NEW_106", - "col": 7, - "row": 14, - "color": { - "h": -12, - "s": 51, - "b": -64, - "c": 0 - } - }, - { - "uid": "f-1771253497231-n2au", - "type": "ASSET_49", - "col": 3, - "row": 16 - }, - { - "uid": "f-1771253498608-apjl", - "type": "ASSET_49", - "col": 8, - "row": 16 - }, - { - "uid": "f-1771253522193-5zaf", - "type": "ASSET_NEW_106", - "col": 7, - "row": 18, - "color": { - "h": -12, - "s": 51, - "b": -64, - "c": 0 - } - }, - { - "uid": "f-1771253587796-nwgi", - "type": "ASSET_49", - "col": 3, - "row": 18 - }, - { - "uid": "f-1771253595155-yets", - "type": "ASSET_NEW_106", - "col": 2, - "row": 18, - "color": { - "h": -12, - "s": 51, - "b": -64, - "c": 0 - } - }, - { - "uid": "f-1771254122890-desz", - "type": "ASSET_142", - "col": 10, - "row": 19 - }, - { - "uid": "f-1771254147816-bpkr", - "type": "ASSET_140", - "col": 14, - "row": 15 - }, - { - "uid": "f-1771254151704-exrz", - "type": "ASSET_141", - "col": 17, - "row": 15 - }, - { - "uid": "f-1771273583560-z9r1", - "type": "ASSET_NEW_112", - "col": 15, - "row": 17 - }, - { - "uid": "f-1771273618430-t3u3", - "type": "ASSET_18", - "col": 18, - "row": 15 - }, - { - "uid": "f-1771273619213-ylzf", - "type": "ASSET_18", - "col": 12, - "row": 15 - }, - { - "uid": "f-1771275688296-pyz3", - "type": "ASSET_109", - "col": 8, - "row": 18 - }, - { - "uid": "f-1771275690326-y60x", - "type": "ASSET_99", - "col": 5, - "row": 3 - }, - { - "uid": "f-1771275699466-nwdh", - "type": "ASSET_109", - "col": 3, - "row": 18 - }, - { - "uid": "f-1771275984894-29h8", - "type": "ASSET_51", - "col": 6, - "row": 4 - }, - { - "uid": "f-1771275987306-bts7", - "type": "ASSET_51", - "col": 16, - "row": 17 - }, - { - "uid": "f-1771277148633-u8j9", - "type": "ASSET_18", - "col": 9, - "row": 10 - }, - { - "uid": "f-1771277212373-enh1", - "type": "ASSET_27_A", - "col": 5, - "row": 2, - "color": { - "h": -11, - "s": 15, - "b": -82, - "c": 0 - } - }, - { - "uid": "f-1771277253664-6618", - "type": "ASSET_34", - "col": 7, - "row": 3, - "color": { - "h": 116, - "s": -3, - "b": -24, - "c": 20 - } - }, - { - "uid": "f-1771277346180-prsv", - "type": "ASSET_34", - "col": 7, - "row": 4, - "color": { - "h": 116, - "s": -3, - "b": -24, - "c": 20 - } - }, - { - "uid": "f-1771277346584-y3x4", - "type": "ASSET_34", - "col": 7, - "row": 5, - "color": { - "h": 116, - "s": -3, - "b": -24, - "c": 20 - } - }, - { - "uid": "f-1771277358914-5w7p", - "type": "ASSET_33", - "col": 4, - "row": 5, - "color": { - "h": 116, - "s": -3, - "b": -24, - "c": 20 - } - }, - { - "uid": "f-1771277359246-bk4b", - "type": "ASSET_33", - "col": 4, - "row": 4, - "color": { - "h": 116, - "s": -3, - "b": -24, - "c": 20 - } - }, - { - "uid": "f-1771277359490-pqu5", - "type": "ASSET_33", - "col": 4, - "row": 3, - "color": { - "h": 116, - "s": -3, - "b": -24, - "c": 20 - } - }, - { - "uid": "f-1771277556757-gadp", - "type": "ASSET_102", - "col": 5, - "row": 0 - }, - { - "uid": "f-1771277574471-hcv0", - "type": "ASSET_142", - "col": 3, - "row": 1 - }, - { - "uid": "f-1771277575299-mfxh", - "type": "ASSET_142", - "col": 8, - "row": 1 - }, - { - "uid": "f-1771277802319-1joi", - "type": "ASSET_140", - "col": 19, - "row": 19 - }, - { - "uid": "f-1771277804247-jvcl", - "type": "ASSET_141", - "col": 12, - "row": 19 - }, - { - "uid": "f-1771279067027-ia1a", - "type": "ASSET_99", - "col": 15, - "row": 17 - }, - { - "uid": "f-1771279925079-5ts8", - "type": "ASSET_44", - "col": 1, - "row": 19 - }, - { - "uid": "f-1771280121112-6ffk", - "type": "ASSET_72", - "col": 6, - "row": 3 - }, - { - "uid": "f-1771280128756-ylw9", - "type": "ASSET_100", - "col": 2, - "row": 14 - }, - { - "uid": "f-1771280255329-3x6d", - "type": "ASSET_139", - "col": 2, - "row": 11 - }, - { - "uid": "f-1771282426463-of9k", - "type": "ASSET_90", - "col": 8, - "row": 14 - }, - { - "uid": "f-1771282455174-4ohy", - "type": "ASSET_90", - "col": 3, - "row": 14 - }, - { - "uid": "f-1771282481279-lthm", - "type": "ASSET_123", - "col": 2, - "row": 19 - } - ] -} \ No newline at end of file diff --git a/webview-ui/public/assets/floors/floor_0.png b/webview-ui/public/assets/floors/floor_0.png new file mode 100644 index 0000000000000000000000000000000000000000..8d7a03f6ec7d803f1bbc9ad66c99413952091b01 GIT binary patch literal 1719 zcmbVN&yU+g6m~gOP*kZBAXS`3?xjfhx8r0+HPtp*x{>g9wT;xkspIiDao6^^V{ejO zaYEw24X%h2M-E((;J_b&;NRec#F00N;|7$iRwVMTnQy-L-uK?T`Re5O@$Fmh-_kVg z_V{RY3eP+0dh-pqfBnrofX7?a(WkYhz5R~5ZfM_rcURMHj6%FcL$Vm`~tQ2+CnUv-ovq5iBF*-|DN~9QREY6!y2TwO46jeuC)~!q+n6;vn<(Re#X$?$RhpWzI zzBZmPi|4%HWm5yz>9EyYG@_o1E2z8ae;EL2qo}j7*O#K`Ow`Tc0>Wqo*^90x7ZtZo zc`eQ*A$_t^#&i6l!e_MtqR_|6suwUW?=z7Cf#k_uT+2r?_^j9XPX(@`fK8H!v; zFbD2dFer~*!%p2k?z_AnbG}_a66{>1U9I2jfJ89hc;%kQ0~|QqGx}bFjUe`2 zBX-i*;5~cb`TYU>@s{n0%wQAJe3w;ul>lSFfi&r|-geakbnE*L_Y+6bY}R0Z%vZ-=uzdR)tLyM8(6K)SyyHim1F zHcKk`!3<=(PSIIcx-QBEmpjf^R8r_ZT-KqUN>LzMRe8p!iqu+^NxP}L+@MCFS0#0& zotiFQFVOW0W)uECfwnv1e+tw}&*rq8ap>HZu0pX?kJ}{P`fp9F=XUzx77k5Ty>{Il zKDzUPmq0C{`~UIN&%eRR@$PtZFuDI8n;z@W@BVK8K>vQBY112j-TCtS&!4EHWqf!% K`ti~8Fa80$!Wy9f literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/floors/floor_1.png b/webview-ui/public/assets/floors/floor_1.png new file mode 100644 index 0000000000000000000000000000000000000000..157e09447a325aafe9acc92a6ef4c3a1cd4638e8 GIT binary patch literal 3590 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 BM^yj- literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/floors/floor_2.png b/webview-ui/public/assets/floors/floor_2.png new file mode 100644 index 0000000000000000000000000000000000000000..0802a3aefc90702acce77281ea02adb1ff09cd98 GIT binary patch literal 3590 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 BM$iBN literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/floors/floor_3.png b/webview-ui/public/assets/floors/floor_3.png new file mode 100644 index 0000000000000000000000000000000000000000..af976465e32a3ab3cc08f7e1eea79eafd4dcd612 GIT binary patch literal 3594 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 FzW^I#NZ|kg literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/floors/floor_4.png b/webview-ui/public/assets/floors/floor_4.png new file mode 100644 index 0000000000000000000000000000000000000000..470fc91a37e1a6ab22a1887258a6558ef84c2fe0 GIT binary patch literal 3594 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 Fe*sKyNsa&j literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/floors/floor_5.png b/webview-ui/public/assets/floors/floor_5.png new file mode 100644 index 0000000000000000000000000000000000000000..7517dcbd20172867a65bdabf01716eaaf83a65ca GIT binary patch literal 3597 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 Jlv#P7`ybkmNhbgR literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/floors/floor_7.png b/webview-ui/public/assets/floors/floor_7.png new file mode 100644 index 0000000000000000000000000000000000000000..d06893948c00690c94bacbdae177726ff6183904 GIT binary patch literal 3596 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 KZAz?t&;Ab(u}QT6 literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/floors/floor_8.png b/webview-ui/public/assets/floors/floor_8.png new file mode 100644 index 0000000000000000000000000000000000000000..49ec35c31bed8448a70ecb1a2243255297e97bb0 GIT binary patch literal 3592 zcmaJ^c{o)4-#)fNS+Xx-jCd-`n6boU?8_)yV;hwiV=$PdnZb-C5-HiTWlbolp-qu& z6(L&`31ugtv4pI>NKM$%4IRXH`8Q#py!P?9WM5d5@@PRl0p!erGhM-ecB@BmFaY8Ppee;hp0wV#i z5|g)C*sVbLI)J$(EMnXk9n0?`CcZBR>D+L!haZ_6JDPv6(%0l_?8!Li(|X?(28*Ab z4`t2GZG7CQp30fp>|k^XMeGw%NuDh52lP?V2Jk+GM0;dIOHBl)IC7X9Bs|&r%9p1R z0Oq+E^l;ikqfL$nB!Ckj_h=jtX&U(zRpp@K3V@PjtegSwoN21g?>t6x+ zOk=PHcYPU0gxfQi6=y^Z4-lMyP7*Tq1R^p`)!G1%9w%TXFWSxpvf;|t?5-Ow@gPT6 zA~}Hi*0lDF@BUl_G@7DEg!LnTbLa+^Ylt5ZtbP0<&&5)O5ZYCk%yCVq7ReWlU?Y}>?m zj!HIHY)KmxnZ5v9P)Oh6O9>GGm@)SCcF%S{Z4Ims|IWb~Cy4G228_d()UV0xNd|;{ zwPB1^07$7@OEgyH0NOK(#sHxDtJLYcsggEtcmcpXKl1QHlihn?iZSa2b6(0-*GqC@ zc9|RRJy35TY`SYaVn6>asdN)*r+RoF_tS@5GWq+mMuf{>LiGfuhDA-8@GUf7!h-U@G zUB|x$(SY+;6R;6oP(hz-pD?GdUAFMNf|gNflE6-&bzBm4uyW`l!Q!&@c^DS&0nWGx z(OKRzmxNyhUQ9}|LffFt?kdkJm5WsIy7EO`g1-6-oO0K`Tsc%^t%2?Zf0Tr_WU72u zlvm7CKJ%BLs7DI&fMCn@aXWrRRd){T?eetV)kZD>K-{&ZKi;Q;}-2q>BPftxp{Rr7B z5F-%Pg!DQn9Uy&9x>%}08j@*qhv$7pX0wy#N0@A;?+u|FBxm&DmBV4;$!|r{;At*t z;b~L%oSdo7Mm>ysdz|h&3*A5Ca{i8`^Am@$621=-_j^j@oL`jamUW;)P>BwmCp-#k zJ+>~cV+9_^L|f*^APb$a6>p^D6(!!la0_VDUWyCXj9S1ATdz}(*Jf>kc{XH&A?Iuy z&8YW$m_ErqxszwxIRxK48lKnxlyj^@u|uHaoI01fk@}-v|5BgQ@X{mwq5ZY}UUQT= ziMg46RuDPYI9Dxq8BTz&2JuztR-LGdt{MpH{ATzbJHwdfTYkGNIQ_ZHYMJn(a9Q#j ztC!WKa70s^S2syI5UPirZ%e{{4Y+}??3%wca(7>=UsZC0UtyPZ;a`<1-!J=)>Chz& zeVC;EUQF#TxWNDej29j@P}ES=I9_|GJvAW9r>d*`J*};+S#6Nn$84ER(C^6!$ivir zsvg*kkG^e@(|9aSH?AqYKRv9KC*3xEm3hO((1qc$?DCOm-FtdQZHCb68t5SFn!Pt0 zp56NFZfSODLvS`>NNiLr@lf}nWk+H^bar!=e>Q7&*PQ+eW<`00u+k~lwqHc9_bBaX z7=;$p792>fo+%p9eqGW~@_O3USJw9yHFrrz^3L(3R*{V03`s{q@zrAD&Cu4g37PLz z-A~^y7iPc69xVMt`0%+6(Rlv;hOI`nY>WGA1k2>d{3?3%Y};6Fadz>mrKBmyncy>S zXEti&?VseR%<`XWa;6QDVW4$CKkF#{@@Bq)g-!t^h6t?o966yLEWtd0y;->@^fO-ODH7 zra(y{)k`kQu3@zXebutM#NYDfil3BS*KAXyoyj_xb2~ zvvt21d&{6VT732>RYtP(_2)`Uwl3zqUznYr&VEwYPt8fqDMSh2)5}#qYE~Squ=i0_ zO%WJpOcLJ8rd)ozIIKeU)^#g<)`=#S00gI7i744X8V+%A1a#?>^t^*ZMep%V89aa$fXj z3@#?1qPlP2HdDRms-0Gn+R{oa`*HF)RveS&?acDe=sD$b;jmusN zrN6iCbI7?R!6-TLqhDC5v(-;k z>8=4a7U!3V-QO>MPL%PIt*V<0|6FMmV&9mTc<%PO-76NX8GjlnaPUE0-D5riN;h5} z2`5ivN09on=dwbcl20BMd}T86@~@#gVdusizTWD1J@~qPDDy(aGVctMq1p|v3u3Al z67mtZYFJ;d73RvFk+Z6;_!Z8Aj$wnEbP`?j@^cR&7S_furi>MP7w6^L86iGo4l-74 zHZudwBmle}2?(zsaa!y_uD*AgX-cU#VQR{4;oEPBMi5yUn0` zXd?Z=W^Mo0x83VP-!7DIKf=`$hJSk2dv?Bd6JiPSUC!H1{q_FxNVip+RgaRTQhZ{r zf`Y7oOwFQqN6JX?<6_ayx8W|o7&FxSQ$t&$8^=4*n|YN)A3A@ks-@wUTK*E+VT_?`W#;Z)f6CS>)}(nf^7@;>%g zr8La~O>-pq&_Xa29AM&2^1^|v2^e1-3WxD#oNd7w005T?-U&@Z+uFdfB!UKJ2ctnJ zklAbiFfgK%G1vec4djLM#S;-=){8nY2=9#myXx3#+LFz1et2Xk1?L!Q=Y$On!0LO0 zjSN8sbU0gpfTLkRbV4AJ3a2B$f9t~8{d0lY)+pXSaFn^_KepI21lW&8 zBf}w(kdP3K5G@T7#TNqA*Vl(=!XPl1Ivb%*We{l?x;l}n^jpCkN5xX`WE!4C1nnqd zyhuSb1eopVe?lOT|CJ?D|49=&VGufo41sEB?u7IkXlwibLkWa`(Nr1=_g{biPhqMP zgN%cqa8yzd1}cNQJ|NHh}Fk3Eb8Kzl)iUuC<4c9S0Vh+{O*M?f?nnIzLP*ZJv9Wz~BOFeCq zBRUqaBfq)kBy11?N2LAcdjFTJ^GEJZI}pfh%jP%=J{ae1Ng)wHe}@gn|JfJVANBs> zdjHuMtv_-hY-b=l#r^RG`&J?Hnd+3p?{NQ_qY4 E18i1DT>t<8 literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/BIN/BIN.png b/webview-ui/public/assets/furniture/BIN/BIN.png new file mode 100644 index 0000000000000000000000000000000000000000..a378be46f9e062cd6ceae5276495c32d874fc081 GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`8$DedLn;{0Otj`=aujfhKCdxD zQaVFJql&3NQEkSo+zm~g$J!o+PT3w1kleqZ^@re=*h$ok&ZtpVccfBb6>3}oDp>J zX;m{n6Q4)s8z-%d*|iC47=0bjhA2F|`!k)jJ^1$Hxz|6t?qZaYk`uoyYQVt6XYnBM wTL=TEf%Su|3ZR?+|Np#c-A5pU4di%c2KCitirLE=+<+nsp00i_>zopr0Humx?*IS* literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/BIN/manifest.json b/webview-ui/public/assets/furniture/BIN/manifest.json new file mode 100644 index 00000000..768d2ffd --- /dev/null +++ b/webview-ui/public/assets/furniture/BIN/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "BIN", + "name": "Bin", + "category": "misc", + "type": "asset", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 0, + "width": 16, + "height": 16, + "footprintW": 1, + "footprintH": 1 +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/BOOKSHELF/BOOKSHELF.png b/webview-ui/public/assets/furniture/BOOKSHELF/BOOKSHELF.png new file mode 100644 index 0000000000000000000000000000000000000000..165f4181486219978edbde1d505fc938161cda58 GIT binary patch literal 388 zcmV-~0ek+5P)Y5Qc{nEG#4z3c`VQ7Ahzb5X8=pN>Q=QPp}Gt2wE8-g4p>F#L7;afUP3_1Us=1 z8yh=Aa^V@6W3$|nOOvEX;K{r*yYK9+RoGgtnvQVfdIK5spY}%k*=TZTnSJ( zK3pZF%r6wA6KwW<8>Jfo*0N}O1eM1V0XAMgyb-#__dVa01wxH*-gi?$ z@BZJXHS$mcx6VG_H~Kdwz*>ECTma3Pcy$0oJUb=5gfl@0s6Bm1KVX0s00030{{}KP ir2qf`21!IgR09AkNafoQ2diZO0000Nj5*obCw;S+9`r&?YaFbISo zGAdCZjhL|e#qnV-p}13P+4mNq7HI--(oCLE#6)(Jqd5rwfdVE1))*7NLxLmX5ZV+cXY0`u+X*o7q3y~Bi*vit6I%nV;it98pfWQNjmh0$ zsBtL+1f|(Yv=-867u^pWnr`a;&Hw^|V=@vnXoYqLQfSm~%uD@LmM&6*bA?_K8L&gK zIAX5%&QqWU*Xwh^88n-ZrrmnB1j6dZyzxN|5)E2%;0*5G-*Wd4PlhoOX@En6mS*4# zSco$s4ZemPqzov4vC$!;0WO4U5Q_S(QwE%kW7Li=qaJE-XnZnd0D%C|;DZ`K<0CQ< w60r}dhw&2t0RR6#QH3}F000I_L_t&o0D`nJP@Yp2Q~&?~07*qoM6N<$f~BA4oB#j- literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/CACTUS/manifest.json b/webview-ui/public/assets/furniture/CACTUS/manifest.json new file mode 100644 index 00000000..8e53f906 --- /dev/null +++ b/webview-ui/public/assets/furniture/CACTUS/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "CACTUS", + "name": "Cactus", + "category": "decor", + "type": "asset", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 1, + "width": 16, + "height": 32, + "footprintW": 1, + "footprintH": 2 +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/CLOCK/CLOCK.png b/webview-ui/public/assets/furniture/CLOCK/CLOCK.png new file mode 100644 index 0000000000000000000000000000000000000000..549db9405fdb3155f0c43fb9c8eeb2d5cce1396d GIT binary patch literal 304 zcmV-00nh%4P)$-mps#+6^)1}gYyiBeb-&vh0)n*X;{d#0 z&ulthSy5!(iwXo`kRi($1%L^S*M{Ncn7ZwT_Y`$TK8&J zndzCZFh)<62`v+SURZKYU|J7 literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/COFFEE/manifest.json b/webview-ui/public/assets/furniture/COFFEE/manifest.json new file mode 100644 index 00000000..dc9a08c6 --- /dev/null +++ b/webview-ui/public/assets/furniture/COFFEE/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "COFFEE", + "name": "Coffee", + "category": "decor", + "type": "asset", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": true, + "backgroundTiles": 0, + "width": 16, + "height": 16, + "footprintW": 1, + "footprintH": 1 +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/COFFEE_TABLE/COFFEE_TABLE.png b/webview-ui/public/assets/furniture/COFFEE_TABLE/COFFEE_TABLE.png new file mode 100644 index 0000000000000000000000000000000000000000..b1bae849ecaeda3e993903e379b47ecba26634e9 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ#Ar*{orWtY_au9HJ_vTmb z>exE9{ldA^6+GJ3{)wthUVIM)b4ueA+HWrLDVv<+BsbG%lKp&nu`(@Y`Md+d68=of z55EhawByGlmT$+`PEp`{K1*W3$9?-%)X!|#!gHftHOu?~X9d%|26Yhi*gIikq?g~U zY98MQfwi08wY<^mj=b0*wV-fc%@UR~T`L;1OJ`q*IiJpSW#RT$%v&z7yjl=>eo+|n z9UjNi4FB1GnKi4KlnOJuBy;N9?>_WBY_iptFMpUB82a{m=p{CgmzWu*7gYrK T@~&|QiZFP(`njxgN@xNA-?D1g literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/COFFEE_TABLE/manifest.json b/webview-ui/public/assets/furniture/COFFEE_TABLE/manifest.json new file mode 100644 index 00000000..9aecce7f --- /dev/null +++ b/webview-ui/public/assets/furniture/COFFEE_TABLE/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "COFFEE_TABLE", + "name": "Coffee Table", + "category": "desks", + "type": "asset", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 0, + "width": 32, + "height": 32, + "footprintW": 2, + "footprintH": 2 +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/CUSHIONED_BENCH/CUSHIONED_BENCH.png b/webview-ui/public/assets/furniture/CUSHIONED_BENCH/CUSHIONED_BENCH.png new file mode 100644 index 0000000000000000000000000000000000000000..9785551e4c59555ca04efaab39e2b6e40c7c356a GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>pfi@Ln;{WOfckYau9HJ&tJH- zSFI!~Gx>R9&YL}rW%G{QJ9p^Pp_XWgDl4W+mCcf35l$H#D)z>|pPyCVT`c*-*YH7! z?K8!n{$Z8OV%IPFIW=um)N5euF1vB%@PSunSXW%!-lvnWF{mdKI;Vst0L|rP`~Uy| literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/CUSHIONED_BENCH/manifest.json b/webview-ui/public/assets/furniture/CUSHIONED_BENCH/manifest.json new file mode 100644 index 00000000..6ab3b993 --- /dev/null +++ b/webview-ui/public/assets/furniture/CUSHIONED_BENCH/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "CUSHIONED_BENCH", + "name": "Cushioned Bench", + "category": "chairs", + "type": "asset", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 0, + "width": 16, + "height": 16, + "footprintW": 1, + "footprintH": 1 +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png b/webview-ui/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_BACK.png new file mode 100644 index 0000000000000000000000000000000000000000..e2bce3b7a58c8602f509ee0da9e51bc720451f43 GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`O`a}}Ar*{oCT`?xaNu!mJ{=M9 zim7bfL$O;oE!&DMT4ENrDt9<*#Gjnlk#xk*rsB@TKYNtKFP#2t>KL}9n{}tzerBGw zKp(LQsh#3GwmrY~R(Eq;!^*-JCzv+%2o-!h`Io!n3f~7?u_u$JE;zS(t@%z%OJ?75 z4|u06$SGr#3YRg~20G&Zf7h?qUI7_wAlEQ67~RaQmi?f+5Gcan>FVdQ&MBb@0KY9x A_W%F@ literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_FRONT.png b/webview-ui/public/assets/furniture/CUSHIONED_CHAIR/CUSHIONED_CHAIR_FRONT.png new file mode 100644 index 0000000000000000000000000000000000000000..3e6848a4ac6c151a20d4cdfd7202e394f12eece5 GIT binary patch literal 247 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ydl>XLn;{0OxnwP$bqL-ciO@R zwu4L|228R^tXeN#NHkkYFfDCt5)^Q5u)DDN-J(AhL0v9YMNg{#o!hs2qU!x$8~(UV znKFY}L&pD^m|&cvOhC;0Q(KQ+dCjnDUjJ;@mhe*yUS*wsnP;r`eEM!r^UbVZyBp8e zW&1vw_JQjOgX*qhjEojLPDmeU4qyDun(0Z$7Hh-T5B6A3+#LQfE~L}7@`tG}8ntHvj@EbJb$ zq&<3Z_0q+Ls~@WrcFd5ta8kwcLXisFPNi=KXYWpwQqW;sEMZ!4*PHRkvaIupZx%9E z_DI>>U+SQ9ONuu~_Lv+4!~g$!fn~8k_p^cA&&*&@mU-6w{>yVf5e83JKbLh*2~7Zz CNoc_U literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/CUSHIONED_CHAIR/manifest.json b/webview-ui/public/assets/furniture/CUSHIONED_CHAIR/manifest.json new file mode 100644 index 00000000..7d2c32f6 --- /dev/null +++ b/webview-ui/public/assets/furniture/CUSHIONED_CHAIR/manifest.json @@ -0,0 +1,44 @@ +{ + "id": "CUSHIONED_CHAIR", + "name": "Cushioned Chair", + "category": "chairs", + "type": "group", + "groupType": "rotation", + "rotationScheme": "3-way-mirror", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 0, + "members": [ + { + "type": "asset", + "id": "CUSHIONED_CHAIR_FRONT", + "file": "CUSHIONED_CHAIR_FRONT.png", + "width": 16, + "height": 16, + "footprintW": 1, + "footprintH": 1, + "orientation": "front" + }, + { + "type": "asset", + "id": "CUSHIONED_CHAIR_BACK", + "file": "CUSHIONED_CHAIR_BACK.png", + "width": 16, + "height": 16, + "footprintW": 1, + "footprintH": 1, + "orientation": "back" + }, + { + "type": "asset", + "id": "CUSHIONED_CHAIR_SIDE", + "file": "CUSHIONED_CHAIR_SIDE.png", + "width": 16, + "height": 16, + "footprintW": 1, + "footprintH": 1, + "orientation": "side", + "mirrorSide": true + } + ] +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/DESK/DESK_FRONT.png b/webview-ui/public/assets/furniture/DESK/DESK_FRONT.png new file mode 100644 index 0000000000000000000000000000000000000000..1d6b4406ce40ff4c29257fcd97c631cce899f516 GIT binary patch literal 310 zcmV-60m=S}P)DK@I|4u;oqX1(Us10ZXkqPJ(iVQINb8R3x!pI-w)4*Y0e*ogm0RR7Y!|l=l000I_L_t&o0A7o|KH%CazyJUM07*qo IM6N<$f_`9o&j0`b literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/DESK/DESK_SIDE.png b/webview-ui/public/assets/furniture/DESK/DESK_SIDE.png new file mode 100644 index 0000000000000000000000000000000000000000..c9adb6691d0e16aa1d1a724791975779c9e25a2b GIT binary patch literal 278 zcmeAS@N?(olHy`uVBq!ia0vp^0zmA*!3HFSYrjteQl~v#978G?-%L^DJLDkXx_q_Z z3N05=Hoe9yCkrO_M@AMxPEE>;fg!3~jsh|ax1afM5-l~3-uTG8^5nLc|L30jb625L zoY&<>vrbjPjq9;oR>!KgPq|u^$0U`bZ2DtQeZ=ezDN_SYcm2x>-70;{lJ-57uyku_ zvrOQd!CcZ{=^$Pq=QFk0HFVKJwMA(&nB*cl;)9z{zf_e=5SsN(U$Z5-T6^D9pH**< zv6x(VrN2N#|FYzr`MnVxqS>tduXlc65H>w<$eWGr-lfIs*cll9|NrKrkqYz{8^~MC Z3<-(H8V~#ZV*!dVc)I$ztaD0e0su*@W%d97 literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/DESK/manifest.json b/webview-ui/public/assets/furniture/DESK/manifest.json new file mode 100644 index 00000000..0f2a098c --- /dev/null +++ b/webview-ui/public/assets/furniture/DESK/manifest.json @@ -0,0 +1,33 @@ +{ + "id": "DESK", + "name": "Desk", + "category": "desks", + "type": "group", + "groupType": "rotation", + "rotationScheme": "2-way", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 1, + "members": [ + { + "type": "asset", + "id": "DESK_FRONT", + "file": "DESK_FRONT.png", + "width": 48, + "height": 32, + "footprintW": 3, + "footprintH": 2, + "orientation": "front" + }, + { + "type": "asset", + "id": "DESK_SIDE", + "file": "DESK_SIDE.png", + "width": 16, + "height": 64, + "footprintW": 1, + "footprintH": 4, + "orientation": "side" + } + ] +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png b/webview-ui/public/assets/furniture/DOUBLE_BOOKSHELF/DOUBLE_BOOKSHELF.png new file mode 100644 index 0000000000000000000000000000000000000000..f406a7b29483acd4ff72ccd6d7fed9907aea094d GIT binary patch literal 627 zcmV-(0*w8MP)>^Bt%0B4KV~E5CSQpcA}yvC_Wl(Y!p$qP*+s25D`)T0iT7P zHll@%4=h9rEwmB^(Za?~(UsqUJ6s6}1j zBobd;npKSUium#Pxk&i%1KRA-6WHB5j8piK{8qqduV}pZ{D2?unw~lU1t`cV6foK= z8ZSOS;0N1#Wq_V<-vuyaH4G7M-OfGCbD)gkco2@%l7 zXYXYXuucTD?J+$a?jnmpKyN*PjI=M>zhtmqfaKM?6oL%bk7%rE8xtTQ0y&34{M(8g!)We>1U1hnm;Cqw7}Ua!%ITIc1Y1GEk< zN>Di5#URlW;I{~h!~lsCc>X8gb-OMB00960A>pTY00006NkladvQU^jo-e@mn}Lx`^N+0Y`5`tvZOe1~g(b z6)TQyeutj$=54mN^vStD=aPThjYIw%Ey1P!)!n7l<;1+fDppd?bOO$6_s_(IJLlr| z+U2^mzN| zSB!s-L*l8MC(>8NpPqaTsLmOjeB=ZQ6i@8@m6*!@2Yd1CVy>nk?1!Q&z|a}kporfp zH3hqFU_Ex%sFDPL4T29h3!KE(WVZ$oD~zuY9CfPj1SuAdLz3|kQ>R8*ZSKM5qdtVEAUNB<*8F)%jS z#+0BPV0(eO(m_`?$%X?2HKJg10yZ>@*;4`mFRwayME15;`4qGSLRxw{QIQd(1om_Itu$oo zENG@X@bYpe$lsqRNdPWmYHlxENl^52Y;l?*IVOm_uwXWNS}5J#!HCgFR-qj6awqHR zSrX22W(;p158|IsM%O#UjxcW|t36SCn8o_ondP~s$jm!|^POSVj1=+6w!P#Q$r-)y z5-lb(p{sl`IXmF&_b`&|RstP_Y*HoTIAvz8Gbeyh-$(7Y6>JWZV)P6_DwU#fiC}ou z3z!5(`i3+tofx9Y^f-0G#T2)i;VkuQB+cndBRAvXq4jW5JV#JT=~oi z;3N`}k?4=$?>d5m12Kf|!Ohhd?}yxJI9pCw$oiRuBE*C3P+LIRfea*X*HHDf62D*_ zhQEe+|9AnGWgf&9crYFtz|VS1A8J_E1seUV-APv#HMrfyGPxfc;y!RiW@le!4K$6&KL$~fV&Ok6bCag;lQ7& zPO2`ok}E$wKY;keAQHSZ?3C!qP6%Xwq=T+Gn7q6tWWT}@i?Sm00030|NHjDdH?_b21!Ig aR09CX$3R~qYDq@`0000w)qc(mkgECsESSU8*JO|h1-21+c_Z7rugf)Agwb$8uopaCJ!P)Yb|E(EVyMeVE zc{m1s)7BP~IY(KJ;L?be`J87@kzJT67buma? z>=-~sD7ppE#A!6OmeZ~VIwljN)$8nm(=*m%29|E#nK``cLOq#{go(t4i#X=2+E)CRhVmQ%0#_)-9S|)tq(Y5Fp(8MMp!<`;9!3E6W4Z_)z z9FGH_^{XtOG|@3Y^eSk`oNK2w*ia?|JkW|D2ZywVUIUsbdchjXA-Ul+2ICn?%>c-W zX?;o21#T-QItDx>4T(KK&E$~WaIZN!dzej~7Oa7})&x!+ItGwwm`xhseMi(mOOMN+ zKYY221G(F9TEIc}8<-ox>mx$10R<+Ukl0Yb@AzlF%>VpVg`qUoe}6GoqM?HrmqX0i zr$6qc1{8^c`H$BX!}0s}ohTsJOb+TyFm0HV)5t-t=HrvYss;!qaq{@35%u)c?E)dx zm|%(#AAx~#@OB(1dU3D;*S%%adV9x!Q$xVLbYaWJ(3&2lnB`p|0R(_oN+ zG~D#bhoujx4~eg3V90=1X$Kkb+4>j`9y;IYi&LZN6jvbz8PLZ7Ft7yI`7tT06j00000NkvXXu0mjfshwN7 literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/LARGE_PLANT/manifest.json b/webview-ui/public/assets/furniture/LARGE_PLANT/manifest.json new file mode 100644 index 00000000..4bde65be --- /dev/null +++ b/webview-ui/public/assets/furniture/LARGE_PLANT/manifest.json @@ -0,0 +1,13 @@ +{ + "id": "LARGE_PLANT", + "name": "Large Plant", + "category": "decor", + "type": "asset", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 0, + "width": 32, + "height": 48, + "footprintW": 2, + "footprintH": 3 +} \ No newline at end of file diff --git a/webview-ui/public/assets/furniture/PC/PC_BACK.png b/webview-ui/public/assets/furniture/PC/PC_BACK.png new file mode 100644 index 0000000000000000000000000000000000000000..773c1fd5fa4889836e2dc0db7b7cd05ad790f945 GIT binary patch literal 349 zcmV-j0iyniP)MJ%*b6!iig!%nc(N-%bl z$;^9ihWvq)tl;%AFnixS=#9qLEYRWk1_(Rtu4MrlOx%DZFWlg`-r4DV$pkbEbpsNi z{QqZZ5Kl2?5UrQKg__C8mT(^HF#{S&0yQDjdoqB`q!8#O5j04W(w>VO=!2LJhG~Oh zJ#o?c2D8Q72|8PUcySX)8aIGT0YG3v2=w3pG{6mhf4)tK_8UusctkhAwLhL%z1px) zTt$1Y(^Ia9);A!jG;0=$OsO|p29!TWH=uwx6hrSqHTb3NA^IwU`v$n>!N=g8|Ajm! vfTr*%+yDRo|Nj^yB7gt@00v1!K~w_(M#2nGUFa}b00000NkvXXu0mjfk5Z4Z literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/PC/PC_FRONT_OFF.png b/webview-ui/public/assets/furniture/PC/PC_FRONT_OFF.png new file mode 100644 index 0000000000000000000000000000000000000000..ad93288181d2f0e2fc3b6671b31b57d62b9b5c9a GIT binary patch literal 427 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 lY{b0400006NklWJmFbO2M-em2 z`Pg@n$`uP;khEeVuLn>|RZmMEm!t{Q`oX?~wWC!vu!%pNmun4}GPN@|unF&lo`3;_ zV6pQdR}<6oV?A>{U79IG4BRdF1`tW)o|~Q^sM5`wi2;M}+Pfh!F&BMQTc7&uec-eC z$zI~0#(;0ao#NLLgVkQ#dBy-``?{&d7iN@f0mpP%ncPL@p>0xYV6ub*Q_NZ4PWQFe z;Ps{A#ckp1Ops=D=pn5ENf@k;ILy0wzyR038mOZalYl{SZ{7UjcwPjEiGVxUUrLi& zSOc1O4>lA6greX-@T>kKsX~KR2x}mOL}`);k2%pr#DEELLO5z}Gk0rHF4t83sR?J+ zJUQN0E~FU^S|iklHF#{dyy|m9U0vQABZOg4LxWcLnvXRg6K6zma5daPzyJc|RCRT`>!@^@nRNm2M9`@;`N0`hu+?TD~SrHj_Kz!Az*`fx@ zn;mslyqW h_j;fQ*+3p-X5f!rq!_oMBnBwL;OXk;vd$@?2>?Lm@?cT(rO?;1s_xQfg^WpBx_pS;5Tnzxw=s|t;z`yKNjY#{1 zD&}Bts13D93zNM9KnjN^SyU78luHMg7_lHEBoIy~5fPIJi9EvM4I-Ho$XR)q-0J|O z$TqWpD&zDrf$5cPEV{;^swz3si3*Tq8J|K7=kW~0@B#R{mLkBU3zGsBz%UG6Ufl$*h26>` zE%3yz6BR&QCNPNlW?*lZAgK~CJPX>Q0u-plO9sb0BbLs>%m8kuo0g~lkx0LHHx}TJ zKEnJ_fbu25$`)z^I9w zCteLy0-OnPlM(3vTEXaEdIF-oTC@!(bI*4uw0Obim+8`r(n3^p+~<`Pj|pP&U*R%5w#UHjkq^fVEM<4z|J zsGM8;;kVf|!5e%(;`zbUPm2h*1pV}e$`lvYLkyd5Xh;7x`ozh=@c;kUb!S!r9n1!D bFf+pqneY!BKW8=rMHoC?{an^LB{Ts5A^%xJ literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_SIDE.png b/webview-ui/public/assets/furniture/SMALL_TABLE/SMALL_TABLE_SIDE.png new file mode 100644 index 0000000000000000000000000000000000000000..675490c5d7f70367ae6f228a297a7f44ab2bcc4d GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^0zhoQ!3HEH;(vt$scD`rjv*C{Z$cOHHW&!}Q3%u! zVd6h%$n3p=soH>xIbjB)V^fNN&KEn)efPPJnA%=`vhMBQ_}QLM7Bq0!dOSS1(dWDk zpIXE0+}EnU(K8tIPS04>Gx0s=;U_#P>YIdPr46{xv7{-6imlpJQrG5t{^zQOR)HUH zLm2k2VHF5${JZBUlPJT+_eC2SVr&_wtohN&@GgmQ-CrgKhX4O9W`sThx{wXzLS}~A W@MVVY-3|PKA`G6celF{r5}E*=15nif literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/SMALL_TABLE/manifest.json b/webview-ui/public/assets/furniture/SMALL_TABLE/manifest.json new file mode 100644 index 00000000..eed5383f --- /dev/null +++ b/webview-ui/public/assets/furniture/SMALL_TABLE/manifest.json @@ -0,0 +1,33 @@ +{ + "id": "SMALL_TABLE", + "name": "Small Table", + "category": "desks", + "type": "group", + "groupType": "rotation", + "rotationScheme": "2-way", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 1, + "members": [ + { + "type": "asset", + "id": "SMALL_TABLE_FRONT", + "file": "SMALL_TABLE_FRONT.png", + "width": 32, + "height": 32, + "footprintW": 2, + "footprintH": 2, + "orientation": "front" + }, + { + "type": "asset", + "id": "SMALL_TABLE_SIDE", + "file": "SMALL_TABLE_SIDE.png", + "width": 16, + "height": 48, + "footprintW": 1, + "footprintH": 3, + "orientation": "side" + } + ] +} diff --git a/webview-ui/public/assets/furniture/SOFA/SOFA_BACK.png b/webview-ui/public/assets/furniture/SOFA/SOFA_BACK.png new file mode 100644 index 0000000000000000000000000000000000000000..996fa46677852da3eff9f12d17c5ac4b87eae443 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYeRH>(nV@L(#o5>4#8w>5pP>yQ-irs2q<3tQjvi01xNja|5Zk#AhkcfIwt%1?riGgRBVvvuSv$Q|>O zIB>SU>Gr-)^?git{xJRE@QG>+ThPaJ!=3Gho$iJ>_IIu){bV1+m`$}m=&#zKV=WA{ m_y7N_-Py~43^tGhm>E8tF)iBCeZU(i!rR|=~&3FBG!8Bkfgwd`%A)axaXWa{VeYD^=qB>&SCp`_C(1Xshr5c zyu*|+Q zE7*_Di8^rQn^ye%-8_sb&;9^i^8bGh?;my`gAL>)W`@0orxsPL+g%S7VeoYIb6Mw< G&;$T#3{3z4 literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/SOFA/SOFA_SIDE.png b/webview-ui/public/assets/furniture/SOFA/SOFA_SIDE.png new file mode 100644 index 0000000000000000000000000000000000000000..77d1621665cef1121a90bb5a6d9df685436ca589 GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3Qd>M-978G?-%M8IYjzNDRiCqH z+p=Y=JVWyX8xJY$6!hTfvR2rrVIp>}f#ILZAW%H==s*yN0xACb1A*e&L-u5HJi^G#Ug%F&Q?2 zfMK|z(I6m-$*>6o48s+T1_4n_hD{(~7_MkE2#8`bYyttpa7CkS2#Ai>X@UWr9Mh+O zKzepMJ3&Av$Mh*6ke;2+P7u(^F?|XMq-UqIlL<^`6P;-Vr?i^c0G&$&LmkYO$mk@Nz^);FV<*11x470VFruHH8E_Bqae9Z>Ix= z0JDpz0%)2~W+Xwb4tP_ik9)iVyW^fD{GcjgIo=>v^~su7fSWK(!g($7azw4JB-j znUcWee5H^SB^&|#1`J073JG>diCo}$f3#x461e~;V#g$qF7W>sus&5E00030|Jq0< iQ2+n{21!IgR09Ap+Qv|bAZ2g>0000VNVNa+5p+*ay-=ol zv}1apE`!v{Q@jD@qDNoMoP07Nm|gd?oN9$+62qMX1}fhQYZ(n@T+ymMbY`mfgIP}H z%op^zYGT8GZf&^vR`AVEpgaEmf8_Rj4UoYGau74aW2b!M80J$MKoJH{S3j3^P6KK!zQNS;BS0ng~i?!jt4X;Q$}748W9xLD4fb)Wqhk(^#);0#EP+dU*c`!S;C z#Gqq90jxJyr~|7;gK4m<4+WwRtA=hGAf)1-3{Lf3F6W!T({kLvyY6=i=5cf|o zlJpVlz0>$e(5A!vgF@Ks)|@SdQ9f*wPo{*>@e#du5TsZ3DZ!nvG<;kqc z>T@PAKffxq^AYD33E|yp3G5a1chieFvY&LBGppq}iVNE^wnpqylkj&-T4<$i-t7Ew zLQT1u<9e?-4%{0zsvWrJR?$4Gp)Gf#|Lg}*9*Y%nvQGb3*O0e-c=z;-y4}jx?QGxM ri`D7_z4!lrDo?BdkiiD>Co{u8m;J$J<#X=?MHoC?{an^LB{Ts5CCqbe literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_SIDE.png b/webview-ui/public/assets/furniture/WOODEN_CHAIR/WOODEN_CHAIR_SIDE.png new file mode 100644 index 0000000000000000000000000000000000000000..f9c58772117e776e99254fb87cc10d1f941d4d77 GIT binary patch literal 261 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3QoB4|978G?-vk-*H5>4l$Xb`a zy0Wl(wci)szb^7u&X*)7rlmM<_`szW_CUwMb3-drPiINtn;5-`#p<^#n-~IL91OJm zQEPvfB|iDw{tp##HRp}$#UA7w7Gdf8)OhpHN&YC4oe`h)pSXI5-V{*!vw)F3W!3tBR3B%3)dpXTt~mO*dFyNr^E@ z1+LzFpdh(rIltkwjurjCC+@z_%)s#f|D*b_JfJVwK)zsR(0rPz@O-M;ZlDN*r>mdK II;Vst0MgEBmH+?% literal 0 HcmV?d00001 diff --git a/webview-ui/public/assets/furniture/WOODEN_CHAIR/manifest.json b/webview-ui/public/assets/furniture/WOODEN_CHAIR/manifest.json new file mode 100644 index 00000000..4b6730a1 --- /dev/null +++ b/webview-ui/public/assets/furniture/WOODEN_CHAIR/manifest.json @@ -0,0 +1,44 @@ +{ + "id": "WOODEN_CHAIR", + "name": "Wooden Chair", + "category": "chairs", + "type": "group", + "groupType": "rotation", + "rotationScheme": "3-way-mirror", + "canPlaceOnWalls": false, + "canPlaceOnSurfaces": false, + "backgroundTiles": 1, + "members": [ + { + "type": "asset", + "id": "WOODEN_CHAIR_FRONT", + "file": "WOODEN_CHAIR_FRONT.png", + "width": 16, + "height": 32, + "footprintW": 1, + "footprintH": 2, + "orientation": "front" + }, + { + "type": "asset", + "id": "WOODEN_CHAIR_BACK", + "file": "WOODEN_CHAIR_BACK.png", + "width": 16, + "height": 32, + "footprintW": 1, + "footprintH": 2, + "orientation": "back" + }, + { + "type": "asset", + "id": "WOODEN_CHAIR_SIDE", + "file": "WOODEN_CHAIR_SIDE.png", + "width": 16, + "height": 32, + "footprintW": 1, + "footprintH": 2, + "orientation": "side", + "mirrorSide": true + } + ] +} \ No newline at end of file diff --git a/webview-ui/public/assets/walls.png b/webview-ui/public/assets/walls/wall_0.png similarity index 100% rename from webview-ui/public/assets/walls.png rename to webview-ui/public/assets/walls/wall_0.png diff --git a/webview-ui/src/fonts/FSPixelSansUnicode-Regular.ttf b/webview-ui/public/fonts/FSPixelSansUnicode-Regular.ttf similarity index 100% rename from webview-ui/src/fonts/FSPixelSansUnicode-Regular.ttf rename to webview-ui/public/fonts/FSPixelSansUnicode-Regular.ttf diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 1bf65413..ed75ef18 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -135,10 +135,15 @@ function App() { subagentTools, subagentCharacters, layoutReady, + layoutWasReset, loadedAssets, workspaceFolders, } = useExtensionMessages(getOfficeState, editor.setLastSavedLayout, isEditDirty); + // Show migration notice once layout reset is detected + const [migrationNoticeDismissed, setMigrationNoticeDismissed] = useState(false); + const showMigrationNotice = layoutWasReset && !migrationNoticeDismissed; + const [isDebugMode, setIsDebugMode] = useState(false); const handleToggleDebugMode = useCallback(() => setIsDebugMode((prev) => !prev), []); @@ -226,6 +231,7 @@ function App() { 50% { opacity: 0.3; } } .pixel-agents-pulse { animation: pixel-agents-pulse ${PULSE_ANIMATION_DURATION_SEC}s ease-in-out infinite; } + .pixel-agents-migration-btn:hover { filter: brightness(0.8); } `} - + {!isDebugMode && } {/* Vignette overlay */}

- Press R to rotate + Rotate (R)
)} @@ -310,10 +316,12 @@ function App() { selectedFurnitureColor={selColor} floorColor={editorState.floorColor} wallColor={editorState.wallColor} + selectedWallSet={editorState.selectedWallSet} onToolChange={editor.handleToolChange} onTileTypeChange={editor.handleTileTypeChange} onFloorColorChange={editor.handleFloorColorChange} onWallColorChange={editor.handleWallColorChange} + onWallSetChange={editor.handleWallSetChange} onSelectedFurnitureColorChange={editor.handleSelectedFurnitureColorChange} onFurnitureTypeChange={editor.handleFurnitureTypeChange} loadedAssets={loadedAssets} @@ -321,16 +329,18 @@ function App() { ); })()} - + {!isDebugMode && ( + + )} {isDebugMode && ( )} + + {showMigrationNotice && ( +
setMigrationNoticeDismissed(true)} + > +
e.stopPropagation()} + > +
+ We owe you an apology! +
+

+ We've just migrated to fully open-source assets, all built from scratch with love. + Unfortunately, this means your previous layout had to be reset. +

+

+ We're really sorry about that. +

+

+ The good news? This was a one-time thing, and it paves the way for some genuinely + exciting updates ahead. +

+

+ Stay tuned, and thanks for using Pixel Agents! +

+ +
+
+ )}
); } diff --git a/webview-ui/src/constants.ts b/webview-ui/src/constants.ts index afc40e2e..ccb3890e 100644 --- a/webview-ui/src/constants.ts +++ b/webview-ui/src/constants.ts @@ -96,6 +96,9 @@ export const NOTIFICATION_NOTE_2_START_SEC = 0.1; export const NOTIFICATION_NOTE_DURATION_SEC = 0.18; export const NOTIFICATION_VOLUME = 0.14; +// ── Furniture Animation ───────────────────────────────────── +export const FURNITURE_ANIM_INTERVAL_SEC = 0.2; + // ── Game Logic ─────────────────────────────────────────────── export const MAX_DELTA_TIME_SEC = 0.1; export const WAITING_BUBBLE_DURATION_SEC = 2.0; diff --git a/webview-ui/src/hooks/useEditorActions.ts b/webview-ui/src/hooks/useEditorActions.ts index a8940eb2..2f7bc0c2 100644 --- a/webview-ui/src/hooks/useEditorActions.ts +++ b/webview-ui/src/hooks/useEditorActions.ts @@ -46,6 +46,7 @@ export interface EditorActions { handleTileTypeChange: (type: TileTypeVal) => void; handleFloorColorChange: (color: FloorColor) => void; handleWallColorChange: (color: FloorColor) => void; + handleWallSetChange: (setIndex: number) => void; handleSelectedFurnitureColorChange: (color: FloorColor | null) => void; handleFurnitureTypeChange: (type: string) => void; // FurnitureType enum or asset ID handleDeleteSelected: () => void; @@ -203,6 +204,14 @@ export function useEditorActions( [editorState, getOfficeState, saveLayout], ); + const handleWallSetChange = useCallback( + (setIndex: number) => { + editorState.selectedWallSet = setIndex; + setEditorTick((n) => n + 1); + }, + [editorState], + ); + // Track which uid we've already pushed undo for during color editing // so dragging sliders doesn't create N undo entries const colorEditUidRef = useRef(null); @@ -608,6 +617,7 @@ export function useEditorActions( handleTileTypeChange, handleFloorColorChange, handleWallColorChange, + handleWallSetChange, handleSelectedFurnitureColorChange, handleFurnitureTypeChange, handleDeleteSelected, diff --git a/webview-ui/src/hooks/useExtensionMessages.ts b/webview-ui/src/hooks/useExtensionMessages.ts index fb283090..f86a8182 100644 --- a/webview-ui/src/hooks/useExtensionMessages.ts +++ b/webview-ui/src/hooks/useExtensionMessages.ts @@ -30,10 +30,15 @@ export interface FurnitureAsset { footprintH: number; isDesk: boolean; canPlaceOnWalls: boolean; - partOfGroup?: boolean; groupId?: string; canPlaceOnSurfaces?: boolean; backgroundTiles?: number; + orientation?: string; + state?: string; + mirrorSide?: boolean; + rotationScheme?: string; + animationGroup?: string; + frame?: number; } export interface WorkspaceFolder { @@ -49,6 +54,7 @@ export interface ExtensionMessageState { subagentTools: Record>; subagentCharacters: SubagentCharacter[]; layoutReady: boolean; + layoutWasReset: boolean; loadedAssets?: { catalog: FurnitureAsset[]; sprites: Record }; workspaceFolders: WorkspaceFolder[]; } @@ -76,6 +82,7 @@ export function useExtensionMessages( >({}); const [subagentCharacters, setSubagentCharacters] = useState([]); const [layoutReady, setLayoutReady] = useState(false); + const [layoutWasReset, setLayoutWasReset] = useState(false); const [loadedAssets, setLoadedAssets] = useState< { catalog: FurnitureAsset[]; sprites: Record } | undefined >(); @@ -120,6 +127,9 @@ export function useExtensionMessages( pendingAgents = []; layoutReadyRef.current = true; setLayoutReady(true); + if (msg.wasReset) { + setLayoutWasReset(true); + } if (os.characters.size > 0) { saveAgentSeats(os); } @@ -365,9 +375,9 @@ export function useExtensionMessages( console.log(`[Webview] Received ${sprites.length} floor tile patterns`); setFloorSprites(sprites); } else if (msg.type === 'wallTilesLoaded') { - const sprites = msg.sprites as string[][][]; - console.log(`[Webview] Received ${sprites.length} wall tile sprites`); - setWallSprites(sprites); + const sets = msg.sets as string[][][][]; + console.log(`[Webview] Received ${sets.length} wall tile set(s)`); + setWallSprites(sets); } else if (msg.type === 'workspaceFolders') { const folders = msg.folders as WorkspaceFolder[]; setWorkspaceFolders(folders); @@ -400,6 +410,7 @@ export function useExtensionMessages( subagentTools, subagentCharacters, layoutReady, + layoutWasReset, loadedAssets, workspaceFolders, }; diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 24c5d076..a3b8ddc9 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -3,7 +3,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url('./fonts/FSPixelSansUnicode-Regular.ttf') format('truetype'); + src: url('/fonts/FSPixelSansUnicode-Regular.ttf') format('truetype'); } :root { @@ -47,8 +47,8 @@ --pixel-status-active: var(--vscode-charts-blue, #3794ff); /* ToolOverlay z-index layers */ - --pixel-overlay-z: 100; - --pixel-overlay-selected-z: 110; + --pixel-overlay-z: 41; + --pixel-overlay-selected-z: 42; --pixel-controls-z: 50; } diff --git a/webview-ui/src/office/colorize.ts b/webview-ui/src/office/colorize.ts index 9da06e7b..62b3cb60 100644 --- a/webview-ui/src/office/colorize.ts +++ b/webview-ui/src/office/colorize.ts @@ -76,10 +76,13 @@ export function colorizeSprite(sprite: SpriteData, color: FloorColor): SpriteDat // Clamp lightness = Math.max(0, Math.min(1, lightness)); + // Preserve original alpha + const alpha = extractAlpha(pixel); + // Convert HSL to RGB const satFrac = s / 100; const hex = hslToHex(h, satFrac, lightness); - newRow.push(hex); + newRow.push(appendAlpha(hex, alpha)); } result.push(newRow); } @@ -87,6 +90,17 @@ export function colorizeSprite(sprite: SpriteData, color: FloorColor): SpriteDat return result; } +/** Extract alpha from a hex pixel string. Returns 255 for #RRGGBB, parsed value for #RRGGBBAA. */ +function extractAlpha(pixel: string): number { + return pixel.length > 7 ? parseInt(pixel.slice(7, 9), 16) : 255; +} + +/** Append alpha to a #RRGGBB hex string, omitting if fully opaque. */ +function appendAlpha(hex: string, alpha: number): string { + if (alpha >= 255) return hex; + return `${hex}${alpha.toString(16).padStart(2, '0').toUpperCase()}`; +} + /** Convert HSL (h: 0-360, s: 0-1, l: 0-1) to #RRGGBB hex string */ function hslToHex(h: number, s: number, l: number): string { const c = (1 - Math.abs(2 * l - 1)) * s; @@ -175,6 +189,7 @@ export function adjustSprite(sprite: SpriteData, color: FloorColor): SpriteData const r = parseInt(pixel.slice(1, 3), 16); const g = parseInt(pixel.slice(3, 5), 16); const bv = parseInt(pixel.slice(5, 7), 16); + const alpha = extractAlpha(pixel); const [origH, origS, origL] = rgbToHsl(r, g, bv); // Shift hue @@ -198,7 +213,7 @@ export function adjustSprite(sprite: SpriteData, color: FloorColor): SpriteData lightness = Math.max(0, Math.min(1, lightness)); const hex = hslToHex(newH, newS, lightness); - newRow.push(hex); + newRow.push(appendAlpha(hex, alpha)); } result.push(newRow); } diff --git a/webview-ui/src/office/components/OfficeCanvas.tsx b/webview-ui/src/office/components/OfficeCanvas.tsx index b8c6c20b..65475866 100644 --- a/webview-ui/src/office/components/OfficeCanvas.tsx +++ b/webview-ui/src/office/components/OfficeCanvas.tsx @@ -135,6 +135,7 @@ export function OfficeCanvas({ editorRender = { showGrid: true, ghostSprite: null, + ghostMirrored: false, ghostCol: editorState.ghostCol, ghostRow: editorState.ghostRow, ghostValid: editorState.ghostValid, @@ -161,6 +162,8 @@ export function OfficeCanvas({ ); editorRender.ghostSprite = entry.sprite; editorRender.ghostRow = placementRow; + editorRender.ghostMirrored = + !!entry.mirrorSide && editorState.selectedFurnitureType.endsWith(':left'); editorRender.ghostValid = canPlaceFurniture( officeState.getLayout(), editorState.selectedFurnitureType, @@ -183,6 +186,8 @@ export function OfficeCanvas({ editorRender.ghostSprite = entry.sprite; editorRender.ghostCol = ghostCol; editorRender.ghostRow = ghostRow; + editorRender.ghostMirrored = + !!entry.mirrorSide && draggedItem.type.endsWith(':left'); editorRender.ghostValid = canPlaceFurniture( officeState.getLayout(), draggedItem.type, diff --git a/webview-ui/src/office/editor/EditorToolbar.tsx b/webview-ui/src/office/editor/EditorToolbar.tsx index 78c5d382..2bd8c92a 100644 --- a/webview-ui/src/office/editor/EditorToolbar.tsx +++ b/webview-ui/src/office/editor/EditorToolbar.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import { getColorizedSprite } from '../colorize.js'; import { getColorizedFloorSprite, getFloorPatternCount, hasFloorSprites } from '../floorTiles.js'; import type { FurnitureCategory, LoadedAssetData } from '../layout/furnitureCatalog.js'; +import { getWallSetCount, getWallSetPreviewSprite } from '../wallTiles.js'; import { buildDynamicCatalog, getActiveCategories, @@ -53,10 +55,12 @@ interface EditorToolbarProps { selectedFurnitureColor: FloorColor | null; floorColor: FloorColor; wallColor: FloorColor; + selectedWallSet: number; onToolChange: (tool: EditTool) => void; onTileTypeChange: (type: TileTypeVal) => void; onFloorColorChange: (color: FloorColor) => void; onWallColorChange: (color: FloorColor) => void; + onWallSetChange: (setIndex: number) => void; onSelectedFurnitureColorChange: (color: FloorColor | null) => void; onFurnitureTypeChange: (type: string) => void; loadedAssets?: LoadedAssetData; @@ -123,6 +127,68 @@ function FloorPatternPreview({ ); } +/** Render a wall set preview showing the first piece (bitmask 0, 16×32) at 1x scale */ +function WallSetPreview({ + setIndex, + color, + selected, + onClick, +}: { + setIndex: number; + color: FloorColor; + selected: boolean; + onClick: () => void; +}) { + const canvasRef = useRef(null); + const displayW = 32; + const displayH = 64; + const previewZoom = 2; + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + canvas.width = displayW; + canvas.height = displayH; + ctx.imageSmoothingEnabled = false; + + const sprite = getWallSetPreviewSprite(setIndex); + if (!sprite) { + ctx.fillStyle = '#444'; + ctx.fillRect(0, 0, displayW, displayH); + return; + } + + // Colorize the preview sprite using the same colorize path as rendering + const cacheKey = `wall-preview-${setIndex}-${color.h}-${color.s}-${color.b}-${color.c}`; + const colorized = getColorizedSprite(cacheKey, sprite, { ...color, colorize: true }); + const cached = getCachedSprite(colorized, previewZoom); + ctx.drawImage(cached, 0, 0); + }, [setIndex, color]); + + return ( + + ); +} + /** Slider control for a single color parameter */ function ColorSlider({ label, @@ -171,10 +237,12 @@ export function EditorToolbar({ selectedFurnitureColor, floorColor, wallColor, + selectedWallSet, onToolChange, onTileTypeChange, onFloorColorChange, onWallColorChange, + onWallSetChange, onSelectedFurnitureColorChange, onFurnitureTypeChange, loadedAssets, @@ -441,6 +509,29 @@ export function EditorToolbar({ />
)} + + {/* Wall set picker — horizontal carousel at the top */} + {getWallSetCount() > 0 && ( +
+ {Array.from({ length: getWallSetCount() }, (_, i) => ( + onWallSetChange(i)} + /> + ))} +
+ )}
)} diff --git a/webview-ui/src/office/editor/editorState.ts b/webview-ui/src/office/editor/editorState.ts index dc8bb784..67a9b656 100644 --- a/webview-ui/src/office/editor/editorState.ts +++ b/webview-ui/src/office/editor/editorState.ts @@ -6,7 +6,7 @@ export class EditorState { isEditMode = false; activeTool: EditTool = EditTool.SELECT; selectedTileType: TileTypeVal = TileType.FLOOR_1; - selectedFurnitureType: string = 'desk'; // FurnitureType.DESK or asset ID + selectedFurnitureType = ''; // asset ID, set when catalog loads // Floor color settings (applied to new tiles when painting) floorColor: FloorColor = { ...DEFAULT_FLOOR_COLOR }; @@ -14,6 +14,9 @@ export class EditorState { // Wall color settings (applied to new wall tiles when painting) wallColor: FloorColor = { ...DEFAULT_WALL_COLOR }; + // Selected wall set index (0-based, indexes into loaded wall sets) + selectedWallSet = 0; + // Tracks toggle direction during wall drag (true=adding walls, false=removing, null=undecided) wallDragAdding: boolean | null = null; diff --git a/webview-ui/src/office/engine/officeState.ts b/webview-ui/src/office/engine/officeState.ts index 47126b36..d89d8cd6 100644 --- a/webview-ui/src/office/engine/officeState.ts +++ b/webview-ui/src/office/engine/officeState.ts @@ -5,6 +5,7 @@ import { CHARACTER_HIT_HEIGHT, CHARACTER_SITTING_OFFSET_PX, DISMISS_BUBBLE_FAST_FADE_SEC, + FURNITURE_ANIM_INTERVAL_SEC, HUE_SHIFT_MIN_DEG, HUE_SHIFT_RANGE_DEG, INACTIVE_SEAT_TIMER_MIN_SEC, @@ -12,7 +13,7 @@ import { PALETTE_COUNT, WAITING_BUBBLE_DURATION_SEC, } from '../../constants.js'; -import { getCatalogEntry, getOnStateType } from '../layout/furnitureCatalog.js'; +import { getAnimationFrames, getCatalogEntry, getOnStateType } from '../layout/furnitureCatalog.js'; import { createDefaultLayout, getBlockedTiles, @@ -41,6 +42,8 @@ export class OfficeState { furniture: FurnitureInstance[]; walkableTiles: Array<{ col: number; row: number }>; characters: Map = new Map(); + /** Accumulated time for furniture animation frame cycling */ + furnitureAnimTimer = 0; selectedAgentId: number | null = null; cameraFollowId: number | null = null; hoveredAgentId: number | null = null; @@ -567,7 +570,8 @@ export class OfficeState { return; } - // Build modified furniture list with auto-state applied + // Build modified furniture list with auto-state and animation applied + const animFrame = Math.floor(this.furnitureAnimTimer / FURNITURE_ANIM_INTERVAL_SEC); const modifiedFurniture: PlacedFurniture[] = this.layout.furniture.map((item) => { const entry = getCatalogEntry(item.type); if (!entry) return item; @@ -575,8 +579,14 @@ export class OfficeState { for (let dr = 0; dr < entry.footprintH; dr++) { for (let dc = 0; dc < entry.footprintW; dc++) { if (autoOnTiles.has(`${item.col + dc},${item.row + dr}`)) { - const onType = getOnStateType(item.type); + let onType = getOnStateType(item.type); if (onType !== item.type) { + // Check if the on-state type has animation frames + const frames = getAnimationFrames(onType); + if (frames && frames.length > 1) { + const frameIdx = animFrame % frames.length; + onType = frames[frameIdx]; + } return { ...item, type: onType }; } return item; @@ -634,6 +644,14 @@ export class OfficeState { } update(dt: number): void { + // Furniture animation cycling + const prevFrame = Math.floor(this.furnitureAnimTimer / FURNITURE_ANIM_INTERVAL_SEC); + this.furnitureAnimTimer += dt; + const newFrame = Math.floor(this.furnitureAnimTimer / FURNITURE_ANIM_INTERVAL_SEC); + if (newFrame !== prevFrame) { + this.rebuildFurnitureInstances(); + } + const toDelete: number[] = []; for (const ch of this.characters.values()) { // Handle matrix effect animation diff --git a/webview-ui/src/office/engine/renderer.ts b/webview-ui/src/office/engine/renderer.ts index e56e4456..9e141925 100644 --- a/webview-ui/src/office/engine/renderer.ts +++ b/webview-ui/src/office/engine/renderer.ts @@ -121,12 +121,25 @@ export function renderScene( const cached = getCachedSprite(f.sprite, zoom); const fx = offsetX + f.x * zoom; const fy = offsetY + f.y * zoom; - drawables.push({ - zY: f.zY, - draw: (c) => { - c.drawImage(cached, fx, fy); - }, - }); + if (f.mirrored) { + drawables.push({ + zY: f.zY, + draw: (c) => { + c.save(); + c.translate(fx + cached.width, fy); + c.scale(-1, 1); + c.drawImage(cached, 0, 0); + c.restore(); + }, + }); + } else { + drawables.push({ + zY: f.zY, + draw: (c) => { + c.drawImage(cached, fx, fy); + }, + }); + } } // Characters @@ -334,14 +347,23 @@ export function renderGhostPreview( offsetX: number, offsetY: number, zoom: number, + mirrored: boolean = false, ): void { const cached = getCachedSprite(sprite, zoom); const x = offsetX + col * TILE_SIZE * zoom; const y = offsetY + row * TILE_SIZE * zoom; ctx.save(); ctx.globalAlpha = GHOST_PREVIEW_SPRITE_ALPHA; - ctx.drawImage(cached, x, y); - // Tint overlay + if (mirrored) { + ctx.translate(x + cached.width, y); + ctx.scale(-1, 1); + ctx.drawImage(cached, 0, 0); + } else { + ctx.drawImage(cached, x, y); + } + // Tint overlay — reset transform for correct fill position + ctx.restore(); + ctx.save(); ctx.globalAlpha = GHOST_PREVIEW_TINT_ALPHA; ctx.fillStyle = valid ? GHOST_VALID_TINT : GHOST_INVALID_TINT; ctx.fillRect(x, y, cached.width, cached.height); @@ -508,6 +530,7 @@ export type RotateButtonBounds = ButtonBounds; export interface EditorRenderState { showGrid: boolean; ghostSprite: SpriteData | null; + ghostMirrored: boolean; ghostCol: number; ghostRow: number; ghostValid: boolean; @@ -622,6 +645,7 @@ export function renderFrame( offsetX, offsetY, zoom, + editor.ghostMirrored, ); } if (editor.hasSelection) { diff --git a/webview-ui/src/office/floorTiles.ts b/webview-ui/src/office/floorTiles.ts index 3afb06fd..7778490c 100644 --- a/webview-ui/src/office/floorTiles.ts +++ b/webview-ui/src/office/floorTiles.ts @@ -1,7 +1,7 @@ /** * Floor tile pattern storage and caching. * - * Stores 7 grayscale floor patterns loaded from floors.png. + * Stores grayscale floor patterns loaded from individual PNGs in assets/floors/. * Uses shared colorize module for HSL tinting (Photoshop-style Colorize). * Caches colorized SpriteData by (pattern, h, s, b, c) key. */ @@ -10,7 +10,7 @@ import { FALLBACK_FLOOR_COLOR, TILE_SIZE } from '../constants.js'; import { clearColorizeCache, getColorizedSprite } from './colorize.js'; import type { FloorColor, SpriteData } from './types.js'; -/** Default solid gray 16×16 tile used when floors.png is not loaded */ +/** Default solid gray 16×16 tile used when floor tile PNGs are not loaded */ const DEFAULT_FLOOR_SPRITE: SpriteData = Array.from( { length: TILE_SIZE }, () => Array(TILE_SIZE).fill(FALLBACK_FLOOR_COLOR) as string[], diff --git a/webview-ui/src/office/layout/furnitureCatalog.ts b/webview-ui/src/office/layout/furnitureCatalog.ts index 1df08689..c4da43f1 100644 --- a/webview-ui/src/office/layout/furnitureCatalog.ts +++ b/webview-ui/src/office/layout/furnitureCatalog.ts @@ -1,15 +1,4 @@ -import { - BOOKSHELF_SPRITE, - CHAIR_SPRITE, - COOLER_SPRITE, - DESK_SQUARE_SPRITE, - LAMP_SPRITE, - PC_SPRITE, - PLANT_SPRITE, - WHITEBOARD_SPRITE, -} from '../sprites/spriteData.js'; import type { FurnitureCatalogEntry, SpriteData } from '../types.js'; -import { FurnitureType } from '../types.js'; export interface LoadedAssetData { catalog: Array<{ @@ -22,11 +11,15 @@ export interface LoadedAssetData { footprintH: number; isDesk: boolean; groupId?: string; - orientation?: string; // 'front' | 'back' | 'left' | 'right' + orientation?: string; // 'front' | 'back' | 'left' | 'right' | 'side' state?: string; // 'on' | 'off' canPlaceOnSurfaces?: boolean; backgroundTiles?: number; canPlaceOnWalls?: boolean; + mirrorSide?: boolean; + rotationScheme?: string; + animationGroup?: string; + frame?: number; }>; sprites: Record; } @@ -44,82 +37,6 @@ export interface CatalogEntryWithCategory extends FurnitureCatalogEntry { category: FurnitureCategory; } -export const FURNITURE_CATALOG: CatalogEntryWithCategory[] = [ - // ── Original hand-drawn sprites ── - { - type: FurnitureType.DESK, - label: 'Desk', - footprintW: 2, - footprintH: 2, - sprite: DESK_SQUARE_SPRITE, - isDesk: true, - category: 'desks', - }, - { - type: FurnitureType.BOOKSHELF, - label: 'Bookshelf', - footprintW: 1, - footprintH: 2, - sprite: BOOKSHELF_SPRITE, - isDesk: false, - category: 'storage', - }, - { - type: FurnitureType.PLANT, - label: 'Plant', - footprintW: 1, - footprintH: 1, - sprite: PLANT_SPRITE, - isDesk: false, - category: 'decor', - }, - { - type: FurnitureType.COOLER, - label: 'Cooler', - footprintW: 1, - footprintH: 1, - sprite: COOLER_SPRITE, - isDesk: false, - category: 'misc', - }, - { - type: FurnitureType.WHITEBOARD, - label: 'Whiteboard', - footprintW: 2, - footprintH: 1, - sprite: WHITEBOARD_SPRITE, - isDesk: false, - category: 'decor', - }, - { - type: FurnitureType.CHAIR, - label: 'Chair', - footprintW: 1, - footprintH: 1, - sprite: CHAIR_SPRITE, - isDesk: false, - category: 'chairs', - }, - { - type: FurnitureType.PC, - label: 'PC', - footprintW: 1, - footprintH: 1, - sprite: PC_SPRITE, - isDesk: false, - category: 'electronics', - }, - { - type: FurnitureType.LAMP, - label: 'Lamp', - footprintW: 1, - footprintH: 1, - sprite: LAMP_SPRITE, - isDesk: false, - category: 'decor', - }, -]; - // ── Rotation groups ────────────────────────────────────────────── // Flexible rotation: supports 2+ orientations (not just all 4) interface RotationGroup { @@ -139,6 +56,10 @@ const stateGroups = new Map(); const offToOn = new Map(); // off asset → on asset const onToOff = new Map(); // on asset → off asset +// ── Animation groups ──────────────────────────────────────────── +// Maps animation group ID → ordered list of asset IDs by frame index +const animationGroups = new Map(); + // Internal catalog (includes all variants for getCatalogEntry lookups) let internalCatalog: CatalogEntryWithCategory[] | null = null; @@ -175,10 +96,27 @@ export function buildDynamicCatalog(assets: LoadedAssetData): boolean { ...(asset.canPlaceOnSurfaces ? { canPlaceOnSurfaces: true } : {}), ...(asset.backgroundTiles ? { backgroundTiles: asset.backgroundTiles } : {}), ...(asset.canPlaceOnWalls ? { canPlaceOnWalls: true } : {}), + ...(asset.mirrorSide ? { mirrorSide: true } : {}), }; }) .filter((e): e is CatalogEntryWithCategory => e !== null); + // Create virtual ":left" entries for mirrorSide assets. + // These share the same sprite but have a distinct type ID so rotation groups work. + for (const asset of assets.catalog) { + if (asset.mirrorSide && asset.orientation === 'side') { + const sideEntry = allEntries.find((e) => e.type === asset.id); + if (sideEntry) { + allEntries.push({ + ...sideEntry, + type: `${asset.id}:left`, + orientation: 'left', + mirrorSide: true, + }); + } + } + } + if (allEntries.length === 0) return false; // Build rotation groups from groupId + orientation metadata @@ -186,8 +124,10 @@ export function buildDynamicCatalog(assets: LoadedAssetData): boolean { stateGroups.clear(); offToOn.clear(); onToOff.clear(); + animationGroups.clear(); // Phase 1: Collect orientations per group (only "off" or stateless variants for rotation) + // For mirrorSide assets with orientation "side", register as both "right" and virtual "left" const groupMap = new Map>(); // groupId → (orientation → assetId) for (const asset of assets.catalog) { if (asset.groupId && asset.orientation) { @@ -198,25 +138,57 @@ export function buildDynamicCatalog(assets: LoadedAssetData): boolean { orientMap = new Map(); groupMap.set(asset.groupId, orientMap); } - orientMap.set(asset.orientation, asset.id); + + if (asset.orientation === 'side') { + // "side" is registered as "right" in the rotation group + orientMap.set('right', asset.id); + if (asset.mirrorSide) { + // Register the virtual ":left" entry with a distinct type ID + orientMap.set('left', `${asset.id}:left`); + } + } else { + orientMap.set(asset.orientation, asset.id); + } + } + } + + // For 2-way rotation schemes, "side" maps to "right" only (no left) + // Check rotationScheme from assets + const rotationSchemes = new Map(); // groupId → rotationScheme + for (const asset of assets.catalog) { + if (asset.groupId && asset.rotationScheme) { + rotationSchemes.set(asset.groupId, asset.rotationScheme); } } // Phase 2: Register rotation groups with 2+ orientations const nonFrontIds = new Set(); const orientationOrder = ['front', 'right', 'back', 'left']; - for (const orientMap of groupMap.values()) { + for (const [groupId, orientMap] of groupMap) { if (orientMap.size < 2) continue; + const scheme = rotationSchemes.get(groupId); + + // For 2-way scheme, only use front and right (side) + let allowedOrients = orientationOrder; + if (scheme === '2-way') { + allowedOrients = ['front', 'right']; + } + // Build ordered list of available orientations - const orderedOrients = orientationOrder.filter((o) => orientMap.has(o)); + const orderedOrients = allowedOrients.filter((o) => orientMap.has(o)); if (orderedOrients.length < 2) continue; const members: Record = {}; for (const o of orderedOrients) { members[o] = orientMap.get(o)!; } const rg: RotationGroup = { orientations: orderedOrients, members }; + // Register each unique asset ID in the rotation group + const registeredIds = new Set(); for (const id of Object.values(members)) { - rotationGroups.set(id, rg); + if (!registeredIds.has(id)) { + rotationGroups.set(id, rg); + registeredIds.add(id); + } } // Track non-front IDs to exclude from visible catalog for (const [orient, id] of Object.entries(members)) { @@ -234,6 +206,8 @@ export function buildDynamicCatalog(assets: LoadedAssetData): boolean { sm = new Map(); stateMap.set(key, sm); } + // For animation groups, use the first frame as the "on" representative + if (asset.animationGroup && asset.frame !== undefined && asset.frame > 0) continue; sm.set(asset.state, asset.id); } } @@ -251,6 +225,9 @@ export function buildDynamicCatalog(assets: LoadedAssetData): boolean { // Also register rotation groups for "on" state variants (so rotation works on on-state items too) for (const asset of assets.catalog) { if (asset.groupId && asset.orientation && asset.state === 'on') { + // Skip non-first animation frames + if (asset.animationGroup && asset.frame !== undefined && asset.frame > 0) continue; + // Find the off-variant's rotation group const offCounterpart = stateGroups.get(asset.id); if (offCounterpart) { @@ -278,7 +255,27 @@ export function buildDynamicCatalog(assets: LoadedAssetData): boolean { } } - // Track "on" variant IDs to exclude from visible catalog + // Phase 4: Build animation groups + const animGroupCollector = new Map>(); + for (const asset of assets.catalog) { + if (asset.animationGroup && asset.frame !== undefined) { + let frames = animGroupCollector.get(asset.animationGroup); + if (!frames) { + frames = []; + animGroupCollector.set(asset.animationGroup, frames); + } + frames.push({ id: asset.id, frame: asset.frame }); + } + } + for (const [groupId, frames] of animGroupCollector) { + frames.sort((a, b) => a.frame - b.frame); + animationGroups.set( + groupId, + frames.map((f) => f.id), + ); + } + + // Track "on" variant IDs and animation frame IDs (non-first) to exclude from visible catalog const onStateIds = new Set(); for (const asset of assets.catalog) { if (asset.state === 'on') onStateIds.add(asset.id); @@ -308,33 +305,32 @@ export function buildDynamicCatalog(assets: LoadedAssetData): boolean { .sort(); const rotGroupCount = new Set(Array.from(rotationGroups.values())).size; + const animGroupCount = animationGroups.size; console.log( - `✓ Built dynamic catalog with ${allEntries.length} assets (${visibleEntries.length} visible, ${rotGroupCount} rotation groups, ${stateGroups.size / 2} state pairs)`, + `✓ Built dynamic catalog with ${allEntries.length} assets (${visibleEntries.length} visible, ${rotGroupCount} rotation groups, ${stateGroups.size / 2} state pairs, ${animGroupCount} animation groups)`, ); return true; } export function getCatalogEntry(type: string): CatalogEntryWithCategory | undefined { - // Check internal catalog first (includes all variants, e.g., non-front rotations) + // Check internal catalog (includes all variants, e.g., non-front rotations) if (internalCatalog) { return internalCatalog.find((e) => e.type === type); } - const catalog = dynamicCatalog || FURNITURE_CATALOG; - return catalog.find((e) => e.type === type); + return dynamicCatalog?.find((e) => e.type === type); } export function getCatalogByCategory(category: FurnitureCategory): CatalogEntryWithCategory[] { - const catalog = dynamicCatalog || FURNITURE_CATALOG; + const catalog = dynamicCatalog ?? []; return catalog.filter((e) => e.category === category); } export function getActiveCatalog(): CatalogEntryWithCategory[] { - return dynamicCatalog || FURNITURE_CATALOG; + return dynamicCatalog ?? []; } export function getActiveCategories(): Array<{ id: FurnitureCategory; label: string }> { - const categories = - dynamicCategories || (FURNITURE_CATEGORIES.map((c) => c.id) as FurnitureCategory[]); + const categories = dynamicCategories ?? []; return FURNITURE_CATEGORIES.filter((c) => categories.includes(c.id)); } @@ -381,3 +377,25 @@ export function getOffStateType(currentType: string): string { export function isRotatable(type: string): boolean { return rotationGroups.has(type); } + +/** Get ordered animation frame asset IDs for a given type, or null if not animated. */ +export function getAnimationFrames(type: string): string[] | null { + // Find the animation group this type belongs to + for (const [, frames] of animationGroups) { + if (frames.includes(type)) return frames; + } + return null; +} + +/** + * Get the orientation of a type within its rotation group, or undefined if not in a group. + * Used by the renderer to determine if a "left" orientation should be mirrored. + */ +export function getOrientationInGroup(type: string): string | undefined { + const group = rotationGroups.get(type); + if (!group) return undefined; + for (const [orient, id] of Object.entries(group.members)) { + if (id === type) return orient; + } + return undefined; +} diff --git a/webview-ui/src/office/layout/index.ts b/webview-ui/src/office/layout/index.ts index ccf5f780..f7c6ca99 100644 --- a/webview-ui/src/office/layout/index.ts +++ b/webview-ui/src/office/layout/index.ts @@ -1,10 +1,5 @@ export type { CatalogEntryWithCategory, FurnitureCategory } from './furnitureCatalog.js'; -export { - FURNITURE_CATALOG, - FURNITURE_CATEGORIES, - getCatalogByCategory, - getCatalogEntry, -} from './furnitureCatalog.js'; +export { FURNITURE_CATEGORIES, getCatalogByCategory, getCatalogEntry } from './furnitureCatalog.js'; export { createDefaultLayout, deserializeLayout, diff --git a/webview-ui/src/office/layout/layoutSerializer.ts b/webview-ui/src/office/layout/layoutSerializer.ts index dc04a971..80cafb24 100644 --- a/webview-ui/src/office/layout/layoutSerializer.ts +++ b/webview-ui/src/office/layout/layoutSerializer.ts @@ -7,15 +7,8 @@ import type { Seat, TileType as TileTypeVal, } from '../types.js'; -import { - DEFAULT_COLS, - DEFAULT_ROWS, - Direction, - FurnitureType, - TILE_SIZE, - TileType, -} from '../types.js'; -import { getCatalogEntry } from './furnitureCatalog.js'; +import { DEFAULT_COLS, DEFAULT_ROWS, Direction, TILE_SIZE, TileType } from '../types.js'; +import { getCatalogEntry, getOrientationInGroup } from './furnitureCatalog.js'; /** Convert flat tile array from layout into 2D grid */ export function layoutToTileMap(layout: OfficeLayout): TileTypeVal[][] { @@ -90,7 +83,16 @@ export function layoutToFurnitureInstances(furniture: PlacedFurniture[]): Furnit ); } - instances.push({ sprite, x, y, zY }); + // Determine if this instance should be mirrored (side asset used in "left" orientation) + let mirrored = false; + if (entry.mirrorSide) { + const orientInGroup = getOrientationInGroup(item.type); + if (orientInGroup === 'left') { + mirrored = true; + } + } + + instances.push({ sprite, x, y, zY, ...(mirrored ? { mirrored: true } : {}) }); } return instances; } @@ -149,6 +151,7 @@ function orientationToFacing(orientation: string): Direction { case 'left': return Direction.LEFT; case 'right': + case 'side': return Direction.RIGHT; default: return Direction.DOWN; @@ -236,48 +239,22 @@ export function getSeatTiles(seats: Map): Set { /** Default floor colors for the two rooms */ const DEFAULT_LEFT_ROOM_COLOR: FloorColor = { h: 35, s: 30, b: 15, c: 0 }; // warm beige const DEFAULT_RIGHT_ROOM_COLOR: FloorColor = { h: 25, s: 45, b: 5, c: 10 }; // warm brown -const DEFAULT_CARPET_COLOR: FloorColor = { h: 280, s: 40, b: -5, c: 0 }; // purple -const DEFAULT_DOORWAY_COLOR: FloorColor = { h: 35, s: 25, b: 10, c: 0 }; // tan -/** Create the default office layout matching the current hardcoded office */ +/** Create a minimal fallback layout (used only when no default-layout.json exists) */ export function createDefaultLayout(): OfficeLayout { const W = TileType.WALL; const F1 = TileType.FLOOR_1; const F2 = TileType.FLOOR_2; - const F3 = TileType.FLOOR_3; - const F4 = TileType.FLOOR_4; const tiles: TileTypeVal[] = []; const tileColors: Array = []; for (let r = 0; r < DEFAULT_ROWS; r++) { for (let c = 0; c < DEFAULT_COLS; c++) { - if (r === 0 || r === DEFAULT_ROWS - 1) { - tiles.push(W); - tileColors.push(null); - continue; - } - if (c === 0 || c === DEFAULT_COLS - 1) { + if (r === 0 || r === DEFAULT_ROWS - 1 || c === 0 || c === DEFAULT_COLS - 1) { tiles.push(W); tileColors.push(null); - continue; - } - if (c === 10) { - if (r >= 4 && r <= 6) { - tiles.push(F4); - tileColors.push(DEFAULT_DOORWAY_COLOR); - } else { - tiles.push(W); - tileColors.push(null); - } - continue; - } - if (c >= 15 && c <= 18 && r >= 7 && r <= 9) { - tiles.push(F3); - tileColors.push(DEFAULT_CARPET_COLOR); - continue; - } - if (c < 10) { + } else if (c < 10) { tiles.push(F1); tileColors.push(DEFAULT_LEFT_ROOM_COLOR); } else { @@ -287,27 +264,8 @@ export function createDefaultLayout(): OfficeLayout { } } - const furniture: PlacedFurniture[] = [ - { uid: 'desk-left', type: FurnitureType.DESK, col: 4, row: 3 }, - { uid: 'desk-right', type: FurnitureType.DESK, col: 13, row: 3 }, - { uid: 'bookshelf-1', type: FurnitureType.BOOKSHELF, col: 1, row: 5 }, - { uid: 'plant-left', type: FurnitureType.PLANT, col: 1, row: 1 }, - { uid: 'cooler-1', type: FurnitureType.COOLER, col: 17, row: 7 }, - { uid: 'plant-right', type: FurnitureType.PLANT, col: 18, row: 1 }, - { uid: 'whiteboard-1', type: FurnitureType.WHITEBOARD, col: 15, row: 0 }, - // Left desk chairs - { uid: 'chair-l-top', type: FurnitureType.CHAIR, col: 4, row: 2 }, - { uid: 'chair-l-bottom', type: FurnitureType.CHAIR, col: 5, row: 5 }, - { uid: 'chair-l-left', type: FurnitureType.CHAIR, col: 3, row: 4 }, - { uid: 'chair-l-right', type: FurnitureType.CHAIR, col: 6, row: 3 }, - // Right desk chairs - { uid: 'chair-r-top', type: FurnitureType.CHAIR, col: 13, row: 2 }, - { uid: 'chair-r-bottom', type: FurnitureType.CHAIR, col: 14, row: 5 }, - { uid: 'chair-r-left', type: FurnitureType.CHAIR, col: 12, row: 4 }, - { uid: 'chair-r-right', type: FurnitureType.CHAIR, col: 15, row: 3 }, - ]; - - return { version: 1, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, tiles, tileColors, furniture }; + // Minimal fallback with no furniture — the default-layout.json provides the real default + return { version: 1, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, tiles, tileColors, furniture: [] }; } /** Serialize layout to JSON string */ @@ -315,6 +273,37 @@ export function serializeLayout(layout: OfficeLayout): string { return JSON.stringify(layout); } +// ── Furniture type migration ──────────────────────────────────── + +/** Map old hardcoded FurnitureType values to new manifest-based IDs */ +const LEGACY_TYPE_MAP: Record = { + desk: 'DESK_FRONT', + chair: 'WOODEN_CHAIR_FRONT', + bookshelf: 'BOOKSHELF', + plant: 'PLANT', + cooler: null, // no equivalent in new assets — remove + whiteboard: 'WHITEBOARD', + pc: 'PC_FRONT_OFF', + lamp: null, // no equivalent in new assets — remove +}; + +/** Migrate old furniture type strings to new manifest IDs */ +function migrateFurnitureTypes(furniture: PlacedFurniture[]): PlacedFurniture[] { + const migrated: PlacedFurniture[] = []; + for (const item of furniture) { + const newType = LEGACY_TYPE_MAP[item.type]; + if (newType === undefined) { + // Not a legacy type — keep as-is + migrated.push(item); + } else if (newType !== null) { + // Migrate to new type + migrated.push({ ...item, type: newType }); + } + // newType === null → remove the item (no equivalent) + } + return migrated; +} + /** Deserialize layout from JSON string, migrating old tile types if needed */ export function deserializeLayout(json: string): OfficeLayout | null { try { @@ -338,11 +327,23 @@ export function migrateLayoutColors(layout: OfficeLayout): OfficeLayout { /** * Migrate old layouts that use legacy tile types (TILE_FLOOR=1, WOOD_FLOOR=2, CARPET=3, DOORWAY=4) - * to the new pattern-based system. If tileColors is already present, no migration needed. + * to the new pattern-based system. Also migrates old furniture type strings and old VOID value. */ function migrateLayout(layout: OfficeLayout): OfficeLayout { + // Migrate furniture types + layout = { ...layout, furniture: migrateFurnitureTypes(layout.furniture) }; + + // Migrate old VOID value (was 8, now 255) — only for legacy layouts since FLOOR_8 reuses value 8 + const OLD_VOID = 8; + if (!layout.layoutRevision && layout.tiles.includes(OLD_VOID as TileTypeVal)) { + layout = { + ...layout, + tiles: layout.tiles.map((t) => (t === OLD_VOID ? (TileType.VOID as TileTypeVal) : t)), + }; + } + if (layout.tileColors && layout.tileColors.length === layout.tiles.length) { - return layout; // Already migrated + return layout; // Already migrated tile colors } // Check if any tiles use old values (1-4) — these map directly to FLOOR_1-4 @@ -360,14 +361,14 @@ function migrateLayout(layout: OfficeLayout): OfficeLayout { tileColors.push(DEFAULT_RIGHT_ROOM_COLOR); break; case 3: // was CARPET → FLOOR_3 purple - tileColors.push(DEFAULT_CARPET_COLOR); + tileColors.push({ h: 280, s: 40, b: -5, c: 0 }); break; case 4: // was DOORWAY → FLOOR_4 tan - tileColors.push(DEFAULT_DOORWAY_COLOR); + tileColors.push({ h: 35, s: 25, b: 10, c: 0 }); break; default: - // New tile types (5-7) without colors — use neutral gray - tileColors.push(tile > 0 ? { h: 0, s: 0, b: 0, c: 0 } : null); + // Floor tile types without colors — use neutral gray + tileColors.push(tile > 0 && tile !== TileType.VOID ? { h: 0, s: 0, b: 0, c: 0 } : null); } } diff --git a/webview-ui/src/office/sprites/bubble-permission.json b/webview-ui/src/office/sprites/bubble-permission.json new file mode 100644 index 00000000..e786a37e --- /dev/null +++ b/webview-ui/src/office/sprites/bubble-permission.json @@ -0,0 +1,27 @@ +{ + "name": "bubble-permission", + "description": "Permission bubble: white square with '...' in amber, and a tail pointer (11x13)", + "width": 11, + "height": 13, + "palette": { + "_": "", + "B": "#555566", + "F": "#EEEEFF", + "A": "#CCA700" + }, + "pixels": [ + ["B","B","B","B","B","B","B","B","B","B","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","A","F","A","F","A","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","B","B","B","B","B","B","B","B","B","B"], + ["_","_","_","_","B","B","B","_","_","_","_"], + ["_","_","_","_","_","B","_","_","_","_","_"], + ["_","_","_","_","_","_","_","_","_","_","_"] + ] +} diff --git a/webview-ui/src/office/sprites/bubble-waiting.json b/webview-ui/src/office/sprites/bubble-waiting.json new file mode 100644 index 00000000..b496c662 --- /dev/null +++ b/webview-ui/src/office/sprites/bubble-waiting.json @@ -0,0 +1,27 @@ +{ + "name": "bubble-waiting", + "description": "Waiting bubble: white square with green checkmark, and a tail pointer (11x13)", + "width": 11, + "height": 13, + "palette": { + "_": "", + "B": "#555566", + "F": "#EEEEFF", + "G": "#44BB66" + }, + "pixels": [ + ["_","B","B","B","B","B","B","B","B","B","_"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","G","F","B"], + ["B","F","F","F","F","F","F","G","F","F","B"], + ["B","F","F","G","F","F","G","F","F","F","B"], + ["B","F","F","F","G","G","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["B","F","F","F","F","F","F","F","F","F","B"], + ["_","B","B","B","B","B","B","B","B","B","_"], + ["_","_","_","_","B","B","B","_","_","_","_"], + ["_","_","_","_","_","B","_","_","_","_","_"], + ["_","_","_","_","_","_","_","_","_","_","_"] + ] +} diff --git a/webview-ui/src/office/sprites/index.ts b/webview-ui/src/office/sprites/index.ts index 1f08de66..a636204e 100644 --- a/webview-ui/src/office/sprites/index.ts +++ b/webview-ui/src/office/sprites/index.ts @@ -1,13 +1,3 @@ export { getCachedSprite, getOutlineSprite } from './spriteCache.js'; export type { CharacterSprites } from './spriteData.js'; -export { - BOOKSHELF_SPRITE, - CHAIR_SPRITE, - COOLER_SPRITE, - DESK_SQUARE_SPRITE, - getCharacterSprites, - LAMP_SPRITE, - PC_SPRITE, - PLANT_SPRITE, - WHITEBOARD_SPRITE, -} from './spriteData.js'; +export { getCharacterSprites } from './spriteData.js'; diff --git a/webview-ui/src/office/sprites/spriteData.ts b/webview-ui/src/office/sprites/spriteData.ts index ed67464b..4fc5c7e0 100644 --- a/webview-ui/src/office/sprites/spriteData.ts +++ b/webview-ui/src/office/sprites/spriteData.ts @@ -1,1010 +1,48 @@ -import { adjustSprite } from '../colorize.js' -import type { Direction, FloorColor,SpriteData } from '../types.js' -import { Direction as Dir } from '../types.js' - -// ── Color Palettes ────────────────────────────────────────────── -const _ = '' // transparent - -// ── Furniture Sprites ─────────────────────────────────────────── - -/** Square desk: 32x32 pixels (2x2 tiles) — top-down wood surface */ -export const DESK_SQUARE_SPRITE: SpriteData = (() => { - const W = '#8B6914' // wood edge - const L = '#A07828' // lighter wood - const S = '#B8922E' // surface - const D = '#6B4E0A' // dark edge - const rows: string[][] = [] - // Row 0: empty - rows.push(new Array(32).fill(_)) - // Row 1: top edge - rows.push([_, ...new Array(30).fill(W), _]) - // Rows 2-5: top surface - for (let r = 0; r < 4; r++) { - rows.push([_, W, ...new Array(28).fill(r < 1 ? L : S), W, _]) - } - // Row 6: horizontal divider - rows.push([_, D, ...new Array(28).fill(W), D, _]) - // Rows 7-12: middle surface area - for (let r = 0; r < 6; r++) { - rows.push([_, W, ...new Array(28).fill(S), W, _]) - } - // Row 13: center line - rows.push([_, W, ...new Array(28).fill(L), W, _]) - // Rows 14-19: lower surface - for (let r = 0; r < 6; r++) { - rows.push([_, W, ...new Array(28).fill(S), W, _]) - } - // Row 20: horizontal divider - rows.push([_, D, ...new Array(28).fill(W), D, _]) - // Rows 21-24: bottom surface - for (let r = 0; r < 4; r++) { - rows.push([_, W, ...new Array(28).fill(r > 2 ? L : S), W, _]) - } - // Row 25: bottom edge - rows.push([_, ...new Array(30).fill(W), _]) - // Rows 26-31: legs/shadow - for (let r = 0; r < 4; r++) { - const row = new Array(32).fill(_) as string[] - row[1] = D; row[2] = D; row[29] = D; row[30] = D - rows.push(row) - } - rows.push(new Array(32).fill(_)) - rows.push(new Array(32).fill(_)) - return rows -})() - -/** Plant in pot: 16x24 */ -export const PLANT_SPRITE: SpriteData = (() => { - const G = '#3D8B37' - const D = '#2D6B27' - const T = '#6B4E0A' - const P = '#B85C3A' - const R = '#8B4422' - return [ - [_, _, _, _, _, _, G, G, _, _, _, _, _, _, _, _], - [_, _, _, _, _, G, G, G, G, _, _, _, _, _, _, _], - [_, _, _, _, G, G, D, G, G, G, _, _, _, _, _, _], - [_, _, _, G, G, D, G, G, D, G, G, _, _, _, _, _], - [_, _, G, G, G, G, G, G, G, G, G, G, _, _, _, _], - [_, G, G, D, G, G, G, G, G, G, D, G, G, _, _, _], - [_, G, G, G, G, D, G, G, D, G, G, G, G, _, _, _], - [_, _, G, G, G, G, G, G, G, G, G, G, _, _, _, _], - [_, _, _, G, G, G, D, G, G, G, G, _, _, _, _, _], - [_, _, _, _, G, G, G, G, G, G, _, _, _, _, _, _], - [_, _, _, _, _, G, G, G, G, _, _, _, _, _, _, _], - [_, _, _, _, _, _, T, T, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, T, T, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, T, T, _, _, _, _, _, _, _, _], - [_, _, _, _, _, R, R, R, R, R, _, _, _, _, _, _], - [_, _, _, _, R, P, P, P, P, P, R, _, _, _, _, _], - [_, _, _, _, R, P, P, P, P, P, R, _, _, _, _, _], - [_, _, _, _, R, P, P, P, P, P, R, _, _, _, _, _], - [_, _, _, _, R, P, P, P, P, P, R, _, _, _, _, _], - [_, _, _, _, R, P, P, P, P, P, R, _, _, _, _, _], - [_, _, _, _, R, P, P, P, P, P, R, _, _, _, _, _], - [_, _, _, _, _, R, P, P, P, R, _, _, _, _, _, _], - [_, _, _, _, _, _, R, R, R, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - ] -})() - -/** Bookshelf: 16x32 (1 tile wide, 2 tiles tall) */ -export const BOOKSHELF_SPRITE: SpriteData = (() => { - const W = '#8B6914' - const D = '#6B4E0A' - const R = '#CC4444' - const B = '#4477AA' - const G = '#44AA66' - const Y = '#CCAA33' - const P = '#9955AA' - return [ - [_, W, W, W, W, W, W, W, W, W, W, W, W, W, W, _], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, R, R, B, B, G, G, Y, Y, R, R, B, B, D, W], - [W, D, R, R, B, B, G, G, Y, Y, R, R, B, B, D, W], - [W, D, R, R, B, B, G, G, Y, Y, R, R, B, B, D, W], - [W, D, R, R, B, B, G, G, Y, Y, R, R, B, B, D, W], - [W, D, R, R, B, B, G, G, Y, Y, R, R, B, B, D, W], - [W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, P, P, Y, Y, B, B, G, G, P, P, R, R, D, W], - [W, D, P, P, Y, Y, B, B, G, G, P, P, R, R, D, W], - [W, D, P, P, Y, Y, B, B, G, G, P, P, R, R, D, W], - [W, D, P, P, Y, Y, B, B, G, G, P, P, R, R, D, W], - [W, D, P, P, Y, Y, B, B, G, G, P, P, R, R, D, W], - [W, D, P, P, Y, Y, B, B, G, G, P, P, R, R, D, W], - [W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, G, G, R, R, P, P, B, B, Y, Y, G, G, D, W], - [W, D, G, G, R, R, P, P, B, B, Y, Y, G, G, D, W], - [W, D, G, G, R, R, P, P, B, B, Y, Y, G, G, D, W], - [W, D, G, G, R, R, P, P, B, B, Y, Y, G, G, D, W], - [W, D, G, G, R, R, P, P, B, B, Y, Y, G, G, D, W], - [W, D, G, G, R, R, P, P, B, B, Y, Y, G, G, D, W], - [W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, D, D, D, D, D, D, D, D, D, D, D, D, D, D, W], - [W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W], - [_, W, W, W, W, W, W, W, W, W, W, W, W, W, W, _], - ] -})() - -/** Water cooler: 16x24 */ -export const COOLER_SPRITE: SpriteData = (() => { - const W = '#CCDDEE' - const L = '#88BBDD' - const D = '#999999' - const B = '#666666' - return [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, D, D, D, D, D, D, _, _, _, _, _], - [_, _, _, _, D, L, L, L, L, L, L, D, _, _, _, _], - [_, _, _, _, D, L, L, L, L, L, L, D, _, _, _, _], - [_, _, _, _, D, L, L, L, L, L, L, D, _, _, _, _], - [_, _, _, _, D, L, L, L, L, L, L, D, _, _, _, _], - [_, _, _, _, D, L, L, L, L, L, L, D, _, _, _, _], - [_, _, _, _, _, D, D, D, D, D, D, _, _, _, _, _], - [_, _, _, _, _, D, W, W, W, W, D, _, _, _, _, _], - [_, _, _, _, _, D, W, W, W, W, D, _, _, _, _, _], - [_, _, _, _, _, D, W, W, W, W, D, _, _, _, _, _], - [_, _, _, _, _, D, W, W, W, W, D, _, _, _, _, _], - [_, _, _, _, _, D, W, W, W, W, D, _, _, _, _, _], - [_, _, _, _, D, D, W, W, W, W, D, D, _, _, _, _], - [_, _, _, _, D, W, W, W, W, W, W, D, _, _, _, _], - [_, _, _, _, D, W, W, W, W, W, W, D, _, _, _, _], - [_, _, _, _, D, D, D, D, D, D, D, D, _, _, _, _], - [_, _, _, _, _, D, B, B, B, B, D, _, _, _, _, _], - [_, _, _, _, _, D, B, B, B, B, D, _, _, _, _, _], - [_, _, _, _, _, D, B, B, B, B, D, _, _, _, _, _], - [_, _, _, _, D, D, B, B, B, B, D, D, _, _, _, _], - [_, _, _, _, D, B, B, B, B, B, B, D, _, _, _, _], - [_, _, _, _, D, D, D, D, D, D, D, D, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - ] -})() - -/** Whiteboard: 32x16 (2 tiles wide, 1 tile tall) — hangs on wall */ -export const WHITEBOARD_SPRITE: SpriteData = (() => { - const F = '#AAAAAA' - const W = '#EEEEFF' - const M = '#CC4444' - const B = '#4477AA' - return [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, _], - [_, F, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, F, _], - [_, F, W, W, M, M, M, W, W, W, W, W, B, B, B, B, W, W, W, W, W, W, W, M, W, W, W, W, W, W, F, _], - [_, F, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, B, B, W, W, M, W, W, W, W, W, W, F, _], - [_, F, W, W, W, W, M, M, M, M, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, B, B, W, W, F, _], - [_, F, W, W, W, W, W, W, W, W, W, W, W, B, B, B, W, W, W, W, W, W, W, W, W, W, W, W, W, W, F, _], - [_, F, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, M, M, M, W, W, W, W, W, W, W, F, _], - [_, F, W, M, M, W, W, W, W, W, W, W, W, W, W, W, B, B, W, W, W, W, W, W, W, W, W, W, W, W, F, _], - [_, F, W, W, W, W, W, W, B, B, B, W, W, W, W, W, W, W, W, W, W, W, W, W, M, M, M, M, W, W, F, _], - [_, F, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, F, _], - [_, F, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, W, F, _], - [_, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, F, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - ] -})() - -/** Chair: 16x16 — top-down desk chair */ -export const CHAIR_SPRITE: SpriteData = (() => { - const W = '#8B6914' - const D = '#6B4E0A' - const B = '#5C3D0A' - const S = '#A07828' - return [ - [_, _, _, _, _, D, D, D, D, D, D, _, _, _, _, _], - [_, _, _, _, D, B, B, B, B, B, B, D, _, _, _, _], - [_, _, _, _, D, B, S, S, S, S, B, D, _, _, _, _], - [_, _, _, _, D, B, S, S, S, S, B, D, _, _, _, _], - [_, _, _, _, D, B, S, S, S, S, B, D, _, _, _, _], - [_, _, _, _, D, B, S, S, S, S, B, D, _, _, _, _], - [_, _, _, _, D, B, S, S, S, S, B, D, _, _, _, _], - [_, _, _, _, D, B, S, S, S, S, B, D, _, _, _, _], - [_, _, _, _, D, B, S, S, S, S, B, D, _, _, _, _], - [_, _, _, _, D, B, B, B, B, B, B, D, _, _, _, _], - [_, _, _, _, _, D, D, D, D, D, D, _, _, _, _, _], - [_, _, _, _, _, _, D, W, W, D, _, _, _, _, _, _], - [_, _, _, _, _, _, D, W, W, D, _, _, _, _, _, _], - [_, _, _, _, _, D, D, D, D, D, D, _, _, _, _, _], - [_, _, _, _, _, D, _, _, _, _, D, _, _, _, _, _], - [_, _, _, _, _, D, _, _, _, _, D, _, _, _, _, _], - ] -})() - -/** PC monitor: 16x16 — top-down monitor on stand */ -export const PC_SPRITE: SpriteData = (() => { - const F = '#555555' - const S = '#3A3A5C' - const B = '#6688CC' - const D = '#444444' - return [ - [_, _, _, F, F, F, F, F, F, F, F, F, F, _, _, _], - [_, _, _, F, S, S, S, S, S, S, S, S, F, _, _, _], - [_, _, _, F, S, B, B, B, B, B, B, S, F, _, _, _], - [_, _, _, F, S, B, B, B, B, B, B, S, F, _, _, _], - [_, _, _, F, S, B, B, B, B, B, B, S, F, _, _, _], - [_, _, _, F, S, B, B, B, B, B, B, S, F, _, _, _], - [_, _, _, F, S, B, B, B, B, B, B, S, F, _, _, _], - [_, _, _, F, S, B, B, B, B, B, B, S, F, _, _, _], - [_, _, _, F, S, S, S, S, S, S, S, S, F, _, _, _], - [_, _, _, F, F, F, F, F, F, F, F, F, F, _, _, _], - [_, _, _, _, _, _, _, D, D, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, D, D, _, _, _, _, _, _, _], - [_, _, _, _, _, _, D, D, D, D, _, _, _, _, _, _], - [_, _, _, _, _, D, D, D, D, D, D, _, _, _, _, _], - [_, _, _, _, _, D, D, D, D, D, D, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - ] -})() - -/** Desk lamp: 16x16 — top-down lamp with light cone */ -export const LAMP_SPRITE: SpriteData = (() => { - const Y = '#FFDD55' - const L = '#FFEE88' - const D = '#888888' - const B = '#555555' - const G = '#FFFFCC' - return [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, G, G, G, G, _, _, _, _, _, _], - [_, _, _, _, _, G, Y, Y, Y, Y, G, _, _, _, _, _], - [_, _, _, _, G, Y, Y, L, L, Y, Y, G, _, _, _, _], - [_, _, _, _, Y, Y, L, L, L, L, Y, Y, _, _, _, _], - [_, _, _, _, Y, Y, L, L, L, L, Y, Y, _, _, _, _], - [_, _, _, _, _, Y, Y, Y, Y, Y, Y, _, _, _, _, _], - [_, _, _, _, _, _, D, D, D, D, _, _, _, _, _, _], - [_, _, _, _, _, _, _, D, D, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, D, D, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, D, D, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, D, D, _, _, _, _, _, _, _], - [_, _, _, _, _, _, D, D, D, D, _, _, _, _, _, _], - [_, _, _, _, _, B, B, B, B, B, B, _, _, _, _, _], - [_, _, _, _, _, B, B, B, B, B, B, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - ] -})() +import { adjustSprite } from '../colorize.js'; +import type { Direction, FloorColor, SpriteData } from '../types.js'; +import { Direction as Dir } from '../types.js'; +import bubblePermissionData from './bubble-permission.json'; +import bubbleWaitingData from './bubble-waiting.json'; // ── Speech Bubble Sprites ─────────────────────────────────────── -/** Permission bubble: white square with "..." in amber, and a tail pointer (11x13) */ -export const BUBBLE_PERMISSION_SPRITE: SpriteData = (() => { - const B = '#555566' // border - const F = '#EEEEFF' // fill - const A = '#CCA700' // amber dots - return [ - [B, B, B, B, B, B, B, B, B, B, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, A, F, A, F, A, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, B, B, B, B, B, B, B, B, B, B], - [_, _, _, _, B, B, B, _, _, _, _], - [_, _, _, _, _, B, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _], - ] -})() - -/** Waiting bubble: white square with green checkmark, and a tail pointer (11x13) */ -export const BUBBLE_WAITING_SPRITE: SpriteData = (() => { - const B = '#555566' // border - const F = '#EEEEFF' // fill - const G = '#44BB66' // green check - return [ - [_, B, B, B, B, B, B, B, B, B, _], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, G, F, B], - [B, F, F, F, F, F, F, G, F, F, B], - [B, F, F, G, F, F, G, F, F, F, B], - [B, F, F, F, G, G, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [B, F, F, F, F, F, F, F, F, F, B], - [_, B, B, B, B, B, B, B, B, B, _], - [_, _, _, _, B, B, B, _, _, _, _], - [_, _, _, _, _, B, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _], - ] -})() - -// ── Character Sprites ─────────────────────────────────────────── -// 16x24 characters with palette substitution - -/** Palette colors for 6 distinct agent characters */ -export const CHARACTER_PALETTES = [ - { skin: '#FFCC99', shirt: '#4488CC', pants: '#334466', hair: '#553322', shoes: '#222222' }, - { skin: '#FFCC99', shirt: '#CC4444', pants: '#333333', hair: '#FFD700', shoes: '#222222' }, - { skin: '#DEB887', shirt: '#44AA66', pants: '#334444', hair: '#222222', shoes: '#333333' }, - { skin: '#FFCC99', shirt: '#AA55CC', pants: '#443355', hair: '#AA4422', shoes: '#222222' }, - { skin: '#DEB887', shirt: '#CCAA33', pants: '#444433', hair: '#553322', shoes: '#333333' }, - { skin: '#FFCC99', shirt: '#FF8844', pants: '#443322', hair: '#111111', shoes: '#222222' }, -] as const - -interface CharPalette { - skin: string - shirt: string - pants: string - hair: string - shoes: string +interface BubbleSpriteJson { + palette: Record; + pixels: string[][]; } -// Template keys for character pixel data -const H = 'hair' -const K = 'skin' -const S = 'shirt' -const P = 'pants' -const O = 'shoes' -const E = '#FFFFFF' // eyes - -type TemplateCell = typeof H | typeof K | typeof S | typeof P | typeof O | typeof E | typeof _ - -/** Resolve a template to SpriteData using a palette */ -function resolveTemplate(template: TemplateCell[][], palette: CharPalette): SpriteData { - return template.map((row) => - row.map((cell) => { - if (cell === _) return '' - if (cell === E) return E - if (cell === H) return palette.hair - if (cell === K) return palette.skin - if (cell === S) return palette.shirt - if (cell === P) return palette.pants - if (cell === O) return palette.shoes - return cell - }), - ) +function resolveBubbleSprite(data: BubbleSpriteJson): SpriteData { + return data.pixels.map((row) => row.map((key) => data.palette[key] ?? key)); } -/** Flip a template horizontally (for generating left sprites from right) */ -function flipHorizontal(template: TemplateCell[][]): TemplateCell[][] { - return template.map((row) => [...row].reverse()) -} - -// ════════════════════════════════════════════════════════════════ -// DOWN-FACING SPRITES -// ════════════════════════════════════════════════════════════════ - -// Walk down: 4 frames (1, 2=standing, 3=mirror legs, 2 again) -const CHAR_WALK_DOWN_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, E, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, P, P, _, _, _, _, P, P, _, _, _, _], - [_, _, _, _, P, P, _, _, _, _, P, P, _, _, _, _], - [_, _, _, _, O, O, _, _, _, _, _, O, O, _, _, _], - [_, _, _, _, O, O, _, _, _, _, _, O, O, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_WALK_DOWN_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, E, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_WALK_DOWN_3: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, E, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, O, O, _, _, _, _, _, _, P, P, _, _, _], - [_, _, _, O, O, _, _, _, _, _, _, P, P, _, _, _], - [_, _, _, _, _, _, _, _, _, _, O, O, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, O, O, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// Down typing: front-facing sitting, arms on keyboard -const CHAR_DOWN_TYPE_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, E, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, K, K, S, S, S, S, S, S, K, K, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_DOWN_TYPE_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, E, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, K, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, _, K, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// Down reading: front-facing sitting, arms at sides, looking at screen -const CHAR_DOWN_READ_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, E, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_DOWN_READ_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, E, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// ════════════════════════════════════════════════════════════════ -// UP-FACING SPRITES (back of head, no face) -// ════════════════════════════════════════════════════════════════ - -// Walk up: back view, legs alternate -const CHAR_WALK_UP_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, P, P, _, _, _, _, P, P, _, _, _, _], - [_, _, _, _, P, P, _, _, _, _, P, P, _, _, _, _], - [_, _, _, O, O, _, _, _, _, _, _, O, O, _, _, _], - [_, _, _, O, O, _, _, _, _, _, _, O, O, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_WALK_UP_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_WALK_UP_3: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, O, O, _, _, _, _, _, _, P, P, _, _, _], - [_, _, _, O, O, _, _, _, _, _, _, P, P, _, _, _], - [_, _, _, _, _, _, _, _, _, _, O, O, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, O, O, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// Up typing: back view, arms out to keyboard -const CHAR_UP_TYPE_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, K, K, S, S, S, S, S, S, K, K, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_UP_TYPE_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, K, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, _, K, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// Up reading: back view, arms at sides -const CHAR_UP_READ_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_UP_READ_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, _, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, H, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, K, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, S, S, S, S, S, S, S, S, _, _, _, _], - [_, _, _, _, K, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, P, P, P, P, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// ════════════════════════════════════════════════════════════════ -// RIGHT-FACING SPRITES (side profile, one eye visible) -// Left sprites are generated by flipHorizontal() -// ════════════════════════════════════════════════════════════════ - -// Right walk: side view, legs step -const CHAR_WALK_RIGHT_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, _, _, _, _, _, _], - [_, _, _, _, _, _, _, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, K, S, S, S, S, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, _, P, P, _, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, P, P, _, _, _, P, P, _, _, _, _], - [_, _, _, _, _, P, P, _, _, _, P, P, _, _, _, _], - [_, _, _, _, _, O, O, _, _, _, _, O, O, _, _, _], - [_, _, _, _, _, O, O, _, _, _, _, O, O, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_WALK_RIGHT_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, _, _, _, _, _, _], - [_, _, _, _, _, _, _, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, K, S, S, S, S, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, _, P, P, _, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, _, P, P, _, _, _, _, _], - [_, _, _, _, _, _, P, P, _, P, P, _, _, _, _, _], - [_, _, _, _, _, _, P, P, _, P, P, _, _, _, _, _], - [_, _, _, _, _, _, O, O, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, O, O, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_WALK_RIGHT_3: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, _, _, _, _, _, _], - [_, _, _, _, _, _, _, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, K, S, S, S, S, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, _, P, P, _, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, P, P, P, _, _, _, _, _], - [_, _, _, _, _, _, _, _, P, P, P, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, O, O, _, _, _, _, _], - [_, _, _, _, _, O, O, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// Right typing: side profile sitting, one arm on keyboard -const CHAR_RIGHT_TYPE_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, _, _, _, _, _, _], - [_, _, _, _, _, _, _, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, K, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, _, P, P, _, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, _, P, P, _, _, _, _, _], - [_, _, _, _, _, _, O, O, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_RIGHT_TYPE_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, _, _, _, _, _, _], - [_, _, _, _, _, _, _, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, K, _, _, _], - [_, _, _, _, _, S, S, S, S, S, _, _, K, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, _, P, P, _, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, _, P, P, _, _, _, _, _], - [_, _, _, _, _, _, O, O, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// Right reading: side sitting, arms at side -const CHAR_RIGHT_READ_1: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, _, _, _, _, _, _], - [_, _, _, _, _, _, _, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, K, S, S, S, S, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, _, P, P, _, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, _, P, P, _, _, _, _, _], - [_, _, _, _, _, _, O, O, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -const CHAR_RIGHT_READ_2: TemplateCell[][] = [ - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, H, H, H, H, H, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, E, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, K, _, _, _, _, _], - [_, _, _, _, _, _, K, K, K, K, _, _, _, _, _, _], - [_, _, _, _, _, _, _, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, S, S, S, S, S, S, _, _, _, _, _], - [_, _, _, _, _, K, S, S, S, S, K, _, _, _, _, _], - [_, _, _, _, _, _, S, S, S, S, _, _, _, _, _, _], - [_, _, _, _, _, _, _, P, P, _, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, P, P, _, _, _, _, _, _], - [_, _, _, _, _, _, P, P, _, P, P, _, _, _, _, _], - [_, _, _, _, _, _, O, O, _, O, O, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], - [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _], -] - -// ════════════════════════════════════════════════════════════════ -// Template export (for export-characters script) -// ════════════════════════════════════════════════════════════════ +/** Permission bubble: white square with "..." in amber, and a tail pointer (11x13) */ +export const BUBBLE_PERMISSION_SPRITE: SpriteData = resolveBubbleSprite(bubblePermissionData); -/** All character templates grouped by direction, for use by the export script. - * Frame order per direction: walk1, walk2, walk3, type1, type2, read1, read2 */ -export const CHARACTER_TEMPLATES = { - down: [ - CHAR_WALK_DOWN_1, CHAR_WALK_DOWN_2, CHAR_WALK_DOWN_3, - CHAR_DOWN_TYPE_1, CHAR_DOWN_TYPE_2, - CHAR_DOWN_READ_1, CHAR_DOWN_READ_2, - ], - up: [ - CHAR_WALK_UP_1, CHAR_WALK_UP_2, CHAR_WALK_UP_3, - CHAR_UP_TYPE_1, CHAR_UP_TYPE_2, - CHAR_UP_READ_1, CHAR_UP_READ_2, - ], - right: [ - CHAR_WALK_RIGHT_1, CHAR_WALK_RIGHT_2, CHAR_WALK_RIGHT_3, - CHAR_RIGHT_TYPE_1, CHAR_RIGHT_TYPE_2, - CHAR_RIGHT_READ_1, CHAR_RIGHT_READ_2, - ], -} as const +/** Waiting bubble: white square with green checkmark, and a tail pointer (11x13) */ +export const BUBBLE_WAITING_SPRITE: SpriteData = resolveBubbleSprite(bubbleWaitingData); // ════════════════════════════════════════════════════════════════ // Loaded character sprites (from PNG assets) // ════════════════════════════════════════════════════════════════ interface LoadedCharacterData { - down: SpriteData[] - up: SpriteData[] - right: SpriteData[] + down: SpriteData[]; + up: SpriteData[]; + right: SpriteData[]; } -let loadedCharacters: LoadedCharacterData[] | null = null +let loadedCharacters: LoadedCharacterData[] | null = null; /** Set pre-colored character sprites loaded from PNG assets. Call this when characterSpritesLoaded message arrives. */ export function setCharacterTemplates(data: LoadedCharacterData[]): void { - loadedCharacters = data + loadedCharacters = data; // Clear cache so sprites are rebuilt from loaded data - spriteCache.clear() + spriteCache.clear(); } /** Flip a SpriteData horizontally (for generating left sprites from right) */ -function flipSpriteHorizontal(sprite: SpriteData): SpriteData { - return sprite.map((row) => [...row].reverse()) +export function flipSpriteHorizontal(sprite: SpriteData): SpriteData { + return sprite.map((row) => [...row].reverse()); } // ════════════════════════════════════════════════════════════════ @@ -1012,21 +50,29 @@ function flipSpriteHorizontal(sprite: SpriteData): SpriteData { // ════════════════════════════════════════════════════════════════ export interface CharacterSprites { - walk: Record - typing: Record - reading: Record + walk: Record; + typing: Record; + reading: Record; } -const spriteCache = new Map() +const spriteCache = new Map(); /** Apply hue shift to every sprite in a CharacterSprites set */ function hueShiftSprites(sprites: CharacterSprites, hueShift: number): CharacterSprites { - const color: FloorColor = { h: hueShift, s: 0, b: 0, c: 0 } - const shift = (s: SpriteData) => adjustSprite(s, color) - const shiftWalk = (arr: [SpriteData, SpriteData, SpriteData, SpriteData]): [SpriteData, SpriteData, SpriteData, SpriteData] => - [shift(arr[0]), shift(arr[1]), shift(arr[2]), shift(arr[3])] - const shiftPair = (arr: [SpriteData, SpriteData]): [SpriteData, SpriteData] => - [shift(arr[0]), shift(arr[1])] + const color: FloorColor = { h: hueShift, s: 0, b: 0, c: 0 }; + const shift = (s: SpriteData) => adjustSprite(s, color); + const shiftWalk = ( + arr: [SpriteData, SpriteData, SpriteData, SpriteData], + ): [SpriteData, SpriteData, SpriteData, SpriteData] => [ + shift(arr[0]), + shift(arr[1]), + shift(arr[2]), + shift(arr[3]), + ]; + const shiftPair = (arr: [SpriteData, SpriteData]): [SpriteData, SpriteData] => [ + shift(arr[0]), + shift(arr[1]), + ]; return { walk: { [Dir.DOWN]: shiftWalk(sprites.walk[Dir.DOWN]), @@ -1046,23 +92,32 @@ function hueShiftSprites(sprites: CharacterSprites, hueShift: number): Character [Dir.RIGHT]: shiftPair(sprites.reading[Dir.RIGHT]), [Dir.LEFT]: shiftPair(sprites.reading[Dir.LEFT]), } as Record, + }; +} + +/** Create a transparent placeholder sprite of given dimensions */ +function emptySprite(w: number, h: number): SpriteData { + const rows: string[][] = []; + for (let y = 0; y < h; y++) { + rows.push(new Array(w).fill('')); } + return rows; } export function getCharacterSprites(paletteIndex: number, hueShift = 0): CharacterSprites { - const cacheKey = `${paletteIndex}:${hueShift}` - const cached = spriteCache.get(cacheKey) - if (cached) return cached + const cacheKey = `${paletteIndex}:${hueShift}`; + const cached = spriteCache.get(cacheKey); + if (cached) return cached; - let sprites: CharacterSprites + let sprites: CharacterSprites; if (loadedCharacters) { // Use pre-colored character sprites directly (no palette swapping) - const char = loadedCharacters[paletteIndex % loadedCharacters.length] - const d = char.down - const u = char.up - const rt = char.right - const flip = flipSpriteHorizontal + const char = loadedCharacters[paletteIndex % loadedCharacters.length]; + const d = char.down; + const u = char.up; + const rt = char.right; + const flip = flipSpriteHorizontal; sprites = { walk: { @@ -1083,40 +138,39 @@ export function getCharacterSprites(paletteIndex: number, hueShift = 0): Charact [Dir.RIGHT]: [rt[5], rt[6]], [Dir.LEFT]: [flip(rt[5]), flip(rt[6])], }, - } + }; } else { - // Fallback: use hardcoded templates with palette swapping - const pal = CHARACTER_PALETTES[paletteIndex % CHARACTER_PALETTES.length] - const r = (t: TemplateCell[][]) => resolveTemplate(t, pal) - const rf = (t: TemplateCell[][]) => resolveTemplate(flipHorizontal(t), pal) - + // Fallback: return transparent placeholder sprites (16×32) + const e = emptySprite(16, 32); + const walkSet: [SpriteData, SpriteData, SpriteData, SpriteData] = [e, e, e, e]; + const pairSet: [SpriteData, SpriteData] = [e, e]; sprites = { walk: { - [Dir.DOWN]: [r(CHAR_WALK_DOWN_1), r(CHAR_WALK_DOWN_2), r(CHAR_WALK_DOWN_3), r(CHAR_WALK_DOWN_2)], - [Dir.UP]: [r(CHAR_WALK_UP_1), r(CHAR_WALK_UP_2), r(CHAR_WALK_UP_3), r(CHAR_WALK_UP_2)], - [Dir.RIGHT]: [r(CHAR_WALK_RIGHT_1), r(CHAR_WALK_RIGHT_2), r(CHAR_WALK_RIGHT_3), r(CHAR_WALK_RIGHT_2)], - [Dir.LEFT]: [rf(CHAR_WALK_RIGHT_1), rf(CHAR_WALK_RIGHT_2), rf(CHAR_WALK_RIGHT_3), rf(CHAR_WALK_RIGHT_2)], + [Dir.DOWN]: walkSet, + [Dir.UP]: walkSet, + [Dir.RIGHT]: walkSet, + [Dir.LEFT]: walkSet, }, typing: { - [Dir.DOWN]: [r(CHAR_DOWN_TYPE_1), r(CHAR_DOWN_TYPE_2)], - [Dir.UP]: [r(CHAR_UP_TYPE_1), r(CHAR_UP_TYPE_2)], - [Dir.RIGHT]: [r(CHAR_RIGHT_TYPE_1), r(CHAR_RIGHT_TYPE_2)], - [Dir.LEFT]: [rf(CHAR_RIGHT_TYPE_1), rf(CHAR_RIGHT_TYPE_2)], + [Dir.DOWN]: pairSet, + [Dir.UP]: pairSet, + [Dir.RIGHT]: pairSet, + [Dir.LEFT]: pairSet, }, reading: { - [Dir.DOWN]: [r(CHAR_DOWN_READ_1), r(CHAR_DOWN_READ_2)], - [Dir.UP]: [r(CHAR_UP_READ_1), r(CHAR_UP_READ_2)], - [Dir.RIGHT]: [r(CHAR_RIGHT_READ_1), r(CHAR_RIGHT_READ_2)], - [Dir.LEFT]: [rf(CHAR_RIGHT_READ_1), rf(CHAR_RIGHT_READ_2)], + [Dir.DOWN]: pairSet, + [Dir.UP]: pairSet, + [Dir.RIGHT]: pairSet, + [Dir.LEFT]: pairSet, }, - } + }; } // Apply hue shift if non-zero if (hueShift !== 0) { - sprites = hueShiftSprites(sprites, hueShift) + sprites = hueShiftSprites(sprites, hueShift); } - spriteCache.set(cacheKey, sprites) - return sprites + spriteCache.set(cacheKey, sprites); + return sprites; } diff --git a/webview-ui/src/office/types.ts b/webview-ui/src/office/types.ts index 814f616c..4a8e4692 100644 --- a/webview-ui/src/office/types.ts +++ b/webview-ui/src/office/types.ts @@ -16,7 +16,9 @@ export const TileType = { FLOOR_5: 5, FLOOR_6: 6, FLOOR_7: 7, - VOID: 8, + FLOOR_8: 8, + FLOOR_9: 9, + VOID: 255, } as const; export type TileType = (typeof TileType)[keyof typeof TileType]; @@ -49,7 +51,7 @@ export const Direction = { } as const; export type Direction = (typeof Direction)[keyof typeof Direction]; -/** 2D array of hex color strings (or '' for transparent). [row][col] */ +/** 2D array of hex color strings: '' = transparent, '#RRGGBB' = opaque, '#RRGGBBAA' = semi-transparent. [row][col] */ export type SpriteData = string[][]; export interface Seat { @@ -72,6 +74,8 @@ export interface FurnitureInstance { y: number; /** Y value used for depth sorting (typically bottom edge) */ zY: number; + /** Render-time horizontal flip flag (for mirrored side variants) */ + mirrored?: boolean; } export interface ToolActivity { @@ -81,19 +85,6 @@ export interface ToolActivity { permissionWait?: boolean; } -export const FurnitureType = { - // Original hand-drawn sprites (kept for backward compat) - DESK: 'desk', - BOOKSHELF: 'bookshelf', - PLANT: 'plant', - COOLER: 'cooler', - WHITEBOARD: 'whiteboard', - CHAIR: 'chair', - PC: 'pc', - LAMP: 'lamp', -} as const; -export type FurnitureType = (typeof FurnitureType)[keyof typeof FurnitureType]; - export const EditTool = { TILE_PAINT: 'tile_paint', WALL_PAINT: 'wall_paint', @@ -106,7 +97,7 @@ export const EditTool = { export type EditTool = (typeof EditTool)[keyof typeof EditTool]; export interface FurnitureCatalogEntry { - type: string; // FurnitureType enum or asset ID + type: string; // asset ID from furniture manifest label: string; footprintW: number; footprintH: number; @@ -121,11 +112,13 @@ export interface FurnitureCatalogEntry { backgroundTiles?: number; /** Whether this item can be placed on wall tiles */ canPlaceOnWalls?: boolean; + /** Whether this is a side-oriented asset that produces a mirrored "left" variant */ + mirrorSide?: boolean; } export interface PlacedFurniture { uid: string; - type: string; // FurnitureType enum or asset ID + type: string; // asset ID from furniture manifest col: number; row: number; /** Optional color override for furniture */ @@ -140,6 +133,8 @@ export interface OfficeLayout { furniture: PlacedFurniture[]; /** Per-tile color settings, parallel to tiles array. null = wall/no color */ tileColors?: Array; + /** Bumped when the bundled default layout changes; forces a reset on existing installs */ + layoutRevision?: number; } export interface Character { diff --git a/webview-ui/src/office/wallTiles.ts b/webview-ui/src/office/wallTiles.ts index 12082096..780bf6ea 100644 --- a/webview-ui/src/office/wallTiles.ts +++ b/webview-ui/src/office/wallTiles.ts @@ -1,7 +1,8 @@ /** * Wall tile auto-tiling: sprite storage and bitmask-based piece selection. * - * Stores 16 wall sprites (one per 4-bit bitmask) loaded from walls.png. + * Stores wall tile sets loaded from individual PNGs in assets/walls/. + * Each set contains 16 wall sprites (one per 4-bit bitmask). * At render time, each wall tile's 4 cardinal neighbors are checked to build * a bitmask, and the corresponding sprite is drawn directly. * No changes to the layout model — auto-tiling is purely visual. @@ -18,41 +19,61 @@ import type { } from './types.js'; import { TILE_SIZE, TileType } from './types.js'; -/** 16 wall sprites indexed by bitmask (0-15) */ -let wallSprites: SpriteData[] | null = null; +/** Wall tile sets: each set has 16 sprites indexed by bitmask (0-15) */ +let wallSets: SpriteData[][] = []; -/** Set wall sprites (called once when extension sends wallTilesLoaded) */ -export function setWallSprites(sprites: SpriteData[]): void { - wallSprites = sprites; +/** Set wall tile sets (called once when extension sends wallTilesLoaded) */ +export function setWallSprites(sets: SpriteData[][]): void { + wallSets = sets; } /** Check if wall sprites have been loaded */ export function hasWallSprites(): boolean { - return wallSprites !== null; + return wallSets.length > 0; +} + +/** Get number of available wall sets */ +export function getWallSetCount(): number { + return wallSets.length; +} + +/** Get the first sprite (bitmask 0, top-left piece) of a wall set for preview rendering */ +export function getWallSetPreviewSprite(setIndex: number): SpriteData | null { + const set = wallSets[setIndex]; + if (!set) return null; + return set[0] ?? null; } /** - * Get the wall sprite for a tile based on its cardinal neighbors. - * Returns the sprite + Y offset, or null to fall back to solid WALL_COLOR. + * Build the 4-bit neighbor bitmask for a wall tile at (col, row). */ -export function getWallSprite( - col: number, - row: number, - tileMap: TileTypeVal[][], -): { sprite: SpriteData; offsetY: number } | null { - if (!wallSprites) return null; - +function buildWallMask(col: number, row: number, tileMap: TileTypeVal[][]): number { const tmRows = tileMap.length; const tmCols = tmRows > 0 ? tileMap[0].length : 0; - // Build 4-bit neighbor bitmask let mask = 0; if (row > 0 && tileMap[row - 1][col] === TileType.WALL) mask |= 1; // N if (col < tmCols - 1 && tileMap[row][col + 1] === TileType.WALL) mask |= 2; // E if (row < tmRows - 1 && tileMap[row + 1][col] === TileType.WALL) mask |= 4; // S if (col > 0 && tileMap[row][col - 1] === TileType.WALL) mask |= 8; // W + return mask; +} + +/** + * Get the wall sprite for a tile based on its cardinal neighbors. + * Returns the sprite + Y offset, or null to fall back to solid WALL_COLOR. + */ +export function getWallSprite( + col: number, + row: number, + tileMap: TileTypeVal[][], + setIndex = 0, +): { sprite: SpriteData; offsetY: number } | null { + if (wallSets.length === 0) return null; + const sprites = wallSets[setIndex] ?? wallSets[0]; - const sprite = wallSprites[mask]; + const mask = buildWallMask(col, row, tileMap); + const sprite = sprites[mask]; if (!sprite) return null; // Anchor sprite at bottom of tile — tall sprites extend upward @@ -69,23 +90,16 @@ export function getColorizedWallSprite( row: number, tileMap: TileTypeVal[][], color: FloorColor, + setIndex = 0, ): { sprite: SpriteData; offsetY: number } | null { - if (!wallSprites) return null; - - const tmRows = tileMap.length; - const tmCols = tmRows > 0 ? tileMap[0].length : 0; - - // Build 4-bit neighbor bitmask (same as getWallSprite) - let mask = 0; - if (row > 0 && tileMap[row - 1][col] === TileType.WALL) mask |= 1; // N - if (col < tmCols - 1 && tileMap[row][col + 1] === TileType.WALL) mask |= 2; // E - if (row < tmRows - 1 && tileMap[row + 1][col] === TileType.WALL) mask |= 4; // S - if (col > 0 && tileMap[row][col - 1] === TileType.WALL) mask |= 8; // W + if (wallSets.length === 0) return null; + const sprites = wallSets[setIndex] ?? wallSets[0]; - const sprite = wallSprites[mask]; + const mask = buildWallMask(col, row, tileMap); + const sprite = sprites[mask]; if (!sprite) return null; - const cacheKey = `wall-${mask}-${color.h}-${color.s}-${color.b}-${color.c}`; + const cacheKey = `wall-${setIndex}-${mask}-${color.h}-${color.s}-${color.b}-${color.c}`; const colorized = getColorizedSprite(cacheKey, sprite, { ...color, colorize: true }); return { sprite: colorized, offsetY: TILE_SIZE - sprite.length }; @@ -100,7 +114,7 @@ export function getWallInstances( tileColors?: Array, cols?: number, ): FurnitureInstance[] { - if (!wallSprites) return []; + if (wallSets.length === 0) return []; const tmRows = tileMap.length; const tmCols = tmRows > 0 ? tileMap[0].length : 0; const layoutCols = cols ?? tmCols; From 8797b6afaab481843c222b39a0a09b306b7524ad Mon Sep 17 00:00:00 2001 From: Florin Timbuc Date: Fri, 13 Mar 2026 23:18:09 +0200 Subject: [PATCH 2/6] ci: add CI workflow, dependabot, and ESLint contributor rules (#116) * ci: add CI workflow, dependabot, and ESLint contributor rules * Update CONTRIBUTING * ci: add CI workflow, dependabot, and ESLint contributor rules * Update CONTRIBUTING --- .github/dependabot.yml | 32 ++++++ .github/workflows/ci.yml | 172 ++++++++++++++++++++++++++++ .vscode/settings.json | 2 +- .vscode/tasks.json | 12 ++ CONTRIBUTING.md | 27 +++-- README.md | 2 +- eslint-rules/pixel-agents-rules.mjs | 142 +++++++++++++++++++++++ eslint.config.mjs | 9 ++ webview-ui/eslint.config.js | 25 +++- 9 files changed, 409 insertions(+), 14 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 eslint-rules/pixel-agents-rules.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..f6fd4ed0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,32 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 10 + groups: + minor-and-patch: + update-types: + - minor + - patch + + - package-ecosystem: npm + directory: /webview-ui + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 10 + groups: + minor-and-patch: + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..165cb3e4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,172 @@ +# CI workflow for pixel-agents + +name: CI + +on: + pull_request: + paths-ignore: + - '**.md' + - 'LICENSE' + - '.github/FUNDING.yml' + push: + branches: + - main + paths-ignore: + - '**.md' + - 'LICENSE' + - '.github/FUNDING.yml' + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + ci: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node + id: setup_node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webview-ui/package-lock.json + + - name: Install Root Dependencies + id: install_root + run: npm ci + + - name: Install Webview Dependencies + id: install_webview + working-directory: webview-ui + run: npm ci + + # --- Quality Checks (blocking) --- + + - name: Type Check + id: type_check + if: always() && steps.install_root.outcome == 'success' + run: npm run check-types + continue-on-error: true + + - name: Root Lint + id: root_lint + if: always() && steps.install_root.outcome == 'success' + run: npm run lint + continue-on-error: true + + - name: Webview Lint + id: webview_lint + if: always() && steps.install_webview.outcome == 'success' + working-directory: webview-ui + run: npm run lint + continue-on-error: true + + - name: Format Check + id: format_check + if: always() && steps.install_root.outcome == 'success' + run: npm run format:check + continue-on-error: true + + # --- Build (blocking) --- + + - name: Build + id: build + if: always() && steps.install_root.outcome == 'success' && steps.install_webview.outcome == 'success' + run: | + node esbuild.js + cd webview-ui && npm run build + continue-on-error: true + + # --- Advisory Checks (non-blocking) --- + + - name: Audit Root Dependencies + id: audit_root + if: always() && steps.install_root.outcome == 'success' + run: npm audit --audit-level=high + continue-on-error: true + + - name: Audit Webview Dependencies + id: audit_webview + if: always() && steps.install_webview.outcome == 'success' + working-directory: webview-ui + run: npm audit --audit-level=high + continue-on-error: true + + # --- Summary --- + + - name: Write Step Summary + if: always() + env: + CHECKOUT: ${{ steps.checkout.outcome }} + SETUP_NODE: ${{ steps.setup_node.outcome }} + INSTALL_ROOT: ${{ steps.install_root.outcome }} + INSTALL_WEBVIEW: ${{ steps.install_webview.outcome }} + TYPE_CHECK: ${{ steps.type_check.outcome }} + ROOT_LINT: ${{ steps.root_lint.outcome }} + WEBVIEW_LINT: ${{ steps.webview_lint.outcome }} + FORMAT_CHECK: ${{ steps.format_check.outcome }} + BUILD: ${{ steps.build.outcome }} + AUDIT_ROOT: ${{ steps.audit_root.outcome }} + AUDIT_WEBVIEW: ${{ steps.audit_webview.outcome }} + run: | + status() { + if [ "$1" = "success" ]; then echo "✅ PASS"; else echo "❌ FAIL"; fi + } + { + echo "## CI Results" + echo + echo "| Check | Result |" + echo "| --- | --- |" + echo "| Checkout | $(status "$CHECKOUT") |" + echo "| Setup Node | $(status "$SETUP_NODE") |" + echo "| Install root deps | $(status "$INSTALL_ROOT") |" + echo "| Install webview deps | $(status "$INSTALL_WEBVIEW") |" + echo "| **Type check** | $(status "$TYPE_CHECK") |" + echo "| **Root lint** | $(status "$ROOT_LINT") |" + echo "| **Webview lint** | $(status "$WEBVIEW_LINT") |" + echo "| **Format check** | $(status "$FORMAT_CHECK") |" + echo "| **Build** | $(status "$BUILD") |" + echo "| Audit root _(advisory)_ | $(status "$AUDIT_ROOT") |" + echo "| Audit webview _(advisory)_ | $(status "$AUDIT_WEBVIEW") |" + } >> "$GITHUB_STEP_SUMMARY" + + # --- Final Gate --- + + - name: Fail If Any Blocking Check Failed + if: always() + env: + CHECKOUT: ${{ steps.checkout.outcome }} + SETUP_NODE: ${{ steps.setup_node.outcome }} + INSTALL_ROOT: ${{ steps.install_root.outcome }} + INSTALL_WEBVIEW: ${{ steps.install_webview.outcome }} + TYPE_CHECK: ${{ steps.type_check.outcome }} + ROOT_LINT: ${{ steps.root_lint.outcome }} + WEBVIEW_LINT: ${{ steps.webview_lint.outcome }} + FORMAT_CHECK: ${{ steps.format_check.outcome }} + BUILD: ${{ steps.build.outcome }} + run: | + failed=0 + for step in CHECKOUT SETUP_NODE INSTALL_ROOT INSTALL_WEBVIEW \ + TYPE_CHECK ROOT_LINT WEBVIEW_LINT FORMAT_CHECK \ + BUILD; do + eval "val=\$$step" + if [ "$val" != "success" ]; then + echo "::error::$step failed" + failed=1 + fi + done + exit "$failed" diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b55c503..82e9b027 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,7 +9,7 @@ "dist": true // set this to false to include "dist" folder in search results }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", + "#js/ts.tsc.autoDetect#": "off", "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, "[json]": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0d3370a7..7f76e553 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -55,6 +55,18 @@ "group": "watch", "reveal": "never" } + }, + { + "label": "Run CI (act)", + "type": "shell", + "command": "act push -j ci --container-architecture linux/amd64", + "group": "test", + "presentation": { + "reveal": "always", + "panel": "dedicated", + "clear": true + }, + "problemMatcher": [] } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d01efc2..e9301128 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,8 @@ This project is licensed under the [MIT License](LICENSE), so your contributions ### Prerequisites -- [Node.js](https://nodejs.org/) (LTS recommended) -- [VS Code](https://code.visualstudio.com/) (v1.109.0 or later) +- [Node.js](https://nodejs.org/) (v22 recommended) +- [VS Code](https://code.visualstudio.com/) (v1.107.0 or later) ### Setup @@ -45,9 +45,10 @@ This starts parallel watchers for both the extension backend (esbuild) and TypeS | `assets/` | Bundled sprites, catalog, and default layout | ## Code Guidelines + ### Constants -**No unused locals or parameters** (`noUnusedLocals` and `noUnusedParameters` are enabled): All magic numbers and strings are centralized — don't add inline constants to source files: +**No unused locals or parameters** (`noUnusedLocals` and `noUnusedParameters` are enabled). All magic numbers and strings are centralized — don't add inline constants to source files: - **Extension backend:** `src/constants.ts` - **Webview:** `webview-ui/src/constants.ts` @@ -59,18 +60,30 @@ The project uses a pixel art aesthetic. All overlays should use: - Sharp corners (`border-radius: 0`) - Solid backgrounds and `2px solid` borders -- Hard offset shadows (`2px 2px 0px`, no blur) +- Hard offset shadows (`2px 2px 0px`, no blur) — use `var(--pixel-shadow)` - The FS Pixel Sans font (loaded in `index.css`) +These conventions are enforced by custom ESLint rules (`eslint-rules/pixel-agents-rules.mjs`): + +| Rule | Scope | What it checks | +|---|---|---| +| `no-inline-colors` | Extension + Webview | No hex/rgb/rgba/hsl/hsla literals outside `constants.ts` | +| `pixel-shadow` | Webview only | Box shadows must use `var(--pixel-shadow)` or `2px 2px 0px` | +| `pixel-font` | Webview only | Font family must reference FS Pixel Sans | + +These rules are set to `warn` — they won't block your PR but will flag violations for cleanup. + ## Submitting a Pull Request 1. Fork the repo and create a feature branch from `main` 2. Make your changes -3. Run the full build to verify everything passes: +3. Verify everything passes locally: ```bash - npm run build + npm run lint # Extension lint + cd webview-ui && npm run lint && cd .. # Webview lint + npm run build # Type check + esbuild + Vite ``` - This runs type-checking, linting, esbuild (extension), and Vite (webview). + CI runs these same checks automatically on every PR. 4. Open a pull request against `main` with: - A clear description of what changed and why - How you tested the changes (steps to reproduce / verify) diff --git a/README.md b/README.md index 9f64a9a5..178da42f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ This is the source code for the free [Pixel Agents extension for VS Code](https: ## Requirements -- VS Code 1.109.0 or later +- VS Code 1.107.0 or later - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured ## Getting Started diff --git a/eslint-rules/pixel-agents-rules.mjs b/eslint-rules/pixel-agents-rules.mjs new file mode 100644 index 00000000..9b97e98a --- /dev/null +++ b/eslint-rules/pixel-agents-rules.mjs @@ -0,0 +1,142 @@ +/** + * Shared ESLint plugin for pixel-agents project conventions. + * + * Rules: + * no-inline-colors — flag hex/rgb/rgba/hsl/hsla color literals (centralize in constants) + * pixel-shadow — flag box-shadow values not using var(--pixel-shadow) or 2px 2px 0px + * pixel-font — flag font-family values not referencing FS Pixel Sans + */ + +const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/; +const RGB_FUNC = /\brgba?\s*\(/; +const HSL_FUNC = /\bhsla?\s*\(/; +const COLOR_PATTERNS = [HEX_COLOR, RGB_FUNC, HSL_FUNC]; + +/** Check whether a raw string value contains a color literal. */ +function containsColor(value) { + return COLOR_PATTERNS.some((p) => p.test(value)); +} + +/** Check whether the node is inside a comment-like context (template literal tag, etc.) */ +function isCommentOnly(value) { + const trimmed = value.trim(); + return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'); +} + +const noInlineColors = { + meta: { + type: 'suggestion', + docs: { + description: + 'Disallow inline color literals (hex, rgb, rgba, hsl, hsla). Use shared constants or --pixel-* CSS tokens.', + }, + schema: [], + messages: { + found: 'Use shared constants or `--pixel-*` tokens instead of inline color literals.', + }, + }, + create(context) { + return { + Literal(node) { + if (typeof node.value !== 'string') return; + if (isCommentOnly(node.value)) return; + if (containsColor(node.value)) { + context.report({ node, messageId: 'found' }); + } + }, + TemplateLiteral(node) { + for (const quasi of node.quasis) { + if (containsColor(quasi.value.raw)) { + context.report({ node: quasi, messageId: 'found' }); + } + } + }, + }; + }, +}; + +/** + * Helper: check if an AST Property node has a key matching `boxShadow` or `box-shadow`. + */ +function isBoxShadowProperty(node) { + if (node.type !== 'Property') return false; + const key = node.key; + if (key.type === 'Identifier' && key.name === 'boxShadow') return true; + if (key.type === 'Literal' && key.value === 'box-shadow') return true; + return false; +} + +const pixelShadow = { + meta: { + type: 'suggestion', + docs: { + description: + 'Require box-shadow values to use var(--pixel-shadow) or the 2px 2px 0px pattern.', + }, + schema: [], + messages: { + found: 'Use `var(--pixel-shadow)` or a hard offset `2px 2px 0px` shadow.', + }, + }, + create(context) { + return { + Property(node) { + if (!isBoxShadowProperty(node)) return; + const value = node.value; + if (value.type !== 'Literal' || typeof value.value !== 'string') return; + const text = value.value; + if (text.includes('var(--pixel-shadow)') || text.includes('2px 2px 0px')) return; + context.report({ node: value, messageId: 'found' }); + }, + }; + }, +}; + +/** + * Helper: check if an AST Property node has a key matching `fontFamily` or `font-family`. + */ +function isFontFamilyProperty(node) { + if (node.type !== 'Property') return false; + const key = node.key; + if (key.type === 'Identifier' && key.name === 'fontFamily') return true; + if (key.type === 'Literal' && key.value === 'font-family') return true; + return false; +} + +const pixelFont = { + meta: { + type: 'suggestion', + docs: { + description: 'Require font-family values to reference FS Pixel Sans.', + }, + schema: [], + messages: { + found: 'Use the FS Pixel Sans font for UI styling.', + }, + }, + create(context) { + return { + Property(node) { + if (!isFontFamilyProperty(node)) return; + const value = node.value; + if (value.type !== 'Literal' || typeof value.value !== 'string') return; + if (value.value.includes('FS Pixel Sans')) return; + context.report({ node: value, messageId: 'found' }); + }, + }; + }, +}; + +const plugin = { + meta: { + name: 'eslint-plugin-pixel-agents', + version: '1.0.0', + }, + rules: { + 'no-inline-colors': noInlineColors, + 'pixel-shadow': pixelShadow, + 'pixel-font': pixelFont, + }, +}; + +export default plugin; diff --git a/eslint.config.mjs b/eslint.config.mjs index d4005d81..9206cb56 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,7 @@ import typescriptEslint from 'typescript-eslint'; import eslintConfigPrettier from 'eslint-config-prettier'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; +import pixelAgentsPlugin from './eslint-rules/pixel-agents-rules.mjs'; export default [ { @@ -10,6 +11,7 @@ export default [ plugins: { '@typescript-eslint': typescriptEslint.plugin, 'simple-import-sort': simpleImportSort, + 'pixel-agents': pixelAgentsPlugin, }, languageOptions: { @@ -32,6 +34,13 @@ export default [ 'no-throw-literal': 'warn', 'simple-import-sort/imports': 'warn', 'simple-import-sort/exports': 'warn', + 'pixel-agents/no-inline-colors': 'warn', + }, + }, + { + files: ['src/constants.ts'], + rules: { + 'pixel-agents/no-inline-colors': 'off', }, }, eslintConfigPrettier, diff --git a/webview-ui/eslint.config.js b/webview-ui/eslint.config.js index d757f9bf..231a349c 100644 --- a/webview-ui/eslint.config.js +++ b/webview-ui/eslint.config.js @@ -6,6 +6,7 @@ import simpleImportSort from 'eslint-plugin-simple-import-sort'; import tseslint from 'typescript-eslint'; import eslintConfigPrettier from 'eslint-config-prettier'; import { defineConfig, globalIgnores } from 'eslint/config'; +import pixelAgentsPlugin from '../eslint-rules/pixel-agents-rules.mjs'; export default defineConfig([ globalIgnores(['dist']), @@ -19,6 +20,7 @@ export default defineConfig([ ], plugins: { 'simple-import-sort': simpleImportSort, + 'pixel-agents': pixelAgentsPlugin, }, languageOptions: { ecmaVersion: 2020, @@ -27,11 +29,24 @@ export default defineConfig([ rules: { 'simple-import-sort/imports': 'warn', 'simple-import-sort/exports': 'warn', - // TODO: Fix these and restore to 'error' — imperative OfficeState/EditorState - // mutations, ref access during render, and setState-in-effect need refactoring - 'react-hooks/refs': 'warn', - 'react-hooks/immutability': 'warn', - 'react-hooks/set-state-in-effect': 'warn', + // These react-hooks rules misfire on this project's imperative game-state patterns: + // - immutability: singleton OfficeState/EditorState mutations are by design + // - refs: containerRef reads during render feed canvas pipeline, not React state + // - set-state-in-effect: timer-based animations and async error handling are legitimate + 'react-hooks/immutability': 'off', + 'react-hooks/refs': 'off', + 'react-hooks/set-state-in-effect': 'off', + 'pixel-agents/no-inline-colors': 'warn', + 'pixel-agents/pixel-shadow': 'warn', + 'pixel-agents/pixel-font': 'warn', + }, + }, + { + files: ['src/constants.ts', 'src/fonts/**', 'src/office/sprites/**'], + rules: { + 'pixel-agents/no-inline-colors': 'off', + 'pixel-agents/pixel-shadow': 'off', + 'pixel-agents/pixel-font': 'off', }, }, eslintConfigPrettier, From 49b8cde9d286e090401e10d0442de8f58b5d62af Mon Sep 17 00:00:00 2001 From: Pablo De Lucca Date: Fri, 13 Mar 2026 21:34:59 +0000 Subject: [PATCH 3/6] fix: recognize 'Agent' tool name for sub-agent visualization Claude Code renamed the sub-agent tool from 'Task' to 'Agent'. Add 'Agent' alongside 'Task' in permission exemption, status formatting, tool completion, and progress handling so sub-agents work with current Claude Code versions. Based on drewf/pixel-agents#76. Co-Authored-By: Claude Opus 4.6 --- src/transcriptParser.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/transcriptParser.ts b/src/transcriptParser.ts index ecddd37f..f462967a 100644 --- a/src/transcriptParser.ts +++ b/src/transcriptParser.ts @@ -16,7 +16,7 @@ import { } from './timerManager.js'; import type { AgentState } from './types.js'; -export const PERMISSION_EXEMPT_TOOLS = new Set(['Task', 'AskUserQuestion']); +export const PERMISSION_EXEMPT_TOOLS = new Set(['Task', 'Agent', 'AskUserQuestion']); export function formatToolStatus(toolName: string, input: Record): string { const base = (p: unknown) => (typeof p === 'string' ? path.basename(p) : ''); @@ -39,7 +39,8 @@ export function formatToolStatus(toolName: string, input: Record TASK_DESCRIPTION_DISPLAY_MAX_LENGTH ? desc.slice(0, TASK_DESCRIPTION_DISPLAY_MAX_LENGTH) + '\u2026' : desc}` @@ -125,8 +126,9 @@ export function processTranscriptLine( if (block.type === 'tool_result' && block.tool_use_id) { console.log(`[Pixel Agents] Agent ${agentId} tool done: ${block.tool_use_id}`); const completedToolId = block.tool_use_id; - // If the completed tool was a Task, clear its subagent tools - if (agent.activeToolNames.get(completedToolId) === 'Task') { + // If the completed tool was a Task/Agent, clear its subagent tools + const completedToolName = agent.activeToolNames.get(completedToolId); + if (completedToolName === 'Task' || completedToolName === 'Agent') { agent.activeSubagentToolIds.delete(completedToolId); agent.activeSubagentToolNames.delete(completedToolId); webview?.postMessage({ @@ -220,8 +222,9 @@ function processProgressRecord( return; } - // Verify parent is an active Task tool (agent_progress handling) - if (agent.activeToolNames.get(parentToolId) !== 'Task') return; + // Verify parent is an active Task/Agent tool (agent_progress handling) + const parentToolName = agent.activeToolNames.get(parentToolId); + if (parentToolName !== 'Task' && parentToolName !== 'Agent') return; const msg = data.message as Record | undefined; if (!msg) return; From 706dc4930134751b03dbb572c6fa406b7d7dcf61 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 14 Mar 2026 09:26:08 -0400 Subject: [PATCH 4/6] Add dual-publish workflow for VS Code Marketplace + Open VSX (#44) * Add Open VSX publishing workflow. Automate extension releases to both VS Code Marketplace and Open VSX, and document the required repository secrets for maintainers. Made-with: Cursor * Apply suggestions from code review Co-authored-by: Florin Timbuc * Harden publish workflow: explicit dry-run, early validation, caching, VSIX upload - Add cache-dependency-path for root + webview lockfiles - Add timeout-minutes: 15 to prevent hung jobs - Add workflow_dispatch dry_run input for safe testing - Add concurrency control to prevent duplicate publishes - Add early type-check and lint step (fail fast before publish) - Upload VSIX to GitHub Release as downloadable artifact --------- Co-authored-by: Florin Timbuc Co-authored-by: Florin Timbuc --- .github/workflows/publish-extension.yml | 66 +++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish-extension.yml diff --git a/.github/workflows/publish-extension.yml b/.github/workflows/publish-extension.yml new file mode 100644 index 00000000..f7ecfd1a --- /dev/null +++ b/.github/workflows/publish-extension.yml @@ -0,0 +1,66 @@ +name: Publish Extension + +on: + release: + types: [published] + workflow_dispatch: + inputs: + dry_run: + description: 'Package without publishing' + type: boolean + default: false + +concurrency: + group: publish-extension + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + webview-ui/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Install webview dependencies + run: npm ci + working-directory: webview-ui + + - name: Check types and lint + run: npm run check-types && npm run lint + + - name: Publish to VS Code Marketplace + id: publish-vscode + uses: HaaLeo/publish-vscode-extension@v2 + with: + pat: ${{ secrets.VSCE_PAT }} + registryUrl: https://marketplace.visualstudio.com + dryRun: ${{ inputs.dry_run == 'true' }} + + - name: Publish to Open VSX + uses: HaaLeo/publish-vscode-extension@v2 + with: + pat: ${{ secrets.OPEN_VSX_TOKEN }} + registryUrl: https://open-vsx.org + extensionFile: ${{ steps.publish-vscode.outputs.vsixPath }} + dryRun: ${{ inputs.dry_run == 'true' }} + + - name: Upload VSIX to Release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.publish-vscode.outputs.vsixPath }} diff --git a/README.md b/README.md index 178da42f..38184c29 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Pixel Agents turns multi-agent AI systems into something you can actually see an Right now it works as a VS Code extension with Claude Code. The vision though, is a fully agent-agnostic, platform-agnostic interface for orchestrating any AI agents, deployable anywhere. -This is the source code for the free [Pixel Agents extension for VS Code](https://marketplace.visualstudio.com/items?itemName=pablodelucca.pixel-agents) — you can install it directly from the marketplace with the full furniture catalog included. +This is the source code for the free Pixel Agents extension for VS Code — install from the [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=pablodelucca.pixel-agents) or [Open VSX](https://open-vsx.org/extension/pablodelucca/pixel-agents) with the full furniture catalog included. ![Pixel Agents screenshot](webview-ui/public/Screenshot.jpg) From c0676a904c9431755f8d90fd0f375902c5f9596a Mon Sep 17 00:00:00 2001 From: Pablo De Lucca Date: Sat, 14 Mar 2026 13:27:04 +0000 Subject: [PATCH 5/6] chore: lower VS Code requirement to ^1.105.0 and add v1.1.0 changelog Broaden compatibility with older VS Code versions and forks (Cursor, Windsurf, VSCodium, etc.). Add changelog entry for v1.1.0 release. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 22 ++++++++++++++++++++++ CONTRIBUTING.md | 2 +- README.md | 2 +- package-lock.json | 4 ++-- package.json | 4 ++-- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e659c94..1d2521fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## v1.1.0 + +### Features + +- **Migrate to open-source assets with modular manifest-based loading** ([#117](https://github.com/pablodelucca/pixel-agents/pull/117)) — Replaces bundled proprietary tileset with open-source assets loaded via a manifest system, enabling community contributions and modding. +- **Recognize 'Agent' tool name for sub-agent visualization** ([#76](https://github.com/pablodelucca/pixel-agents/pull/76)) — Claude Code renamed the sub-agent tool from 'Task' to 'Agent'; sub-agent characters now spawn correctly with current Claude Code versions. +- **Dual-publish workflow for VS Code Marketplace + Open VSX** ([#44](https://github.com/pablodelucca/pixel-agents/pull/44)) — Automates extension releases to both VS Code Marketplace and Open VSX via GitHub Actions. + +### Maintenance + +- **Add linting, formatting, and repo infrastructure** ([#82](https://github.com/pablodelucca/pixel-agents/pull/82)) — ESLint, Prettier, Husky pre-commit hooks, and lint-staged for consistent code quality. +- **Add CI workflow, Dependabot, and ESLint contributor rules** ([#116](https://github.com/pablodelucca/pixel-agents/pull/116)) — Continuous integration, automated dependency updates, and shared linting configuration. +- **Lower VS Code engine requirement to ^1.105.0** — Broadens compatibility with older VS Code versions and forks (Cursor, Antigravity, Windsurf, VSCodium, Kiro, TRAE, Positron, etc.). + +### Contributors + +Thank you to the contributors who made this release possible: + +- [@drewf](https://github.com/drewf) — Agent tool recognition for sub-agent visualization +- [@Matthew-Smith](https://github.com/Matthew-Smith) — Open VSX publishing workflow +- [@florintimbuc](https://github.com/florintimbuc) — Project coordination, CI workflow, Dependabot, linting infrastructure, publish workflow hardening, code review + ## v1.0.2 ### Bug Fixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9301128..4b48bdf2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ This project is licensed under the [MIT License](LICENSE), so your contributions ### Prerequisites - [Node.js](https://nodejs.org/) (v22 recommended) -- [VS Code](https://code.visualstudio.com/) (v1.107.0 or later) +- [VS Code](https://code.visualstudio.com/) (v1.105.0 or later) ### Setup diff --git a/README.md b/README.md index 38184c29..173163e5 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ This is the source code for the free Pixel Agents extension for VS Code — inst ## Requirements -- VS Code 1.107.0 or later +- VS Code 1.105.0 or later - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured ## Getting Started diff --git a/package-lock.json b/package-lock.json index a03e1019..63de9d4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@anthropic-ai/sdk": "^0.74.0", "@types/node": "22.x", "@types/pngjs": "^6.0.5", - "@types/vscode": "^1.107.0", + "@types/vscode": "^1.105.0", "esbuild": "^0.27.2", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", @@ -27,7 +27,7 @@ "typescript-eslint": "^8.54.0" }, "engines": { - "vscode": "^1.107.0" + "vscode": "^1.105.0" } }, "node_modules/@anthropic-ai/sdk": { diff --git a/package.json b/package.json index 216bfcec..ac694653 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "icon": "icon.png", "license": "MIT", "engines": { - "vscode": "^1.107.0" + "vscode": "^1.105.0" }, "categories": [ "Other" @@ -71,7 +71,7 @@ "@anthropic-ai/sdk": "^0.74.0", "@types/node": "22.x", "@types/pngjs": "^6.0.5", - "@types/vscode": "^1.107.0", + "@types/vscode": "^1.105.0", "esbuild": "^0.27.2", "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", From 111fc0185db8cae765d2f1c29c368551ab8dd396 Mon Sep 17 00:00:00 2001 From: Florin Timbuc Date: Sat, 14 Mar 2026 15:48:15 +0200 Subject: [PATCH 6/6] docs: add issue templates, PR template, and security policy (#121) --- .github/ISSUE_TEMPLATE/bug_report.yml | 74 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 +++ .github/pull_request_template.md | 25 +++++++++ CONTRIBUTING.md | 13 +++-- SECURITY.md | 24 +++++++++ 5 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/pull_request_template.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..1ca8035c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,74 @@ +name: "Bug report" +description: "Report a bug to help us improve Pixel Agents." +title: "[Bug]: " +labels: ["bug"] +body: + - type: textarea + id: description + attributes: + label: "Describe the bug" + description: "A clear and concise description of what the bug is." + placeholder: "e.g., Characters freeze after switching terminals." + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: "Steps to reproduce" + description: "List exact steps to reproduce the behavior." + placeholder: | + 1. Open Pixel Agents panel + 2. Click '+ Agent' + 3. ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: "Expected behavior" + description: "What should happen?" + validations: + required: true + + - type: textarea + id: actual + attributes: + label: "Actual behavior" + description: "What happens instead? Include any error messages." + validations: + required: true + + - type: textarea + id: screenshots + attributes: + label: "Screenshots / GIFs" + description: "If applicable, add screenshots or screen recordings." + + - type: input + id: vscode-version + attributes: + label: "VS Code version" + description: "Run 'Help > About' to find your version." + placeholder: "e.g., 1.109.0" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: "Operating System" + options: + - Windows + - macOS + - Linux + validations: + required: true + + - type: textarea + id: logs + attributes: + label: "Logs" + description: "Open Developer Tools (Help > Toggle Developer Tools) and paste any relevant console output." + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..c9c0a773 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: "Feature request" + url: "https://github.com/pablodelucca/pixel-agents/discussions/categories/ideas" + about: "Suggest new features or ideas in Discussions." + - name: "Question" + url: "https://github.com/pablodelucca/pixel-agents/discussions/categories/q-a" + about: "Ask questions and get help in Discussions." diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..17cf2d5d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +## Description + + +## Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor / code cleanup +- [ ] Documentation +- [ ] CI / build +- [ ] Other: ___ + +## Related issues + + +## Screenshots / GIFs + + +## Test plan + +- [ ] PR targets `main` branch +- [ ] `npm run build` passes locally +- [ ] Tested in Extension Development Host (F5) +- [ ] No inline constants (all in `src/constants.ts` or `webview-ui/src/constants.ts`) +- [ ] UI follows pixel art style (sharp corners, solid borders, pixel font) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b48bdf2..90ad90ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,16 +91,15 @@ These rules are set to `warn` — they won't block your PR but will flag violati ## Reporting Bugs -[Open an issue](https://github.com/pablodelucca/pixel-agents/issues) with: - -- What you expected to happen -- What actually happened -- Steps to reproduce -- VS Code version and OS +[Open a bug report](https://github.com/pablodelucca/pixel-agents/issues/new?template=bug_report.yml) — the form will guide you through providing the details we need. ## Feature Requests -Have an idea? [Open an issue](https://github.com/pablodelucca/pixel-agents/issues) to discuss it before building. This helps avoid duplicate work and ensures the feature fits the project's direction. +Have an idea? [Start a discussion](https://github.com/pablodelucca/pixel-agents/discussions/categories/ideas) in the Ideas category. We love hearing new ideas, and discussing them first helps us collaborate on the best approach together. + +## Security Issues + +Please report security vulnerabilities privately — see [SECURITY.md](SECURITY.md) for details. ## Code of Conduct diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..1d3e73e8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------|-----------| +| 1.x.x | :white_check_mark: | + +## Reporting a Vulnerability + +Please report security vulnerabilities through [GitHub's private vulnerability reporting](https://github.com/pablodelucca/pixel-agents/security/advisories/new). + +**Do not open a public issue for security vulnerabilities.** + +We will acknowledge your report within 7 days and aim to release a fix within 30 days of confirmation. + +## Scope + +Security issues relevant to this project include: + +- Command injection via terminal spawning or JSONL parsing +- Arbitrary file read/write beyond intended paths +- Cross-site scripting (XSS) in the webview +- Sensitive data exposure (e.g., leaking terminal output or session content)