diff --git a/DEFAULT_ART_AUDIT.md b/DEFAULT_ART_AUDIT.md deleted file mode 100644 index 762a0d5..0000000 --- a/DEFAULT_ART_AUDIT.md +++ /dev/null @@ -1,36 +0,0 @@ -# Default Art Audit - -## 実施内容 - -`app.js` の `buildDefaultGalleryPack()` を確認し、初期同梱作品の品質を監査した。低品質または初期作品として弱いものは、削除対象リストで除外する方式にした。既存ローカル状態に残っている Island Team 製の該当作品も、次回ロード時に Collection / island placement / dynamic summon から除外される。 - -## 削除した初期作品 - -| 作品名 | 理由 | -|---|---| -| Shell Rock | 16×16に対して実質的な形状が小さく、島上で石・貝の識別点が弱い。 | -| Azure Minnow | 魚としての頭・尾・ヒレが弱く、単なる水色の塊に近い。 | -| Firefly Swirl | 8×8で透明部が多く、作品というより粒子エフェクトに近い。 | -| Leaf Sparrow | 鳥のシルエットが薄く、頭・翼・尾の分離が不足している。 | -| Silver Trout | 魚としては読めるが、既存の Cloud Koi / Sunset Koi より低密度で初期作品として弱い。 | -| Butterfly Fish | 小型魚としての情報量が少なく、島上で主題が読み取りにくい。 | - -## 残したが改変候補に入る作品 - -| 作品名 | コメント | -|---|---| -| River Stones | 8×8としては成立しているが、石の影と水際の情報を足すと良い。 | -| Paper Crane | モチーフは明確だが、分断セルが多い。羽根の接続を調整できる。 | -| Linen Cottage | 読みやすいが、屋根と壁の面が単純。影と窓を増やせる。 | -| Human 系小型作品 | 役割差はあるが、体型・顔・道具の差分が弱いものがある。 | - -## コード上の変更点 - -- `REMOVED_LOW_QUALITY_SEED_ASSET_NAMES` を追加。 -- `buildDefaultGalleryPack()` の戻り値に品質フィルタを適用。 -- `mergeDefaultGallery()` で既存ローカル状態の Island Team 製削除対象も除外。 -- Python 側テストは `14 passed`。 - -## 次の作業 - -ユーザー確認後、新しいデフォルト作品を追加する。追加時は `PIXEL_ART_QUALITY.md` の基準に従い、既存の弱いカテゴリを補う。 diff --git a/DEFAULT_ART_EXPANSION.md b/DEFAULT_ART_EXPANSION.md deleted file mode 100644 index f0724c6..0000000 --- a/DEFAULT_ART_EXPANSION.md +++ /dev/null @@ -1,35 +0,0 @@ -# Default Art Expansion / Depth Lighting Tuning - -## Depth tuning - -Adjusted the depth-local light response so that: - -- local light influence stays colorized instead of washing into a neutral gray band, -- the light source itself and nearby sprite/depth corners catch the strongest highlight, -- brightness depends less on raw source-to-pixel distance than before, -- convex edges still read clearly under nearby inspection/night lights. - -Implementation areas: `depthLocalLightResponse()`, `getDepthEdgeLightBoost()`, and local-light color mixing inside `shadeAssetPixelColor()` in `app.js`. - -## UI bubble correction - -- Reduced the selection bubble top offset so the bubble appears closer to the selected sprite. - -## New large default works - -### Static / building -- Smile Portrait — 24x32 -- Rosetta Stela — 24x32 -- Pharaoh Mask — 24x32 -- Terracotta Sentinel — 20x32 - -### Static / nature -- Great Wave Panel — 32x24 -- Sunflower Still Life — 24x32 -- Stone Circle — 32x24 - -## Notes - -- `assets.json` and `default_assets_sheet.png` were refreshed. -- The seeded island placement list was expanded so these works appear by default. -- Save schema was bumped to `25`. diff --git a/PIXEL_ART_QUALITY.md b/PIXEL_ART_QUALITY.md deleted file mode 100644 index 578a256..0000000 --- a/PIXEL_ART_QUALITY.md +++ /dev/null @@ -1,69 +0,0 @@ -# ドット絵品質基準 / Pixel Art Quality Standard - -このプロトタイプで「良質なドット絵」と見なす基準。共有サービスの初期作品、審査、改変、削除判断で使う。 - -## 1. 最低条件 - -- **透明セルを作品の一部として設計している**:余白はただの空きではなく、シルエットを読ませるために残す。 -- **1秒以内に主題が判別できる**:縮小表示、島上表示、夜間表示でも「家」「鳥」「魚」「橋」などの役割が読める。 -- **接地・重心が明確**:静物は地面や水面に乗って見える。動物・人は足元または接地点が曖昧でない。 -- **パレットに収まる**:任意色の多用ではなく、既存パレットコードに近い色で統一する。 -- **クリック対象として破綻しない**:極端に薄い線、孤立点だけの作品、透明部分が多すぎる作品は避ける。 - -## 2. 具体的な評価軸 - -| 評価軸 | 良い状態 | 低品質と判断する状態 | -|---|---|---| -| シルエット | 背景なしでも輪郭だけで主題が読める | 点・棒・塊にしか見えない | -| 明暗 | 上面・側面・影の方向がある | 全面が同じ明度で平坦 | -| 色数 | 小物は3〜6色、中型は5〜10色程度で役割が分かれる | 1〜2色だけ、または無秩序に多色 | -| ピクセル密度 | 余白、密部、抜きのバランスがある | ほぼ空白、またはベタ塗りの矩形 | -| エッジ | 角、段差、斜線に意図がある | ガタつきが偶然に見える | -| 内部ディテール | 目、窓、葉、波、道具など識別点がある | 主題を補強する点がない | -| 島との親和性 | 地形、昼夜、他作品のサイズ感と合う | 尺度が浮く、接地しない、光源が不自然 | -| メタ情報 | building の door、light、depth、particle が主題に沿う | メタ情報がない、または見た目と矛盾する | - -## 3. サイズ別の合格目安 - -### 8×8 - -- 主題は1つに絞る。 -- 色は3〜5色までを目安にする。 -- 透明を含めても、主題の有効ピクセルは概ね16セル以上ほしい。 -- 「点の集合」ではなく、頭・胴・尾、花・葉・茎などの構造を最低2部位に分ける。 - -### 12×12〜16×16 - -- デフォルト作品の中心サイズ。 -- 輪郭色、主色、中間色、ハイライト、影の5層を基本にする。 -- 動物・人は顔または進行方向が読めること。 -- 建物・自然物は接地線、屋根/幹/水面などの基礎形を入れる。 - -### 20×20以上 - -- 大型作品はディテール量だけでなく、遠目の大形状を優先する。 -- 3〜5個の大きな面に分け、その中に小ディテールを置く。 -- 光・depth・particle を使う場合、効果が主題を補強していること。 - -## 4. 削除・改変の判断 - -### 削除候補 - -- 何を描いたか判別できない。 -- 既存の高品質作品と役割が重複し、見劣りする。 -- ほぼ点、線、単色塊で、改変より作り直しが早い。 -- 島上でクリックや配置の視認性を落とす。 - -### 改変候補 - -- 主題は読めるが、明暗・輪郭・接地が弱い。 -- 色の階調が足りない。 -- サイズに対してディテールが不足している。 -- 良いアイデアはあるが、他作品群と密度が合っていない。 - -## 5. デフォルト作品としての追加基準 - -- 初回表示でサービスの品質水準を示せること。 -- ユーザーがリミックスしたくなる明確なモチーフを持つこと。 -- 小型、中型、大型、静物、動物、人、船、水辺、建物が偏りすぎないこと。 -- 低品質作品を大量に残して作品数を稼がないこと。 diff --git a/README.md b/README.md index 5bd66ef..5456d68 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Open `index.html` in a modern browser. No build step or third-party dependency i - Save assets to the local collection. - Place static assets and summon dynamic assets onto the island. - Dynamic assets default to right-facing on spawn; left-facing is rendered by mirroring during movement only. -- Local voting, hiding, remix/copy edit, and import/export tools are included. +- Local voting, hiding, remix/copy edit, and collection management are included. ## Phase 1 changes @@ -33,7 +33,6 @@ Open `index.html` in a modern browser. No build step or third-party dependency i - Local save uses a compact Phase 2 envelope in `localStorage`. - Pixel planes are bbox-cropped and RLE-compressed when this is smaller than raw cropped data. -- Compact asset bundles can be exported separately from world snapshots. - World snapshots contain placements, dynamic summons, and an asset manifest rather than full asset blobs. - Local object/asset mutations append small sync events to `eventLog`. - IndexedDB asset/snapshot cache helpers are included for future server-backed missing-asset fetches. @@ -46,7 +45,6 @@ Open `index.html` in a modern browser. No build step or third-party dependency i - Dynamic runtime updates are stepped at 15 Hz and capped per frame to avoid spiral-of-death on slow devices. - Rendering pauses while the browser tab is hidden and resumes on visibility change. - Tile/object lookups use a small spatial index for inspect, erase, and nearby-target searches. -- Data tab reports visible terrain chunks for quick performance inspection. ## Phase 4 changes @@ -55,20 +53,11 @@ Open `index.html` in a modern browser. No build step or third-party dependency i - Added Select tool. Drag to select a rectangular area, then drag the selection or use arrow keys/buttons to move it. - Nudge buttons move the active selection; without a selection they shift the whole drawing. - Added horizontal/vertical flip, palette-colored outline generation, and clear selection. -- Added editor PNG export/import. PNG import quantizes the image to the current island palette and current canvas size. - Added keyboard shortcuts: B/E/F/I/L/R/S for tools, Ctrl/Cmd+Z/Y for history, arrows for moving selected pixels, Escape to clear selection. -## Export formats - -- **Export full JSON**: human-readable full local state. -- **Export compact**: compact local state suitable for saving/transferring. -- **Export snapshot**: placement state plus asset manifest; requires asset bundles or cache to render fully. -- **Export asset bundle**: compact asset blob list for missing-asset transfer. - ## Phase 4 follow-up adjustments -- PNG import now chooses the editor canvas size from the image size. Square 8/16/32/64 PNGs import at their native size; other sizes are fit into the nearest supported square canvas. - Depth is now three-state: high, normal, and low. High brightens sprite pixels; low darkens them. - Right-clicking the world canvas clears the current map selection instead of moving or placing the selected asset. - Dynamic sprites now pause intermittently instead of walking continuously. @@ -80,16 +69,7 @@ Open `index.html` in a modern browser. No build step or third-party dependency i - Editor schema is now 7. - Fixed the Advanced Draw depth controls so `Depth / High / Low / Clear` wrap inside the drawer instead of overflowing to the right. -- Added Phase 5 guardrails in the Data tab: - - local asset/object volume counters, - - duplicate-content count, - - orphan placement checks, - - terrain compatibility checks, - - hidden/report counters. -- Added `Validate world` export for pre-server integrity checks. - Added local object reporting from the selection bubble. Reported objects are hidden locally and stored in a local moderation report log. -- Added `Export moderation report` for review/debug data. -- Added `Clear reports` to clear local report logs without un-hiding already hidden objects. - Added soft local guardrail limits: 160 assets and 220 world objects. Existing objects can still be moved; new saves/placements are blocked when the local cap is reached. ## Phase 5 follow-up changes @@ -129,7 +109,6 @@ Open `index.html` in a modern browser. No build step or third-party dependency i - Walking particles are less frequent and use more visibly varied colors. - Remix always creates a new derivative asset; Edit updates the user's own original asset. - Advanced Draw now includes configurable particle emitter cells for any asset. -- PNG export/import moved to the Data tab. - Nudge arrow buttons under the canvas were removed; keyboard arrow nudging remains. - UI text is larger overall, while palette color-code labels keep their small size. @@ -138,7 +117,7 @@ Open `index.html` in a modern browser. No build step or third-party dependency i This build adds count-based island rotation. - Default island display cap: 250 objects. -- The local display cap is adjustable from Data > Visual settings. +- The local display cap is adjustable from Settings. - Objects are not deleted when the cap is exceeded; the effective oldest objects are omitted from the island display. - Upvotes delay rotation-out by slot adjustment. Downvotes advance rotation-out. - Local publish / republish quota: 5 objects per author per rolling hour. @@ -306,7 +285,6 @@ Use this to compare minified JSON, gzip, zlib, and Brotli when available. Produc - Reworked depth local-light behavior so the source itself and nearby outer/depth corners catch the strongest highlights, while distance has a weaker effect on brightness than before. - Reduced the selection bubble offset so the bubble sits noticeably closer to the top of the selected artwork. - Added several new large default works themed around masterpieces, heritage, and artifacts: **Smile Portrait**, **Great Wave Panel**, **Sunflower Still Life**, **Rosetta Stela**, **Pharaoh Mask**, **Stone Circle**, **Terracotta Sentinel**. -- Refreshed `assets.json` and `default_assets_sheet.png` after the additions. ## Custom update 4: cursor-light smoothing + gray-bloom hotfix diff --git a/app.js b/app.js index af460ca..9cb911b 100644 --- a/app.js +++ b/app.js @@ -20,15 +20,15 @@ const TERRAIN_CHUNK_SIZE = 16; const VIEW_CULL_MARGIN = 192; const DYNAMIC_LOGIC_STEP_MS = 1000 / 15; + const TARGET_RENDER_FRAME_MS = 1000 / 30; const MAX_DYNAMIC_STEPS_PER_FRAME = 3; const MAX_SPRITE_CACHE_ENTRIES = 260; const DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER = 5; - // TEMPORARY MODERATION / ADMIN EASEMENT: - // While this flag is true, every collection work can be deleted regardless of owner. - // Set back to false to restore the original owner-only deletion rule in shared worlds. - const TEMP_ALLOW_DELETE_ALL_WORKS = true; - - const COLOR_CODES = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const BASE_COLOR_CODES = '0123456789abcdefghij'; + const ADVANCED_COLOR_CODES = 'klmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + "!#$%&'()*+,-/:;<=>?@[]^_`{|}~"; + const COLOR_CODES = BASE_COLOR_CODES + ADVANCED_COLOR_CODES; + const BASIC_PALETTE_COUNT = BASE_COLOR_CODES.length; + const DEFAULT_SELECTED_COLOR_CODE = BASE_COLOR_CODES.includes('a') ? 'a' : (BASE_COLOR_CODES[0] || '0'); const PALETTE = buildPalette(); const PALETTE_BY_CODE = Object.fromEntries(PALETTE.map((p) => [p.code, p.color])); const MODULES = window.PixelIslandModules || {}; @@ -107,7 +107,7 @@ const DEFAULT_SERVER_AUTHORITY = Object.freeze({ publish: 'server', objectMove: 'server', - dayNight: 'server', + dayNight: 'local', dynamicMotion: 'server' }); @@ -118,15 +118,11 @@ } function isServerAuthoritative(feature) { + if (feature === 'dayNight') return false; return serverAuthority()[feature] === 'server'; } function getAuthoritativeNow() { - const clock = state.serverSync?.clock || null; - if (isServerAuthoritative('dayNight') && clock && Number.isFinite(clock.worldTimeMs)) { - const syncedAt = Number(clock.syncedAt || Date.now()); - return Number(clock.worldTimeMs) + Math.max(0, Date.now() - syncedAt); - } return Date.now(); } @@ -179,7 +175,7 @@ } function roleToCategory(role) { - return role === 'human' || role === 'animal' || role === 'bird' ? 'dynamic' : 'static'; + return role === 'human' || role === 'animal' || role === 'bird' || role === 'fish' ? 'dynamic' : 'static'; } function roleToSubtype(role) { @@ -188,7 +184,7 @@ function subtypeToRole(asset) { const subtype = asset?.subtype; - if (['human', 'animal', 'bird', 'nature', 'building', 'ship', 'other'].includes(subtype)) return subtype; + if (['human', 'animal', 'bird', 'fish', 'nature', 'building', 'ship', 'other'].includes(subtype)) return subtype; if (subtype === 'water') return 'other'; if (asset?.category === 'dynamic') return 'animal'; return 'other'; @@ -229,15 +225,14 @@ sideSwitcher: $('sideSwitcher'), editRight: $('editRight'), editLeft: $('editLeft'), paintColor: $('paintColor'), toolBrush: $('toolBrush'), toolErase: $('toolErase'), toolFill: $('toolFill'), toolPick: $('toolPick'), toolLine: $('toolLine'), toolRect: $('toolRect'), toolSelect: $('toolSelect'), undoPaint: $('undoPaint'), redoPaint: $('redoPaint'), - toolLight: $('toolLight'), toolParticle: $('toolParticle'), toolDoor: $('toolDoor'), toolDepth: $('toolDepth'), depthHigh: $('depthHigh'), depthLow: $('depthLow'), toggleAdvanced: $('toggleAdvanced'), advancedHint: $('advancedHint'), advancedToolGroup: $('advancedToolGroup'), particleDirectionWrap: $('particleDirectionWrap'), particleDirection: $('particleDirection'), particleUseSelection: $('particleUseSelection'), particleClearRange: $('particleClearRange'), particleRangeStatus: $('particleRangeStatus'), clearPaint: $('clearPaint'), flipHorizontal: $('flipHorizontal'), flipVertical: $('flipVertical'), outlinePaint: $('outlinePaint'), clearSelection: $('clearSelection'), nudgeLeft: $('nudgeLeft'), nudgeRight: $('nudgeRight'), nudgeUp: $('nudgeUp'), nudgeDown: $('nudgeDown'), exportPng: $('exportPng'), importPng: $('importPng'), pngImportInput: $('pngImportInput'), + toolLight: $('toolLight'), toolParticle: $('toolParticle'), toolDoor: $('toolDoor'), toolDepth: $('toolDepth'), depthHigh: $('depthHigh'), depthLow: $('depthLow'), toggleAdvanced: $('toggleAdvanced'), advancedHint: $('advancedHint'), advancedToolGroup: $('advancedToolGroup'), particleDirectionWrap: $('particleDirectionWrap'), particleDirection: $('particleDirection'), particleUseSelection: $('particleUseSelection'), particleClearRange: $('particleClearRange'), particleRangeStatus: $('particleRangeStatus'), clearPaint: $('clearPaint'), flipHorizontal: $('flipHorizontal'), flipVertical: $('flipVertical'), outlinePaint: $('outlinePaint'), clearSelection: $('clearSelection'), nudgeLeft: $('nudgeLeft'), nudgeRight: $('nudgeRight'), nudgeUp: $('nudgeUp'), nudgeDown: $('nudgeDown'), paintCanvas: $('paintCanvas'), editHint: $('editHint'), paletteGrid: $('paletteGrid'), lightColor: $('lightColor'), staticSettingsPanel: $('staticSettingsPanel'), dynamicSettingsPanel: $('dynamicSettingsPanel'), doorMarkerHint: $('doorMarkerHint'), settingsSummary: $('settingsSummary'), saveAsset: $('saveAsset'), saveAndPlace: $('saveAndPlace'), checkOnIsland: $('checkOnIsland'), newAsset: $('newAsset'), lineageNote: $('lineageNote'), assetList: $('assetList'), likedCodex: $('likedCodex'), showHiddenAssets: $('showHiddenAssets'), hiddenAssetPanel: $('hiddenAssetPanel'), hiddenAssetList: $('hiddenAssetList'), - exportData: $('exportData'), importData: $('importData'), resetAll: $('resetAll'), dataBox: $('dataBox'), - exportCompact: $('exportCompact'), exportSnapshot: $('exportSnapshot'), exportAssetBundle: $('exportAssetBundle'), syncStats: $('syncStats'), guardrailStats: $('guardrailStats'), validateWorld: $('validateWorld'), exportModerationReport: $('exportModerationReport'), clearReports: $('clearReports'), settingLights: $('settingLights'), settingParticles: $('settingParticles'), settingDayNight: $('settingDayNight'), displayLimit: $('displayLimit'), rotationStats: $('rotationStats'), - reportDialog: $('reportDialog'), reportReason: $('reportReason'), reportCancel: $('reportCancel'), reportSubmit: $('reportSubmit'), reportObjectName: $('reportObjectName') + reportDialog: $('reportDialog'), reportReason: $('reportReason'), reportCancel: $('reportCancel'), reportSubmit: $('reportSubmit'), reportObjectName: $('reportObjectName'), + confirmDialog: $('confirmDialog'), confirmTitle: $('confirmTitle'), confirmMessage: $('confirmMessage'), confirmOk: $('confirmOk'), confirmCancel: $('confirmCancel') }; const ctx = els.canvas.getContext('2d', { alpha: false }); @@ -265,6 +260,7 @@ let spriteCache = new Map(); let lastRenderedSpriteInfo = new Map(); let lastRuntimeUpdate = performance.now(); + let lastRenderAt = 0; let lastClockSecond = -1; let toastTimer = null; @@ -275,8 +271,9 @@ let editorLeftPixels = blankPixels(8); let editingSide = 'right'; let paintTool = 'brush'; - let selectedColorCode = 'a'; + let selectedColorCode = DEFAULT_SELECTED_COLOR_CODE; let advancedDraw = false; + let activeTabName = 'draw'; let depthPixels = blankPixels(8).map(() => 0); let depthPaintMode = 1; let isPainting = false; @@ -309,6 +306,9 @@ let cameraFollowSelected = false; let pendingReport = null; let libraryFilter = 'all'; + let librarySearch = ''; + let libraryViewTab = 'mine'; + let defaultGalleryAssetNames = null; let renderPhase = null; let editorView = { zoom: 1, x: 0, y: 0 }; let editorPointer = { panning: false, pointerId: null, lastX: 0, lastY: 0 }; @@ -339,8 +339,6 @@ clearEditorHistory(); renderLibrary(); updateSelectedLabel(); - updateSyncStats(); - updateGuardrailStats(); hydrateVisualSettingsUI(); updateRotationStats(); scheduleFrame(); @@ -354,11 +352,11 @@ els.openEditor?.addEventListener('click', () => { clearWorldSelection(false); setDrawerOpen(true); }); els.openCreate?.addEventListener('click', () => { setTab('draw'); clearWorldSelection(false); setDrawerOpen(true); }); - els.openCollection?.addEventListener('click', () => { setTab('library'); clearWorldSelection(false); setDrawerOpen(true); }); + els.openCollection?.addEventListener('click', () => { setTab('library', { preserveScroll: true }); clearWorldSelection(false); setDrawerOpen(true); }); els.closeEditor.addEventListener('click', () => setDrawerOpen(false)); els.tabs.forEach((button) => { - button.addEventListener('click', () => setTab(button.dataset.tab)); + button.addEventListener('click', () => setTab(button.dataset.tab, { preserveScroll: button.dataset.tab === 'library' })); }); els.authorName?.addEventListener('input', () => { @@ -476,6 +474,7 @@ particleConfig = normalizeParticleConfig({ ...particleConfig, dir: els.particleDirection.value || 'up' }); if (particleConfig.enabled && JSON.stringify(particleConfig) !== before) markEditorChanged(); updateParticleUI(); + renderPalette(); drawEditor(); }); els.particleUseSelection?.addEventListener('click', setParticleRangeFromSelection); @@ -483,20 +482,7 @@ els.depthHigh?.addEventListener('click', () => setDepthPaintMode(1)); els.depthLow?.addEventListener('click', () => setDepthPaintMode(-1)); els.toggleAdvanced?.addEventListener('click', () => { - advancedDraw = !advancedDraw; - toggleHidden(els.advancedToolGroup, !advancedDraw); - toggleHidden(els.toolLight, !advancedDraw); - toggleHidden(els.toolParticle, !advancedDraw); - toggleHidden(els.toolDepth, !advancedDraw); - toggleHidden(els.depthHigh, !advancedDraw); - toggleHidden(els.depthLow, !advancedDraw); - toggleHidden(els.particleDirectionWrap, !advancedDraw || paintTool !== 'particle'); - toggleHidden(els.advancedHint, !advancedDraw); - els.toggleAdvanced.classList.toggle('active', advancedDraw); - els.drawer?.classList.toggle('advancedTools', advancedDraw); - if (!advancedDraw && (paintTool === 'depth' || paintTool === 'light' || paintTool === 'particle')) setPaintTool('brush'); - updateParticleUI(); - drawEditor(); + setAdvancedDraw(!advancedDraw); }); els.clearPaint.addEventListener('click', () => { if (!confirm('Clear the entire canvas? This removes all pixels, lights, depth, particles, and selection.')) return; @@ -519,9 +505,6 @@ els.nudgeRight?.addEventListener('click', () => nudgeEditor(1, 0)); els.nudgeUp?.addEventListener('click', () => nudgeEditor(0, -1)); els.nudgeDown?.addEventListener('click', () => nudgeEditor(0, 1)); - els.exportPng?.addEventListener('click', exportEditorPng); - els.importPng?.addEventListener('click', () => els.pngImportInput?.click()); - els.pngImportInput?.addEventListener('change', importEditorPng); els.undoPaint?.addEventListener('click', undoEditor); els.redoPaint?.addEventListener('click', redoEditor); window.addEventListener('keydown', onEditorKeyDown); @@ -535,6 +518,9 @@ els.reportCancel?.addEventListener('click', () => closeReportDialog()); els.reportSubmit?.addEventListener('click', () => submitReportDialog()); els.reportDialog?.addEventListener('click', (event) => { if (event.target === els.reportDialog) closeReportDialog(); }); + els.confirmCancel?.addEventListener('click', () => resolveConfirmDialog(false)); + els.confirmOk?.addEventListener('click', () => resolveConfirmDialog(true)); + els.confirmDialog?.addEventListener('click', (event) => { if (event.target === els.confirmDialog) resolveConfirmDialog(false); }); els.bubbleHide?.addEventListener('click', () => hideSelected()); els.paintCanvas.addEventListener('pointerdown', onPaintPointerDown); @@ -553,15 +539,6 @@ els.saveAndPlace.addEventListener('click', saveAndPlaceFromEditor); els.checkOnIsland?.addEventListener('click', checkCurrentEditorOnIsland); els.newAsset?.addEventListener('click', newAsset); - els.exportData.addEventListener('click', exportData); - els.importData.addEventListener('click', importData); - els.exportCompact?.addEventListener('click', exportCompactData); - els.exportSnapshot?.addEventListener('click', exportSnapshotData); - els.exportAssetBundle?.addEventListener('click', exportAssetBundleData); - els.resetAll.addEventListener('click', resetAll); - els.validateWorld?.addEventListener('click', validateWorld); - els.exportModerationReport?.addEventListener('click', exportModerationReport); - els.clearReports?.addEventListener('click', clearModerationReports); els.showHiddenAssets?.addEventListener('click', () => { els.hiddenAssetPanel.hidden = !els.hiddenAssetPanel.hidden; renderHiddenAssets(); @@ -612,9 +589,40 @@ setDrawerOpen(false); } - function setTab(name) { + function setTab(name, options = {}) { + const scroller = els.drawer?.querySelector('.drawerBody'); + const keepScroll = Boolean(options.preserveScroll); + const scrollTop = keepScroll ? scroller?.scrollTop : null; + activeTabName = name; els.tabs.forEach((button) => button.classList.toggle('active', button.dataset.tab === name)); els.panels.forEach((panel) => panel.classList.toggle('active', panel.id === `tab-${name}`)); + if (name !== 'draw' && advancedDraw) setAdvancedDraw(false, { redraw: false }); + else els.drawer?.classList.toggle('advancedTools', name === 'draw' && advancedDraw); + if (keepScroll && Number.isFinite(scrollTop)) restoreDrawerScroll(scrollTop); + } + + function setAdvancedDraw(enabled, options = {}) { + const redraw = options.redraw !== false; + advancedDraw = Boolean(enabled); + const selectedPaletteIndex = PALETTE.findIndex((entry) => entry.code === selectedColorCode); + if (!advancedDraw && selectedPaletteIndex >= BASIC_PALETTE_COUNT) { + selectedColorCode = nearestBasicPaletteCode(PALETTE_BY_CODE[selectedColorCode] || PALETTE_BY_CODE[DEFAULT_SELECTED_COLOR_CODE] || '#ffffff'); + } + toggleHidden(els.advancedToolGroup, !advancedDraw); + toggleHidden(els.toolLight, !advancedDraw); + toggleHidden(els.toolParticle, !advancedDraw); + toggleHidden(els.toolDepth, !advancedDraw); + toggleHidden(els.depthHigh, !advancedDraw); + toggleHidden(els.depthLow, !advancedDraw); + toggleHidden(els.particleDirectionWrap, !advancedDraw || paintTool !== 'particle'); + toggleHidden(els.advancedHint, !advancedDraw); + els.toggleAdvanced?.classList.toggle('active', advancedDraw); + if (els.toggleAdvanced) els.toggleAdvanced.textContent = advancedDraw ? '竏・Advanced' : '・・Advanced'; + els.drawer?.classList.toggle('advancedTools', activeTabName === 'draw' && advancedDraw); + if (!advancedDraw && (paintTool === 'depth' || paintTool === 'light' || paintTool === 'particle')) setPaintTool('brush'); + updateParticleUI(); + renderPalette(); + if (redraw) drawEditor(); } function setMode(nextMode) { @@ -631,7 +639,7 @@ } function refreshCategoryUI() { - els.drawer?.classList.toggle('advancedTools', advancedDraw); + els.drawer?.classList.toggle('advancedTools', activeTabName === 'draw' && advancedDraw); const role = currentRole(); const isStatic = roleToCategory(role) === 'static'; if (isStatic) staticKind = roleToSubtype(role); @@ -646,6 +654,8 @@ toggleHidden(els.toolLight, !advancedDraw); toggleHidden(els.toolParticle, !advancedDraw); toggleHidden(els.toolDepth, !advancedDraw); + toggleHidden(els.depthHigh, !advancedDraw); + toggleHidden(els.depthLow, !advancedDraw); toggleHidden(els.particleDirectionWrap, !advancedDraw || paintTool !== 'particle'); toggleHidden(els.toolDoor, role !== 'building'); toggleHidden(els.doorMarkerHint, role !== 'building'); @@ -667,13 +677,14 @@ if (!els.roleHint) return; const role = currentRole(); const textMap = { - human: 'Humans move by target direction. Draw as ▶ Right, or press ◀ Left if your canvas is left-facing so Save mirrors it.', - animal: 'Animals move by target direction. Draw as ▶ Right, or press ◀ Left if your canvas is left-facing so Save mirrors it.', - bird: 'Birds fly over terrain and seek nature. Draw as ▶ Right, or press ◀ Left if your canvas is left-facing so Save mirrors it.', - nature: 'Nature attracts animals and birds. Static sprites are drawn at 2× scale.', + human: 'Humans move by target direction. Draw as 笆カ Right, or press 笳€ Left if your canvas is left-facing so Save mirrors it.', + animal: 'Animals move by target direction. Draw as 笆カ Right, or press 笳€ Left if your canvas is left-facing so Save mirrors it.', + bird: 'Birds fly over terrain and seek nature. Draw as 笆カ Right, or press 笳€ Left if your canvas is left-facing so Save mirrors it.', + nature: 'Nature attracts animals and birds. Static sprites are drawn at 2テ・scale.', + fish: 'Fish swim on water tiles. Draw as right-facing, or use Left if your canvas is left-facing so Save mirrors it.', building: 'Buildings attract humans. Use Door to mark the entrance.', ship: 'Ships are static water objects. They float and emit ring ripples.', - other: 'Other objects are neutral scenery and render at 2× scale.' + other: 'Other objects are neutral scenery and render at 2テ・scale.' }; els.roleHint.textContent = textMap[role] || textMap.other; } @@ -687,11 +698,11 @@ const lightText = lightCount ? `${lightCount}/${maxLights} lamp cell${lightCount === 1 ? '' : 's'}` : `no light (${maxLights} max)`; const particleText = particlePixels.length ? `particles ${particlePixels.length} cell${particlePixels.length === 1 ? '' : 's'}` : 'no particles'; const doorText = role === 'building' ? ` / door ${Math.round(doorPixel.x)},${Math.round(doorPixel.y)}` : ''; - els.settingsSummary.textContent = `${cap(role)} / ${editorWidth}×${editorHeight} canvas / 2× static pixels / ${lightText} / ${particleText}${doorText}.`; + els.settingsSummary.textContent = `${cap(role)} / ${editorWidth}テ・{editorHeight} canvas / 2テ・static pixels / ${lightText} / ${particleText}${doorText}.`; } else { const particleText = particlePixels.length ? ` / particles ${particlePixels.length} cell${particlePixels.length === 1 ? '' : 's'}` : ''; - const sideText = editingSide === 'left' ? 'canvas marked ◀ Left; Save mirrors it into canonical ▶ Right' : 'canvas marked canonical ▶ Right'; - els.settingsSummary.textContent = `${cap(role)} / ${editorWidth}×${editorHeight} canvas / ${sideText}${particleText}.`; + const sideText = editingSide === 'left' ? 'canvas marked 笳€ Left; Save mirrors it into canonical 笆カ Right' : 'canvas marked canonical 笆カ Right'; + els.settingsSummary.textContent = `${cap(role)} / ${editorWidth}テ・{editorHeight} canvas / ${sideText}${particleText}.`; } } @@ -1038,7 +1049,7 @@ const limit = getLocalDisplayLimit(); const publish = getPublishQuotaStatus(); const quotaText = publish.accountRequired ? 'account required to publish' : `publish quota ${publish.used}/${publish.limit} this hour`; - els.rotationStats.textContent = `Island exhibition ${visible}/${entries.length} objects · newest ${buckets.newest.length}/150 · revival ${buckets.revival.length}/100 · local cap ${limit} · rotated out ${hiddenByRotation} · ${quotaText}.`; + els.rotationStats.textContent = `Island exhibition ${visible}/${entries.length} objects ツキ newest ${buckets.newest.length}/150 ツキ revival ${buckets.revival.length}/100 ツキ local cap ${limit} ツキ rotated out ${hiddenByRotation} ツキ ${quotaText}.`; updateAccountUI(); } @@ -1117,6 +1128,7 @@ assets: target.tombstones?.assets && typeof target.tombstones.assets === 'object' ? target.tombstones.assets : {}, objects: target.tombstones?.objects && typeof target.tombstones.objects === 'object' ? target.tombstones.objects : {} }; + target.deletedSeedAssetNames = Array.isArray(target.deletedSeedAssetNames) ? [...new Set(target.deletedSeedAssetNames.filter(Boolean).map(String))] : []; return target; } @@ -1136,7 +1148,11 @@ } function canCurrentAccountModifyObject(kind, object, showToast = true) { - if (!isSharedWorld()) return true; + if (!isSharedWorld()) { + const allowed = isCurrentUserAsset(findAsset(object?.assetId)); + if (!allowed && showToast) toast('Only the owner can move or delete this island object.'); + return allowed; + } const actor = currentAccountId(); if (!actor) { if (showToast) toast('Shared worlds require an account before changing island objects.'); @@ -1148,10 +1164,37 @@ return false; } + function removeObjectLocally(kind, objectId) { + if (!objectId) return null; + const list = kind === 'dynamic' ? state.dynamicSummons : state.placed; + const index = list.findIndex((item) => item.id === objectId); + if (index < 0) return null; + const [object] = list.splice(index, 1); + ensureWorldProtectionState(); + state.tombstones.objects[objectId] = { + id: objectId, + assetId: object.assetId, + deletedAt: Date.now(), + deletedBy: currentAccountId() || 'local', + ownerAccountId: normalizeOwnerAccountId(object.ownerAccountId), + version: Number(object.version) || 1 + }; + delete state.objectVotes?.[objectId]; + delete state.hiddenObjects?.[objectId]; + state.moderationReports = (state.moderationReports || []).filter((report) => report.objectId !== objectId); + if (selectedObject?.id === objectId) selectedObject = null; + rebuildWorldIndex(); + if (kind === 'dynamic') hydrateRuntime(); + saveState(); + updateSelectionBubble(performance.now()); + return object; + } + function canCurrentAccountDeleteAsset(asset, showToast = true) { - // Temporary measure: allow deleting any work so the collection can be cleaned up quickly. - // Revert by setting TEMP_ALLOW_DELETE_ALL_WORKS back to false above. - if (TEMP_ALLOW_DELETE_ALL_WORKS) return true; + if (!isCurrentUserAsset(asset)) { + if (showToast) toast('Only the owner can delete this asset.'); + return false; + } if (!isSharedWorld()) return true; const actor = currentAccountId(); if (!actor) { @@ -1164,6 +1207,22 @@ return false; } + function isCurrentUserAsset(asset) { + if (!asset) return false; + if (isSampleGalleryAsset(asset)) return false; + if (!isSharedWorld()) { + const owner = normalizeOwnerAccountId(asset.ownerAccountId); + if (owner) return owner === currentAccountId(); + return (asset.author || 'Local Artist') === (state.authorName || 'Local Artist'); + } + return normalizeOwnerAccountId(asset.ownerAccountId) === currentAccountId(); + } + + function displayAssetAuthor(asset) { + if (!asset) return state.authorName || 'Local Artist'; + return isSharedWorld() ? (asset.author || 'Local Artist') : (state.authorName || 'Local Artist'); + } + function makeSharedCommand(type, payload = {}) { return { id: `cmd_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`, @@ -1174,7 +1233,7 @@ }; } - function queueSharedCommand(command) { + function queueSharedCommand(command, options = {}) { if (!command) return; ensureWorldProtectionState(); state.serverSync.pendingCommands.push(command); @@ -1200,7 +1259,7 @@ }; } saveState(); - toast('Change queued for server validation.'); + if (options.toast !== false) toast('Change queued for server validation.'); } @@ -1324,7 +1383,7 @@ localY = worldY - info.drawY; } if (localX < 0 || localY < 0 || localX >= info.sprite.width || localY >= info.sprite.height) return false; - // Small works at 16×16 or below are hard to click. Use the whole sprite box + // Small works at 16テ・6 or below are hard to click. Use the whole sprite box // for them, including transparent cells, while larger works still use opaque pixels. const aw = assetWidth(asset); const ah = assetHeight(asset); @@ -1362,7 +1421,7 @@ } const asset = findAsset(selectedAssetId); if (els.selectedAssetName) { - els.selectedAssetName.textContent = asset ? `${asset.name || 'Untitled'} · ${cap(subtypeToRole(asset))}` : 'No collection work selected'; + els.selectedAssetName.textContent = asset ? `${asset.name || 'Untitled'} ツキ ${cap(subtypeToRole(asset))}` : 'No collection work selected'; } } @@ -1423,6 +1482,7 @@ if (!canPlace(placementPreview.asset, tile)) return; placementPreview.x = x; placementPreview.y = y; + if (visualSettings().enableParticles) spawnEffects.push({ x: x + .5, y: y + .5, started: performance.now(), preview: true }); render(); toast(adjusted.moved ? 'Preview nudged away from the remix source. Use Place here or Back to canvas.' : 'Preview set. Use Place here or Back to canvas.'); return; @@ -1564,43 +1624,36 @@ const staticObjects = getPlacedAtTile(x, y); const staticTarget = staticObjects.at(-1); if (staticTarget) { + if (!canCurrentAccountModifyObject('static', staticTarget)) return; if (isSharedWorld()) { - if (!canCurrentAccountModifyObject('static', staticTarget)) return; queueSharedCommand(makeSharedCommand('object.delete', { kind: 'static', objectId: staticTarget.id })); return; } - const index = state.placed.findIndex((p) => p.id === staticTarget.id); - if (index >= 0) { - const item = state.placed[index]; - const asset = findAsset(item.assetId); - state.placed.splice(index, 1); - rebuildWorldIndex(); - recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('static', item.id)); - if (selectedObject?.id === item.id) selectedObject = null; - saveState(); - toast(`${asset?.name ?? 'Object'} removed.`); + const item = removeObjectLocally('static', staticTarget.id); + if (item) { + if (!isSharedWorld()) { + recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('static', item.id)); + saveState(); + } + toast(`${findAsset(item.assetId)?.name ?? 'Object'} removed.`); return; } } const dynamicObjects = getDynamicHomesAtTile(x, y); const dynamicTarget = dynamicObjects.at(-1); if (dynamicTarget) { + if (!canCurrentAccountModifyObject('dynamic', dynamicTarget)) return; if (isSharedWorld()) { - if (!canCurrentAccountModifyObject('dynamic', dynamicTarget)) return; queueSharedCommand(makeSharedCommand('object.delete', { kind: 'dynamic', objectId: dynamicTarget.id })); return; } - const index = state.dynamicSummons.findIndex((p) => p.id === dynamicTarget.id); - if (index >= 0) { - const item = state.dynamicSummons[index]; - const asset = findAsset(item.assetId); - state.dynamicSummons.splice(index, 1); - rebuildWorldIndex(); - recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('dynamic', item.id)); - if (selectedObject?.id === item.id) selectedObject = null; - hydrateRuntime(); - saveState(); - toast(`${asset?.name ?? 'Dynamic object'} removed.`); + const item = removeObjectLocally('dynamic', dynamicTarget.id); + if (item) { + if (!isSharedWorld()) { + recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('dynamic', item.id)); + saveState(); + } + toast(`${findAsset(item.assetId)?.name ?? 'Dynamic object'} removed.`); return; } } @@ -1613,39 +1666,36 @@ if (kind === 'static') { const index = state.placed.findIndex((p) => p.id === objectId); if (index >= 0) { + if (!canCurrentAccountModifyObject('static', state.placed[index])) return; if (isSharedWorld()) { - if (!canCurrentAccountModifyObject('static', state.placed[index])) return; queueSharedCommand(makeSharedCommand('object.delete', { kind: 'static', objectId })); return; } - const asset = findAsset(state.placed[index].assetId); - state.placed.splice(index, 1); - rebuildWorldIndex(); - recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('static', objectId)); - if (selectedObject?.id === objectId) selectedObject = null; - saveState(); - updateSelectionBubble(performance.now()); - toast(`${asset?.name ?? 'Object'} removed.`); + const item = removeObjectLocally('static', objectId); + if (!item) return; + if (!isSharedWorld()) { + recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('static', objectId)); + saveState(); + } + toast(`${findAsset(item.assetId)?.name ?? 'Object'} removed.`); return; } } if (kind === 'dynamic') { const index = state.dynamicSummons.findIndex((p) => p.id === objectId); if (index >= 0) { + if (!canCurrentAccountModifyObject('dynamic', state.dynamicSummons[index])) return; if (isSharedWorld()) { - if (!canCurrentAccountModifyObject('dynamic', state.dynamicSummons[index])) return; queueSharedCommand(makeSharedCommand('object.delete', { kind: 'dynamic', objectId })); return; } - const asset = findAsset(state.dynamicSummons[index].assetId); - state.dynamicSummons.splice(index, 1); - rebuildWorldIndex(); - recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('dynamic', objectId)); - if (selectedObject?.id === objectId) selectedObject = null; - hydrateRuntime(); - saveState(); - updateSelectionBubble(performance.now()); - toast(`${asset?.name ?? 'Dynamic object'} removed.`); + const item = removeObjectLocally('dynamic', objectId); + if (!item) return; + if (!isSharedWorld()) { + recordSyncEvent(Phase2Sync?.createObjectDeleteEvent?.('dynamic', objectId)); + saveState(); + } + toast(`${findAsset(item.assetId)?.name ?? 'Dynamic object'} removed.`); } } } @@ -2515,84 +2565,6 @@ }); } - function exportEditorPng() { - const scale = Math.max(8, Math.floor(512 / Math.max(editorWidth, editorHeight))); - const canvas = document.createElement('canvas'); - canvas.width = editorWidth * scale; - canvas.height = editorHeight * scale; - const c = canvas.getContext('2d'); - c.imageSmoothingEnabled = false; - const pixels = getDisplayEditorPixels(); - for (let y = 0; y < editorHeight; y++) { - for (let x = 0; x < editorWidth; x++) { - const code = pixels[y * editorSize + x]; - if (!code) continue; - c.fillStyle = colorToHex(code); - c.fillRect(x * scale, y * scale, scale, scale); - } - } - const link = document.createElement('a'); - const rawName = (els.assetName.value || 'pixel-art').trim().replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '') || 'pixel-art'; - link.download = `${rawName}-${editorWidth}x${editorHeight}.png`; - link.href = canvas.toDataURL('image/png'); - link.click(); - toast('PNG exported.'); - } - - function importEditorPng(event) { - const file = event.target.files?.[0]; - event.target.value = ''; - if (!file) return; - const img = new Image(); - img.onload = () => { - const target = chooseImportCanvasSize(img.naturalWidth || img.width, img.naturalHeight || img.height); - commitEditorMutation(() => { - const previousSize = editorSize; - const canvas = document.createElement('canvas'); - canvas.width = target.width; - canvas.height = target.height; - const c = canvas.getContext('2d', { willReadFrequently: true }); - c.imageSmoothingEnabled = false; - c.clearRect(0, 0, target.width, target.height); - c.drawImage(img, 0, 0, target.width, target.height); - const data = c.getImageData(0, 0, target.width, target.height).data; - const next = blankPixels(target.width, target.height); - for (let i = 0; i < target.width * target.height; i++) { - const alpha = data[i * 4 + 3]; - if (alpha < 32) continue; - const hex = rgbToHex(data[i * 4], data[i * 4 + 1], data[i * 4 + 2]); - next[i] = nearestPaletteCode(hex); - } - editorWidth = target.width; - editorHeight = target.height; - editorSize = Math.max(target.width, target.height); - updateDimensionInputs(); - setDisplayEditorPixels(next); - depthPixels = Array(editorSize * editorSize).fill(0); - lightPixels = []; - particlePixels = []; - doorPixel = { x: Math.min(doorPixel.x, target.width - 1), y: Math.min(doorPixel.y, target.height - 1) }; - editorSelection = null; - if (previousSize !== editorSize) resetEditorView(); - return true; - }); - URL.revokeObjectURL(img.src); - const note = (img.naturalWidth === target.width && img.naturalHeight === target.height) ? '' : ` (scaled from ${img.naturalWidth}×${img.naturalHeight})`; - toast(`PNG imported as ${target.width}×${target.height}${note}.`); - }; - img.onerror = () => toast('Could not read that PNG.'); - img.src = URL.createObjectURL(file); - } - - function chooseImportCanvasSize(width, height) { - const w = Math.max(1, Math.round(Number(width) || 1)); - const h = Math.max(1, Math.round(Number(height) || 1)); - const scale = Math.min(1, MAX_EDITOR_DIMENSION / Math.max(w, h)); - return { - width: clampDimension(Math.round(w * scale), Math.min(w, MAX_EDITOR_DIMENSION)), - height: clampDimension(Math.round(h * scale), Math.min(h, MAX_EDITOR_DIMENSION)) - }; - } function rgbToHex(r, g, b) { const part = (value) => clamp(Math.round(value), 0, 255).toString(16).padStart(2, '0'); @@ -2867,7 +2839,7 @@ else { const maxLights = lightBudgetForArea(editorWidth, editorHeight); if (lightPixels.length >= maxLights) { - toast(`Light limit: ${maxLights} for ${editorWidth}×${editorHeight} cells.`); + toast(`Light limit: ${maxLights} for ${editorWidth}テ・{editorHeight} cells.`); return; } lightPixels.push(point); @@ -3220,6 +3192,8 @@ if (els.placementPreviewBar) els.placementPreviewBar.hidden = true; if (els.openEditor) els.openEditor.hidden = false; placeSelected(x, y); + setMode('inspect'); + updateSelectionBubble(performance.now()); } function newAsset() { @@ -3306,7 +3280,7 @@ if (els.accountId) els.accountId.value = ''; if (els.authorName) els.authorName.value = state.authorName || 'Local Artist'; if (els.accountPass) els.accountPass.value = ''; - els.accountNote.textContent = 'Save + Place creates a local account for island publishing.'; + els.accountNote.textContent = 'Publish creates a local account for island publishing.'; if (els.createAccount) els.createAccount.hidden = false; updatePublishQuotaUI(); return; @@ -3352,43 +3326,126 @@ function renderLibrary() { els.assetList.innerHTML = ''; + if (els.likedCodex) { + els.likedCodex.innerHTML = ''; + els.likedCodex.hidden = true; + } const visibleAssets = state.assets.filter((asset) => !isModeratedAssetHidden(asset) && !state.hiddenAssets?.[asset.id]); + renderLibraryToolbar(visibleAssets); if (!visibleAssets.length) { - els.assetList.textContent = 'No visible assets.'; + renderCollectionTabs({ mine: [], others: [], favorite: [] }); + els.assetList.append(makeLibraryEmptyNote('No visible assets.')); renderHiddenAssets(); return; } - const myName = (state.authorName || 'Local Artist').trim() || 'Local Artist'; - const myAccountId = currentAccountId(); const matchesFilter = (asset) => libraryFilter === 'all' || subtypeToRole(asset) === libraryFilter; - const isMyAsset = (asset) => isSharedWorld() - ? normalizeOwnerAccountId(asset.ownerAccountId) === myAccountId - : (asset.author || 'Local Artist') === myName; - const mine = visibleAssets.filter((asset) => isMyAsset(asset) && matchesFilter(asset)); - const others = visibleAssets.filter((asset) => !isMyAsset(asset) && matchesFilter(asset)); + const matchesSearch = (asset) => { + const query = librarySearch.trim().toLowerCase(); + if (!query) return true; + return [asset.name, asset.author, asset.subtype, asset.category].some((value) => String(value || '').toLowerCase().includes(query)); + }; + const isMyAsset = (asset) => isCurrentUserAsset(asset); + const mine = visibleAssets.filter((asset) => isMyAsset(asset) && matchesFilter(asset) && matchesSearch(asset)); + const others = visibleAssets.filter((asset) => !isMyAsset(asset) && matchesFilter(asset) && matchesSearch(asset)); + const favorite = getLikedAssets().filter((asset) => !isModeratedAssetHidden(asset) && !state.hiddenAssets?.[asset.id] && matchesFilter(asset) && matchesSearch(asset)); + const groups = { mine, others, favorite }; + if (!groups[libraryViewTab]) libraryViewTab = 'mine'; - addLibrarySection('My works', mine); - addLibrarySection('Others', others); + renderCollectionTabs(groups); + if (libraryViewTab === 'mine') addLibrarySection('My works', mine, 'No assets in this tab.'); + else if (libraryViewTab === 'others') addLibrarySection('Others', others, 'No assets in this tab.'); + else addLibrarySection('Favorite', favorite, 'Works you upvote appear here.'); renderHiddenAssets(); - renderLikedCodex(); } - function addLibrarySection(title, assets) { - const heading = document.createElement('button'); - heading.type = 'button'; - heading.className = 'librarySectionTitle'; - heading.textContent = `${title} · ${libraryFilterLabel()}`; - heading.title = 'Click to filter by genre'; - heading.addEventListener('click', () => { - cycleLibraryFilter(); + function renderLibraryToolbar(visibleAssets) { + if (!els.assetList) return; + const toolbar = document.createElement('div'); + toolbar.className = 'collectionToolbar'; + const summary = document.createElement('div'); + summary.className = 'collectionSummary'; + const placedCount = visibleAssets.filter((asset) => state.placed.some((p) => p.assetId === asset.id) || state.dynamicSummons.some((p) => p.assetId === asset.id)).length; + summary.textContent = `${visibleAssets.length} works / ${placedCount} on island`; + toolbar.append(summary); + + const search = document.createElement('input'); + search.className = 'collectionSearch'; + search.type = 'search'; + search.placeholder = 'Search collection'; + search.value = librarySearch; + search.addEventListener('input', () => { + librarySearch = search.value || ''; + const caret = search.selectionStart || librarySearch.length; renderLibrary(); + requestAnimationFrame(() => { + const nextSearch = els.assetList?.querySelector('.collectionSearch'); + if (!nextSearch) return; + nextSearch.focus?.({ preventScroll: true }); + nextSearch.setSelectionRange?.(caret, caret); + }); }); + toolbar.append(search); + + const chips = document.createElement('div'); + chips.className = 'collectionFilters'; + for (const filter of ['all', 'human', 'animal', 'bird', 'fish', 'nature', 'building', 'ship', 'other']) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'collectionFilter'; + button.classList.toggle('active', libraryFilter === filter); + button.textContent = filter === 'all' ? 'All' : cap(filter); + button.addEventListener('click', () => { + const scrollTop = els.drawer?.querySelector('.drawerBody')?.scrollTop; + libraryFilter = filter; + renderLibrary(); + restoreDrawerScroll(scrollTop); + }); + chips.append(button); + } + toolbar.append(chips); + els.assetList.append(toolbar); + } + + function renderCollectionTabs(groups) { + const tabs = document.createElement('div'); + tabs.className = 'collectionViewTabs tabs'; + const labels = [ + ['mine', 'My works'], + ['others', 'Others'], + ['favorite', 'Favorite'] + ]; + for (const [key, label] of labels) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'tab collectionViewTab'; + button.classList.toggle('active', libraryViewTab === key); + button.textContent = `${label} ${groups[key]?.length ?? 0}`; + button.addEventListener('click', () => { + const scrollTop = els.drawer?.querySelector('.drawerBody')?.scrollTop; + libraryViewTab = key; + renderLibrary(); + restoreDrawerScroll(scrollTop); + }); + tabs.append(button); + } + els.assetList.append(tabs); + } + + function makeLibraryEmptyNote(text) { + const empty = document.createElement('div'); + empty.className = 'libraryEmptyNote'; + empty.textContent = text; + return empty; + } + + function addLibrarySection(title, assets, emptyText = 'No assets in this genre.') { + const heading = document.createElement('div'); + heading.className = 'librarySectionTitle'; + heading.textContent = `${title} (${assets.length})`; + heading.title = ''; els.assetList.append(heading); if (!assets.length) { - const empty = document.createElement('div'); - empty.className = 'libraryEmptyNote'; - empty.textContent = 'No assets in this genre.'; - els.assetList.append(empty); + els.assetList.append(makeLibraryEmptyNote(emptyText)); return; } const grid = document.createElement('div'); @@ -3397,15 +3454,11 @@ els.assetList.append(grid); } - function libraryFilterLabel() { - return libraryFilter === 'all' ? 'All' : cap(libraryFilter); - } - - function cycleLibraryFilter() { - const filters = ['all', 'human', 'animal', 'nature', 'building', 'ship', 'other']; - const index = filters.indexOf(libraryFilter); - libraryFilter = filters[(index + 1) % filters.length]; - toast(`Collection filter: ${libraryFilterLabel()}`); + function restoreDrawerScroll(scrollTop) { + const scroller = els.drawer?.querySelector('.drawerBody'); + if (!scroller || !Number.isFinite(scrollTop)) return; + scroller.scrollTop = scrollTop; + requestAnimationFrame(() => { scroller.scrollTop = scrollTop; }); } function makeAssetCard(asset) { @@ -3424,37 +3477,48 @@ const title = document.createElement('strong'); title.textContent = asset.name; meta.append(title); + const isPlaced = state.placed.some((p) => p.assetId === asset.id) || state.dynamicSummons.some((p) => p.assetId === asset.id); + const statusRow = document.createElement('div'); + statusRow.className = 'assetStatusRow'; + const roleTag = document.createElement('span'); + roleTag.className = 'assetStatus role'; + roleTag.textContent = cap(subtypeToRole(asset)); + const placeTag = document.createElement('span'); + placeTag.className = `assetStatus ${isPlaced ? 'placed' : 'stored'}`; + placeTag.textContent = isPlaced ? 'On island' : 'Stored'; + statusRow.append(roleTag, placeTag); + meta.append(statusRow); card.addEventListener('click', () => { + const scrollTop = els.drawer?.querySelector('.drawerBody')?.scrollTop; selectedAssetId = expanded ? null : asset.id; updateSelectedLabel(); renderLibrary(); + restoreDrawerScroll(scrollTop); if (selectedAssetId) focusAssetInWorld(asset); }); if (expanded) { const lineage = asset.parentAssetId ? ' / derivative' : ''; const span1 = document.createElement('span'); - span1.textContent = `${displayCategory(asset)} / ${cap(asset.subtype)} / ${assetWidth(asset)}×${assetHeight(asset)}${lineage}`; + span1.textContent = `${displayCategory(asset)} / ${cap(asset.subtype)} / ${assetWidth(asset)}テ・{assetHeight(asset)}${lineage}`; const span2 = document.createElement('span'); - span2.textContent = `Author: ${asset.author || 'Local Artist'} · Remixed: ${getRemixCount(asset.id)}`; + span2.textContent = `Author: ${displayAssetAuthor(asset)} ツキ Remixed: ${getRemixCount(asset.id)}`; meta.append(span1, span2); const actions = document.createElement('div'); actions.className = 'assetActions'; const assetVotes = getAssetVoteCounts(asset.id); const assetPreviousVote = assetVotes.voters?.[currentVoterKey()] || 0; - const up = makeButton(`▲ ${assetVotes.up}`, (event) => { event.stopPropagation(); voteAsset(asset.id, 1); }); - const down = makeButton(`▼ ${assetVotes.down}`, (event) => { event.stopPropagation(); voteAsset(asset.id, -1); }); + const up = makeButton(`笆イ ${assetVotes.up}`, (event) => { event.stopPropagation(); voteAsset(asset.id, 1); }); + const down = makeButton(`笆シ ${assetVotes.down}`, (event) => { event.stopPropagation(); voteAsset(asset.id, -1); }); up.classList.toggle('activeVote', assetPreviousVote > 0); down.classList.toggle('activeVote', assetPreviousVote < 0); up.classList.toggle('mutedVote', assetPreviousVote < 0); down.classList.toggle('mutedVote', assetPreviousVote > 0); const hide = makeButton('Hide', (event) => { event.stopPropagation(); hideAsset(asset.id); }); hide.classList.toggle('mutedAction', assetPreviousVote >= 0); - const isMine = isSharedWorld() - ? normalizeOwnerAccountId(asset.ownerAccountId) === currentAccountId() - : (asset.author || 'Local Artist') === (state.authorName || 'Local Artist'); + const isMine = isCurrentUserAsset(asset); const remix = makeButton('Remix', (event) => { event?.stopPropagation?.(); remixEdit(asset); }); const edit = isMine ? makeButton('Edit', (event) => { event?.stopPropagation?.(); editOriginalAsset(asset); }) : null; const move = isMine ? makeButton('Place/Move', (event) => { @@ -3468,16 +3532,23 @@ }) : null; const del = makeButton('Delete', (event) => { event?.stopPropagation?.(); deleteAsset(asset); }); del.classList.add('danger'); - const canDeleteAny = TEMP_ALLOW_DELETE_ALL_WORKS || isMine; actions.append(up, down, hide, remix); if (edit) actions.append(edit); if (move) actions.append(move); - if (canDeleteAny) actions.append(del); + if (isMine) actions.append(del); meta.append(actions); } else { const mini = document.createElement('span'); - mini.textContent = `${cap(asset.subtype)} · ${assetWidth(asset)}×${assetHeight(asset)}`; + mini.textContent = `${cap(asset.subtype)} ツキ ${assetWidth(asset)}テ・{assetHeight(asset)}`; meta.append(mini); + if (isCurrentUserAsset(asset)) { + const quickActions = document.createElement('div'); + quickActions.className = 'assetActions'; + const del = makeButton('Delete', (event) => { event?.stopPropagation?.(); deleteAsset(asset); }); + del.classList.add('danger'); + quickActions.append(del); + meta.append(quickActions); + } } card.append(preview, meta); @@ -3485,8 +3556,7 @@ } - function renderLikedCodex() { - if (!els.likedCodex) return; + function getLikedAssets() { const voter = currentVoterKey(); const likedIds = new Set(); for (const [assetId, votes] of Object.entries(state.assetVotes || {})) { @@ -3498,7 +3568,12 @@ if (obj?.assetId) likedIds.add(obj.assetId); } } - const liked = [...likedIds].map(findAsset).filter(Boolean).slice(0, 12); + return [...likedIds].map(findAsset).filter(Boolean); + } + + function renderLikedCodex() { + if (!els.likedCodex) return; + const liked = getLikedAssets().slice(0, 12); if (!liked.length) { els.likedCodex.innerHTML = '
Favorite
Works you upvote appear here.
'; return; @@ -3624,7 +3699,7 @@ drawPreview(preview, asset); const meta = document.createElement('div'); meta.className = 'assetMeta'; - meta.innerHTML = `Author: ${asset.author || 'Local Artist'}`; + meta.innerHTML = `Author: ${displayAssetAuthor(asset)}`; meta.querySelector('strong').textContent = asset.name; const actions = document.createElement('div'); actions.className = 'assetActions'; @@ -3667,7 +3742,8 @@ function renderPalette() { if (!els.paletteGrid) return; els.paletteGrid.innerHTML = ''; - for (const entry of PALETTE) { + const visiblePalette = advancedDraw ? PALETTE : PALETTE.slice(0, BASIC_PALETTE_COUNT); + for (const entry of visiblePalette) { const swatch = document.createElement('button'); swatch.type = 'button'; swatch.className = `paletteSwatch${entry.code === selectedColorCode ? ' active' : ''}`; @@ -3722,7 +3798,7 @@ setupEditor(aw, right, null, depthPixels, ah); clearEditorHistory(); refreshCategoryUI(); - els.lineageNote.textContent = editExisting ? `Editing “${asset.name}”. Save updates this collection work.` : `Remixing “${asset.name}”. Save creates a separate new collection work.`; + els.lineageNote.textContent = editExisting ? `Editing 窶・{asset.name}窶・ Save updates this collection work.` : `Remixing 窶・{asset.name}窶・ Save creates a separate new collection work.`; } function editOriginalAsset(asset) { @@ -3737,45 +3813,122 @@ remixEdit(asset); } + function assetMatchesDeleteTarget(candidate, target) { + if (!candidate || !target) return false; + if (candidate.id === target.id) return true; + if (target.contentHash && candidate.contentHash === target.contentHash) return true; + if ((candidate.name || '') === (target.name || '') + && (candidate.category || '') === (target.category || '') + && (candidate.subtype || '') === (target.subtype || '') + && assetWidth(candidate) === assetWidth(target) + && assetHeight(candidate) === assetHeight(target)) return true; + return false; + } - function deleteAsset(asset) { + function getDefaultGalleryAssetNames() { + if (!defaultGalleryAssetNames) { + defaultGalleryAssetNames = new Set(buildDefaultGalleryPack(Date.now()).assets.map((asset) => asset.name).filter(Boolean)); + } + return defaultGalleryAssetNames; + } + + function isSampleGalleryAsset(asset) { + if (!asset) return false; + return asset.meta?.galleryOrigin === 'default-gallery' || getDefaultGalleryAssetNames().has(asset.name || ''); + } + + async function deleteAsset(asset) { if (!asset) return; - if (isSharedWorld()) { - if (!canCurrentAccountDeleteAsset(asset)) return; - queueSharedCommand(makeSharedCommand('asset.delete', { assetId: asset.id })); + const deletedAssetIds = new Set([asset.id]); + await deleteAssetIds(deletedAssetIds, `"${asset.name}"`, asset); + } + + async function deleteAssetIds(deletedAssetIds, label = 'selected works', permissionAsset = null) { + deletedAssetIds = new Set([...deletedAssetIds].filter(Boolean)); + if (!deletedAssetIds.size) return; + const assetsToDelete = (state.assets || []).filter((candidate) => deletedAssetIds.has(candidate.id)); + if (!assetsToDelete.length) return; + if (assetsToDelete.some((candidate) => !canCurrentAccountDeleteAsset(candidate, false))) { + canCurrentAccountDeleteAsset(assetsToDelete.find((candidate) => !canCurrentAccountDeleteAsset(candidate, false)), true); return; } - const used = state.placed.some((p) => p.assetId === asset.id) || state.dynamicSummons.some((p) => p.assetId === asset.id); + const used = state.placed.some((p) => deletedAssetIds.has(p.assetId)) || state.dynamicSummons.some((p) => deletedAssetIds.has(p.assetId)); const usageText = used ? ' Island placements that use it will also be removed.' : ''; - if (!confirm(`Delete “${asset.name}”?${usageText}`)) return; - + const confirmed = await confirmWebsiteDialog({ + title: 'Delete work', + message: `Delete ${label}?${usageText}`, + okText: 'Delete', + danger: true + }); + if (!confirmed) return; + const sharedDelete = isSharedWorld(); + if (sharedDelete) { + for (const assetId of deletedAssetIds) queueSharedCommand(makeSharedCommand('asset.delete', { assetId }), { toast: false }); + toast('Delete queued for server validation.'); + } const removedObjectIds = new Set(); - for (const placed of state.placed || []) if (placed.assetId === asset.id) removedObjectIds.add(placed.id); - for (const summon of state.dynamicSummons || []) if (summon.assetId === asset.id) removedObjectIds.add(summon.id); + const removedObjectAssetIds = new Map(); + for (const placed of state.placed || []) { + if (!deletedAssetIds.has(placed.assetId)) continue; + removedObjectIds.add(placed.id); + removedObjectAssetIds.set(placed.id, placed.assetId); + } + for (const summon of state.dynamicSummons || []) { + if (!deletedAssetIds.has(summon.assetId)) continue; + removedObjectIds.add(summon.id); + removedObjectAssetIds.set(summon.id, summon.assetId); + } + ensureWorldProtectionState(); + for (const assetId of deletedAssetIds) { + const deleted = state.assets.find((candidate) => candidate.id === assetId) || permissionAsset || assetsToDelete[0]; + state.tombstones.assets[assetId] = { + id: assetId, + name: deleted.name || '', + deletedAt: Date.now(), + deletedBy: currentAccountId() || 'local', + ownerAccountId: normalizeOwnerAccountId(deleted.ownerAccountId), + author: deleted.author || '', + version: Number(deleted.version) || 1 + }; + const deletedName = deleted.name || ''; + if (deletedName && !state.deletedSeedAssetNames.includes(deletedName)) state.deletedSeedAssetNames.push(deletedName); + } + for (const id of removedObjectIds) { + state.tombstones.objects[id] = { + id, + assetId: removedObjectAssetIds.get(id) || permissionAsset?.id || assetsToDelete[0]?.id || '', + deletedAt: Date.now(), + deletedBy: currentAccountId() || 'local' + }; + } - state.assets = state.assets.filter((a) => a.id !== asset.id); - state.placed = state.placed.filter((p) => p.assetId !== asset.id); - state.dynamicSummons = state.dynamicSummons.filter((p) => p.assetId !== asset.id); + state.assets = state.assets.filter((a) => !deletedAssetIds.has(a.id)); + state.placed = state.placed.filter((p) => !deletedAssetIds.has(p.assetId)); + state.dynamicSummons = state.dynamicSummons.filter((p) => !deletedAssetIds.has(p.assetId)); - delete state.assetVotes?.[asset.id]; - delete state.hiddenAssets?.[asset.id]; + for (const assetId of deletedAssetIds) { + delete state.assetVotes?.[assetId]; + delete state.hiddenAssets?.[assetId]; + } for (const id of removedObjectIds) { delete state.objectVotes?.[id]; delete state.hiddenObjects?.[id]; } state.moderationReports = (state.moderationReports || []).filter((report) => !removedObjectIds.has(report.objectId)); - if (selectedAssetId === asset.id) selectedAssetId = state.assets[0]?.id ?? null; - if (selectedObject && (selectedObject.assetId === asset.id || removedObjectIds.has(selectedObject.id))) selectedObject = null; + if (deletedAssetIds.has(selectedAssetId)) selectedAssetId = state.assets[0]?.id ?? null; + if (selectedObject && (deletedAssetIds.has(selectedObject.assetId) || removedObjectIds.has(selectedObject.id))) selectedObject = null; - recordSyncEvent(Phase2Sync?.createAssetDeleteEvent?.(asset.id)); + if (!sharedDelete) { + for (const assetId of deletedAssetIds) recordSyncEvent(Phase2Sync?.createAssetDeleteEvent?.(assetId)); + } saveState(); spriteCache.clear(); hydrateRuntime(); renderLibrary(); updateSelectedLabel(); updateSelectionBubble(performance.now()); - toast(`${asset.name} deleted.`); + toast(`${label} deleted.`); } function hydrateRuntime() { @@ -4004,6 +4157,7 @@ lastRuntimeUpdate = performance.now(); dynamicLogicRemainder = 0; render(lastRuntimeUpdate); + lastRenderAt = lastRuntimeUpdate; scheduleFrame(); } @@ -4024,7 +4178,10 @@ } if (steps >= MAX_DYNAMIC_STEPS_PER_FRAME) dynamicLogicRemainder = 0; updateClock(); - render(time); + if (time - lastRenderAt >= TARGET_RENDER_FRAME_MS) { + render(time); + lastRenderAt = time; + } scheduleFrame(); } @@ -4036,7 +4193,7 @@ if (els.analogClock) { els.analogClock.style.setProperty('--clock-rotate', `${degrees.toFixed(2)}deg`); els.analogClock.setAttribute('aria-label', `${phase.label} island clock`); - els.analogClock.title = `${phase.label} · 10 min = 1 island day`; + els.analogClock.title = `${phase.label} ツキ 10 min = 1 island day`; } const second = Math.floor(getAuthoritativeNow() / 1000); if (second !== lastClockSecond) { @@ -4265,22 +4422,14 @@ ctx.translate(view.x, view.y); ctx.scale(view.zoom, view.zoom); ctx.imageSmoothingEnabled = false; - drawTerrainCache(terrainCache); lastRenderedSpriteInfo.clear(); - const lightSources = []; const visibleItems = getDrawableItems(time, getViewportWorldRect(VIEW_CULL_MARGIN)); const activeLights = collectLightSourcesForItems(visibleItems, time, phase); - if (lightSources && activeLights.length) lightSources.push(...activeLights); - if (beginProjectedShadowLayer(phase)) { - drawTerrainProjectedShadows(phase); - drawObjectProjectedShadows(visibleItems, time, phase); - endProjectedShadowLayer(phase); - } else { - drawTerrainProjectedShadows(phase); - drawObjectProjectedShadows(visibleItems, time, phase); - } + const lightSources = [...activeLights]; + drawTerrainCache(terrainCache); + drawTerrainProjectedShadows(phase); + drawObjectsFromItems(visibleItems, time, activeLights, phase, true); drawHoverTile(); - drawObjectsFromItems(visibleItems, time, activeLights, phase, false); if (visualSettings().enableParticles) { spawnConfiguredParticleEmitters(visibleItems, time); drawNatureDriftParticles(time); @@ -4373,7 +4522,7 @@ const pulse = 0.9 + Math.sin(time / 420) * 0.1; const radius = Math.max(28, 54 * view.zoom) * pulse; ctx.save(); - ctx.globalCompositeOperation = 'lighter'; + ctx.globalCompositeOperation = 'screen'; const glow = ctx.createRadialGradient(sx, sy, 0, sx, sy, radius); glow.addColorStop(0, 'rgba(198, 236, 255, .16)'); glow.addColorStop(0.22, 'rgba(170, 225, 255, .09)'); @@ -4423,11 +4572,20 @@ } } - function drawObjectProjectedShadows(items, time, phase) { - for (const item of items) { - if (item.asset.category === 'dynamic' && item.asset.subtype === 'fish') continue; - drawSpriteShadow(getSpriteDrawInfo(item, time, true), phase); - } + function getHighlandWallFaceEntries(tile, worldData) { + if (!tile || tile.type !== 'highland') return []; + const lift = getTileLift(tile); + const out = []; + const add = (side, nx, ny) => { + const neighbor = worldData.get(nx, ny); + if (neighbor && getTileLift(neighbor) >= lift) return; + const p = tileToWorld(nx, ny); + const neighborSurfaceDepth = neighbor ? p.y - getTileLift(neighbor) : tileToWorld(tile.x, tile.y).y + TILE_H / 2; + out.push({ side, depth: neighborSurfaceDepth - 0.06 }); + }; + add('left', tile.x, tile.y + 1); + add('right', tile.x + 1, tile.y); + return out; } function getSpriteDrawInfo(item, time, includeSelectBounce = true) { @@ -4481,17 +4639,17 @@ } if (includeSelectBounce && selectedObject?.id === item.source?.id) { - const selectedT = clamp((time - (selectedObject.selectedAt || 0)) / 560, 0, 1); - if (selectedT < 1) { - const bounce = Math.sin(selectedT * Math.PI); + const selectionElapsed = time - (selectedObject.selectedAt || 0); + const selectedT = clamp(selectionElapsed / 560, 0, 1); + if (selectionElapsed < 700) { + const bounce = selectedT < 1 ? Math.sin(selectedT * Math.PI) : 0; bob += -bounce * (asset.category === 'dynamic' ? 9 : 8); const tiltSeed = parseInt(fnv1a(`${item.source?.id || asset.id}:select`).slice(0, 6), 16) || 1; const tiltRand = pseudoNoise(tiltSeed * 0.013) - 0.5; angle += tiltRand * 0.32 * bounce; - // Selection landing squash/stretch: keep it tied to the visible landing moment. - // The previous delayed timer placed the squash after the bounce had already ended, - // so it looked like the aspect-ratio animation had disappeared. - const land = Math.exp(-Math.pow((selectedT - 0.88) / 0.085, 2)); + // Squash/stretch lands later than the vertical bounce peak. + const landT = clamp((selectionElapsed - 300) / 560, 0, 1); + const land = Math.exp(-Math.pow((landT - 0.88) / 0.085, 2)); stretchX += land * 0.30; stretchY -= land * 0.21; } @@ -4669,37 +4827,42 @@ for (let y = 0; y < ah; y++) { for (let x = 0; x < aw; x++) { - if (!pixels[y * aw + x]) continue; + const pixelCode = pixels[y * aw + x]; + if (!pixelCode) continue; const depth = depths[y * aw + x] || 0; if (!depth) continue; + const baseRgb = parseHex(colorToHex(pixelCode)); let rr = 0, gg = 0, bb = 0, aa = 0; for (const { source, local } of sources) { + if (source.ownerId && source.ownerId === (item.source?.id || item.asset?.id)) continue; const lc = parseHex(source.color || '#ffd86a'); if (!lc) continue; const cellPx = (x + 0.5) * scale; const cellPy = (y + 0.5) * scale; const d = Math.hypot(local.x - cellPx, local.y - cellPy); - const reach = Math.max(scale * 2.2, source.radius * 3.8); + const reach = Math.max(scale * 2.1, source.radius * 2.35); const t = 1 - clamp(d / reach, 0, 1); if (t <= 0) continue; const edge = getDepthLightFacing(depth, x, y, aw, pixels, depths, local.x / scale - 0.5, local.y / scale - 0.5, ah); - const depthFactor = depth > 0 ? 0.98 + edge * 0.42 : 0.74 + edge * 0.22; - const sourceIntensity = clamp(Number(source.intensity ?? 1) || 1, 0.25, 1.75); - // Make weak light subtle and strong light visibly snap on, instead of a flat linear ramp. - const exponential = (Math.exp(3.15 * t * sourceIntensity) - 1) / (Math.exp(3.15 * sourceIntensity) - 1); - const amount = exponential * depthFactor * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER; - rr += lc.r * amount; - gg += lc.g * amount; - bb += lc.b * amount; + const depthFactor = depth > 0 ? 0.28 + edge * 0.36 : 0.18 + edge * 0.16; + const sourceIntensity = clamp(Number(source.intensity ?? 1) || 1, 0.18, 1.15); + const falloff = Math.pow(t, 2.2) * sourceIntensity; + const amount = falloff * depthFactor; + const br = baseRgb?.r ?? lc.r; + const bg = baseRgb?.g ?? lc.g; + const bb0 = baseRgb?.b ?? lc.b; + rr += (lc.r * 0.82 + br * 0.18) * amount; + gg += (lc.g * 0.82 + bg * 0.18) * amount; + bb += (lc.b * 0.82 + bb0 * 0.18) * amount; aa += amount; } if (aa <= 0.002) continue; const inv = 1 / aa; - const alpha = Math.min(0.86, 0.10 + Math.pow(aa, 1.05) * 0.42); + const alpha = Math.min(0.13, 0.012 + Math.pow(aa, 1.08) * 0.055); octx.fillStyle = `rgba(${Math.round(rr * inv)}, ${Math.round(gg * inv)}, ${Math.round(bb * inv)}, ${alpha.toFixed(3)})`; octx.fillRect(x * scale, y * scale, scale, scale); - if (aa > 0.42 && scale >= 4) { - octx.fillStyle = `rgba(${Math.round(rr * inv)}, ${Math.round(gg * inv)}, ${Math.round(bb * inv)}, ${(alpha * 0.34).toFixed(3)})`; + if (aa > 0.82 && scale >= 4) { + octx.fillStyle = `rgba(${Math.round(rr * inv)}, ${Math.round(gg * inv)}, ${Math.round(bb * inv)}, ${(alpha * 0.18).toFixed(3)})`; octx.fillRect(x * scale + 1, y * scale + 1, Math.max(1, scale - 2), Math.max(1, scale - 2)); } } @@ -4707,7 +4870,7 @@ ctx.save(); ctx.globalAlpha = 1; - ctx.globalCompositeOperation = 'lighter'; + ctx.globalCompositeOperation = 'screen'; if (info.angle) { ctx.translate(Math.round(info.drawX + sprite.width / 2), Math.round(info.drawY + sprite.height * 0.8)); ctx.rotate(info.angle); @@ -5084,7 +5247,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return; } els.bubbleName.textContent = asset.name || 'Untitled'; - els.bubbleAuthor.textContent = `by ${asset.author || 'Local Artist'}`; + els.bubbleAuthor.textContent = `by ${displayAssetAuthor(asset)}`; const parent = asset.parentAssetId ? findAsset(asset.parentAssetId) : null; const remixSource = findRemixSourceObject(asset); if (els.bubbleRemixFrom) { @@ -5106,10 +5269,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { els.voteDown.classList.toggle('mutedVote', previous > 0); if (els.bubbleHide) els.bubbleHide.hidden = previous >= 0; if (els.bubbleEdit) { - const mine = isSharedWorld() - ? normalizeOwnerAccountId(asset.ownerAccountId) === currentAccountId() - : (asset.author || 'Local Artist') === (state.authorName || 'Local Artist'); - els.bubbleEdit.hidden = !mine; + els.bubbleEdit.hidden = !isCurrentUserAsset(asset); } if (els.bubbleReport) { const reported = hasReportForObject(objectId); @@ -5177,9 +5337,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { if (!selectedObject) return; const asset = findAsset(selectedObject.assetId); if (!asset) return; - const mine = isSharedWorld() - ? normalizeOwnerAccountId(asset.ownerAccountId) === currentAccountId() - : (asset.author || 'Local Artist') === (state.authorName || 'Local Artist'); + const mine = isCurrentUserAsset(asset); if (!mine) { toast('Only your own assets can be edited directly. Use Remix instead.'); return; @@ -5231,6 +5389,28 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { if (els.reportDialog) els.reportDialog.hidden = true; } + let pendingConfirmResolve = null; + + function confirmWebsiteDialog({ title = 'Confirm', message = 'Are you sure?', okText = 'OK', danger = false } = {}) { + if (!els.confirmDialog || !els.confirmOk || !els.confirmCancel) return Promise.resolve(false); + if (els.confirmTitle) els.confirmTitle.textContent = title; + if (els.confirmMessage) els.confirmMessage.textContent = message; + els.confirmOk.textContent = okText; + els.confirmOk.classList.toggle('danger', danger); + els.confirmDialog.hidden = false; + els.confirmOk.focus?.({ preventScroll: true }); + return new Promise((resolve) => { + pendingConfirmResolve = resolve; + }); + } + + function resolveConfirmDialog(confirmed) { + if (els.confirmDialog) els.confirmDialog.hidden = true; + const resolve = pendingConfirmResolve; + pendingConfirmResolve = null; + resolve?.(Boolean(confirmed)); + } + function submitReportDialog() { const reason = els.reportReason?.value || 'local-report'; submitReport(reason); @@ -5501,8 +5681,8 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { c.translate(-bounds.x, -bounds.y); const orderedTiles = tiles.slice().sort(terrainDrawCompare); for (const tile of orderedTiles) { - if (tile.type === 'highland') drawHighlandWallFaces(c, tile, worldData); drawTerrainTile(c, tile, worldData); + if (tile.type === 'highland') drawHighlandWallFaces(c, tile, worldData); } c.restore(); chunks.push({ canvas, x: bounds.x, y: bounds.y, w: canvas.width, h: canvas.height, tileX: cx, tileY: cy }); @@ -5557,6 +5737,24 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return { x: minX, y: minY, w: Math.max(1, maxX - minX), h: Math.max(1, maxY - minY) }; } + function drawHighlandForegroundFaces(worldData, items = []) { + const rect = getViewportWorldRect(64); + const tiles = []; + for (let y = 0; y < WORLD_H; y++) { + for (let x = 0; x < WORLD_W; x++) { + const tile = worldData.get(x, y); + if (!tile || tile.type !== 'highland') continue; + const p = tileToWorld(x, y); + if (p.x + TILE_W < rect.left || p.x - TILE_W > rect.right || p.y + TILE_H < rect.top || p.y - 40 > rect.bottom) continue; + const frontLeft = worldData.get(x, y + 1); + const frontRight = worldData.get(x + 1, y); + if (!frontLeft || frontLeft.type !== 'highland' || !frontRight || frontRight.type !== 'highland') tiles.push(tile); + } + } + tiles.sort(terrainDrawCompare); + for (const tile of tiles) drawHighlandWallFaces(ctx, tile, worldData, true); + } + function drawTerrainCache(cache) { const rect = getViewportWorldRect(64); for (const chunk of cache.chunks) { @@ -5673,14 +5871,14 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { // Terrain AO linework removed: it produced horizontal artifacts on highland tiles. } - function drawHighlandWallFaces(c, tile, worldData) { + function drawHighlandWallFaces(c, tile, worldData, foreground = false) { + for (const face of getHighlandWallFaceEntries(tile, worldData)) drawHighlandWallFace(c, tile, worldData, face.side); + } + + function drawHighlandWallFace(c, tile, worldData, side) { const { x, y } = tileToWorld(tile.x, tile.y); const lift = getTileLift(tile); - const frontLeft = worldData.get(tile.x, tile.y + 1); - const frontRight = worldData.get(tile.x + 1, tile.y); - const leftLower = !frontLeft || frontLeft.type !== 'highland'; - const rightLower = !frontRight || frontRight.type !== 'highland'; - if (rightLower) { + if (side === 'right') { c.fillStyle = '#7f8b66'; c.beginPath(); c.moveTo(x, y + TILE_H - lift); @@ -5689,8 +5887,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { c.lineTo(x, y + TILE_H); c.closePath(); c.fill(); - } - if (leftLower) { + } else if (side === 'left') { c.fillStyle = '#6f7d5d'; c.beginPath(); c.moveTo(x - TILE_W / 2, y + TILE_H / 2 - lift); @@ -5747,7 +5944,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { // v16 reset local art because the palette code mapping and seed artwork were intentionally rebuilt. // v17 expanded the default gallery. v18 added more nature-themed defaults. v19 added giant showcase objects. v20 added more animals, fish, and birds. v21 added human and artificial-object defaults. v22 widened small-sprite hitboxes and fixed night cursor depth light. v23 strengthens multi-light depth response and upgrades lower-quality seed art while preserving v16+ user state. v24 sharpens near-light depth reflection falloff and expands the outlined default gallery. v25 fixes gray local-light wash, brings the selection bubble closer to sprite tops, and adds large heritage/masterpiece default works. v26 removes the single-light gray bloom regression, smooths cursor-light falloff, and removes the unwanted blue cursor halo artifact. v27 reduces gray bloom from asset-mounted lights, fixes the clock-hand offset, temporarily enables delete-for-all works, and expands the default gallery again. if (loadedSchema < 16) return seedState(); - return mergeDefaultGallery(normalizeState(expanded)); + return normalizeState(expanded); } } } catch (error) { @@ -5760,6 +5957,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { state.schema = SAVE_SCHEMA; ensureWorldProtectionState(); canonicalizeAssetStorage(); + if (state.worldMode !== 'shared') adoptLocalCollectionOwnership(state); state.guardrails = { ...PHASE5_GUARDRAILS, ...(state.guardrails || {}) }; state.moderationReports = normalizeModerationReports(state.moderationReports || []); state.settings = { ...defaultVisualSettings(), ...(state.settings || {}) }; @@ -5767,8 +5965,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { const payload = Phase2Sync?.compactState ? Phase2Sync.compactState(state) : state; localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); cachePhase2State(); - updateSyncStats(); - updateGuardrailStats(); updateRotationStats(); } @@ -5788,7 +5984,52 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return out; } + function assetUnsupportedPaletteCodes(asset) { + const unsupported = new Set(); + const scanEncodedPixels = (value) => { + if (typeof value !== 'string' || value === 'mirror') return; + for (const ch of value) { + if (ch !== '.' && !PALETTE_BY_CODE[ch]) unsupported.add(ch); + } + }; + scanEncodedPixels(asset?.pixels); + if (asset?.faces && typeof asset.faces === 'object') { + scanEncodedPixels(asset.faces.right); + scanEncodedPixels(asset.faces.left); + } + const meta = asset?.meta || {}; + for (const point of meta.lightPixels || []) { + if (typeof point?.c === 'string' && point.c.length === 1 && !PALETTE_BY_CODE[point.c]) unsupported.add(point.c); + } + for (const point of meta.particlePixels || []) { + if (typeof point?.c === 'string' && point.c.length === 1 && !PALETTE_BY_CODE[point.c]) unsupported.add(point.c); + } + return unsupported; + } + + function removeUnrepresentableAssets(input) { + if (!input || !Array.isArray(input.assets)) return input; + const removedIds = new Set(); + const removedNames = []; + const assets = input.assets.filter((asset) => { + const unsupported = assetUnsupportedPaletteCodes(asset); + if (!unsupported.size) return true; + if (asset?.id) removedIds.add(asset.id); + removedNames.push(`${asset?.name || 'Untitled'} [${[...unsupported].join(' ')}]`); + return false; + }); + if (!removedIds.size) return input; + console.warn(`Removed ${removedIds.size} collection work(s) with palette codes unavailable in the current palette: ${removedNames.join(', ')}`); + const next = { ...input, assets }; + next.placed = Array.isArray(input.placed) ? input.placed.filter((item) => !removedIds.has(item?.assetId)) : input.placed; + next.dynamicSummons = Array.isArray(input.dynamicSummons) ? input.dynamicSummons.filter((item) => !removedIds.has(item?.assetId)) : input.dynamicSummons; + next.assetVotes = Object.fromEntries(Object.entries(input.assetVotes || {}).filter(([id]) => !removedIds.has(id))); + next.hiddenAssets = Object.fromEntries(Object.entries(input.hiddenAssets || {}).filter(([id]) => !removedIds.has(id))); + return next; + } + function normalizeState(input) { + input = removeUnrepresentableAssets(input); const normalized = { schema: SAVE_SCHEMA, authorName: input.authorName || 'Local Artist', @@ -5808,18 +6049,32 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { publishLog: Array.isArray(input.publishLog) ? input.publishLog.slice(-300) : [], worldMode: input.worldMode === 'shared' ? 'shared' : 'local', serverSync: input.serverSync || { lastServerEventId: null, pendingCommands: [] }, - tombstones: input.tombstones || { assets: {}, objects: {} } + tombstones: input.tombstones || { assets: {}, objects: {} }, + deletedSeedAssetNames: Array.isArray(input.deletedSeedAssetNames) ? input.deletedSeedAssetNames : [] }; ensureWorldProtectionState(normalized); + for (const tombstone of Object.values(normalized.tombstones.assets || {})) { + if (tombstone?.name && !normalized.deletedSeedAssetNames.includes(tombstone.name)) normalized.deletedSeedAssetNames.push(tombstone.name); + } const ownerByAssetId = new Map(normalized.assets.map((asset) => [asset.id, normalizeOwnerAccountId(asset.ownerAccountId)])); normalized.placed.forEach((item) => { item.ownerAccountId = normalizeOwnerAccountId(item.ownerAccountId, ownerByAssetId.get(item.assetId) || ''); }); normalized.dynamicSummons.forEach((item) => { item.ownerAccountId = normalizeOwnerAccountId(item.ownerAccountId, ownerByAssetId.get(item.assetId) || ''); }); normalized.assets = normalized.assets.filter((asset) => !normalized.tombstones.assets?.[asset.id]); normalized.placed = normalized.placed.filter((item) => !normalized.tombstones.objects?.[item.id]); normalized.dynamicSummons = normalized.dynamicSummons.filter((item) => !normalized.tombstones.objects?.[item.id]); + normalized.placed = repairStaticPlacementTerrain(normalized.placed, normalized.assets); + if (normalized.worldMode !== 'shared') adoptLocalCollectionOwnership(normalized); return normalized; } + function adoptLocalCollectionOwnership(target) { + const author = target.authorName || 'Local Artist'; + target.assets = (target.assets || []).map((asset) => ({ ...asset, author, ownerAccountId: '' })); + target.placed = (target.placed || []).map((item) => ({ ...item, ownerAccountId: '' })); + target.dynamicSummons = (target.dynamicSummons || []).map((item) => ({ ...item, ownerAccountId: '' })); + return target; + } + function canonicalizeAssetStorage() { let changed = false; state.assets = (state.assets || []).map((asset) => { @@ -5874,7 +6129,9 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { .map((p) => ({ x: clampInt(p.x, 0, width - 1, 0), y: clampInt(p.y, 0, height - 1, 0), c: p.c || nearestPaletteCode('#ffffff'), dir: p.dir || 'up' })) .filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y)) : []; - return buildAssetMeta(category, subtype, normalizedDepth, lightPixels, meta.lightColor || '#ffd86a', meta.door || null, width, legacyParticlePixels, sourcePixels, height); + const normalized = buildAssetMeta(category, subtype, normalizedDepth, lightPixels, meta.lightColor || '#ffd86a', meta.door || null, width, legacyParticlePixels, sourcePixels, height); + if (meta.galleryOrigin) normalized.galleryOrigin = String(meta.galleryOrigin); + return normalized; } function buildAssetMeta(category, subtype, sourceDepthPixels, sourceLightPixels, lightColor, door, width = 8, sourceParticle = null, sourcePixels = null, height = width) { @@ -5923,6 +6180,20 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { })).filter((item) => item.assetId); } + function repairStaticPlacementTerrain(placements, assets) { + const assetById = new Map((assets || []).map((asset) => [asset.id, asset])); + return (placements || []).map((item) => { + const asset = assetById.get(item.assetId); + if (!asset || asset.category !== 'static') return item; + const tile = world.get(item.x, item.y); + const wantsWater = asset.subtype === 'water' || asset.subtype === 'ship'; + const valid = tile && (wantsWater ? tile.type === 'water' : tile.type !== 'water'); + if (valid) return item; + const target = wantsWater ? findNearestTerrain('water', item.x, item.y) : findNearestLandTerrain(item.x, item.y); + return { ...item, x: target.x, y: target.y }; + }); + } + function normalizeDynamicSummons(items) { return dedupeByAsset(items).map((item) => ({ id: item.id || uid(), @@ -5964,6 +6235,28 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return { x: startX, y: startY }; } + function findNearestTerrainByPredicate(startX, startY, predicate) { + let best = null; + let bestD = Infinity; + const maxR = Math.max(WORLD_W, WORLD_H); + for (let r = 0; r < maxR; r++) { + for (let y = Math.max(0, startY - r); y <= Math.min(WORLD_H - 1, startY + r); y++) { + for (let x = Math.max(0, startX - r); x <= Math.min(WORLD_W - 1, startX + r); x++) { + const tile = world.get(x, y); + if (!tile || !predicate(tile, x, y)) continue; + const d = Math.hypot(x - startX, y - startY); + if (d < bestD) { bestD = d; best = { x, y }; } + } + } + if (best) return best; + } + return { x: startX, y: startY }; + } + + function findNearestLandTerrain(startX, startY) { + return findNearestTerrainByPredicate(startX, startY, (tile) => tile.type !== 'water'); + } + function homeFromPos(pos) { return { homeX: pos.x, homeY: pos.y }; } @@ -6339,12 +6632,30 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { ]; const idByName = Object.fromEntries(assets.map((a) => [a.name, a.id])); + const assetById = new Map(assets.map((asset) => [asset.id, asset])); const removedLowQualitySeedAssetIds = new Set([...REMOVED_LOW_QUALITY_SEED_ASSET_NAMES].map((name) => idByName[name]).filter(Boolean)); + const normalizeSeedPlacementTerrain = (object) => { + const asset = assetById.get(object.assetId); + if (!asset || asset.category !== 'static') return object; + const tile = world.get(object.x, object.y); + const wantsWater = asset.subtype === 'water' || asset.subtype === 'ship'; + const valid = tile && (wantsWater ? tile.type === 'water' : tile.type !== 'water'); + if (valid) return object; + const target = wantsWater ? findNearestTerrain('water', object.x, object.y) : findNearestLandTerrain(object.x, object.y); + return { ...object, x: target.x, y: target.y }; + }; const applySeedQualityFilter = (gallery) => ({ ...gallery, - assets: gallery.assets.filter((asset) => !removedLowQualitySeedAssetIds.has(asset.id)), - placed: gallery.placed.filter((object) => !removedLowQualitySeedAssetIds.has(object.assetId)), - dynamicSummons: gallery.dynamicSummons.filter((object) => !removedLowQualitySeedAssetIds.has(object.assetId)) + assets: gallery.assets + .filter((asset) => !removedLowQualitySeedAssetIds.has(asset.id)) + .map((asset) => ({ ...asset, author: 'Local Artist', ownerAccountId: '' })), + placed: gallery.placed + .filter((object) => !removedLowQualitySeedAssetIds.has(object.assetId)) + .map(normalizeSeedPlacementTerrain) + .map((object) => ({ ...object, ownerAccountId: '' })), + dynamicSummons: gallery.dynamicSummons + .filter((object) => !removedLowQualitySeedAssetIds.has(object.assetId)) + .map((object) => ({ ...object, ownerAccountId: '' })) }); const whalePos = findNearestTerrain('water', 62, 58); const koiPos = findNearestTerrain('water', 66, 60); @@ -6460,6 +6771,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { baseState.assets = Array.isArray(baseState.assets) ? baseState.assets : []; baseState.placed = Array.isArray(baseState.placed) ? baseState.placed : []; baseState.dynamicSummons = Array.isArray(baseState.dynamicSummons) ? baseState.dynamicSummons : []; + baseState.deletedSeedAssetNames = Array.isArray(baseState.deletedSeedAssetNames) ? baseState.deletedSeedAssetNames : []; const removedExistingSeedAssetIds = new Set(baseState.assets .filter((asset) => REMOVED_LOW_QUALITY_SEED_ASSET_NAMES.has(asset.name) && (asset.ownerAccountId === 'island-team' || asset.author === 'Island Team')) .map((asset) => asset.id)); @@ -6474,7 +6786,12 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { } const assetByName = new Map(baseState.assets.map((asset) => [asset.name, asset])); const assetIdByName = new Map(baseState.assets.map((asset) => [asset.name, asset.id])); + const tombstonedDefaultNames = new Set(Object.values(baseState.tombstones?.assets || {}) + .filter((tombstone) => tombstone?.name) + .map((tombstone) => tombstone.name)); + for (const name of baseState.deletedSeedAssetNames) tombstonedDefaultNames.add(name); for (const seededAsset of seeded.assets) { + if (tombstonedDefaultNames.has(seededAsset.name)) continue; const existing = assetByName.get(seededAsset.name); if (!existing) { baseState.assets.push(seededAsset); @@ -6482,24 +6799,10 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { assetIdByName.set(seededAsset.name, seededAsset.id); continue; } - const isDefaultAsset = existing.ownerAccountId === 'island-team' || existing.author === 'Island Team'; - if (isDefaultAsset) { - const existingId = existing.id; - const createdAt = existing.createdAt || seededAsset.createdAt; - Object.assign(existing, { - ...seededAsset, - id: existingId, - createdAt, - updatedAt: Date.now(), - ownerAccountId: 'island-team', - author: 'Island Team' - }); - existing.contentHash = computeAssetContentHash(existing); - assetIdByName.set(seededAsset.name, existingId); - } + assetIdByName.set(seededAsset.name, existing.id); } const seededNameById = new Map(seeded.assets.map((asset) => [asset.id, asset.name])); - const hasPlacedAsset = new Set(baseState.placed.filter((item) => item && item.ownerAccountId === 'island-team').map((item) => item.assetId)); + const hasPlacedAsset = new Set(baseState.placed.filter((item) => item).map((item) => item.assetId)); for (const item of seeded.placed) { const name = seededNameById.get(item.assetId); const resolvedAssetId = assetIdByName.get(name); @@ -6508,7 +6811,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { hasPlacedAsset.add(resolvedAssetId); } } - const hasDynamicAsset = new Set(baseState.dynamicSummons.filter((item) => item && item.ownerAccountId === 'island-team').map((item) => item.assetId)); + const hasDynamicAsset = new Set(baseState.dynamicSummons.filter((item) => item).map((item) => item.assetId)); for (const item of seeded.dynamicSummons) { const name = seededNameById.get(item.assetId); const resolvedAssetId = assetIdByName.get(name); @@ -6547,6 +6850,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { pendingObjectVisuals: {} }, tombstones: { assets: {}, objects: {} }, + deletedSeedAssetNames: [], dynamicSummons: gallery.dynamicSummons }; } @@ -6576,10 +6880,13 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { originalAssetId: null, createdAt: Date.now(), updatedAt: Date.now(), - author: 'Island Team', - ownerAccountId: 'island-team', + author: 'Local Artist', + ownerAccountId: '', version: 1, - meta: buildAssetMeta(category, subtype, meta.depthPixels || [], meta.lightPixels || [], meta.lightColor || '#ffd86a', meta.door || null, w, meta.particleConfig || meta.particlePixels || [], right, h) + meta: { + ...buildAssetMeta(category, subtype, meta.depthPixels || [], meta.lightPixels || [], meta.lightColor || '#ffd86a', meta.door || null, w, meta.particleConfig || meta.particlePixels || [], right, h), + galleryOrigin: 'default-gallery' + } }; asset.contentHash = computeAssetContentHash(asset); return asset; @@ -6610,120 +6917,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return out; } - function exportData() { - els.dataBox.value = JSON.stringify(state, null, 2); - toast('Full JSON exported.'); - } - - function exportCompactData() { - const payload = Phase2Sync?.compactState ? Phase2Sync.compactState(state) : state; - els.dataBox.value = JSON.stringify(payload); - toast('Compact JSON exported.'); - } - - function exportSnapshotData() { - if (!Phase2Sync?.makeSnapshot) { - exportData(); - return; - } - els.dataBox.value = JSON.stringify(Phase2Sync.makeSnapshot(state)); - toast('Snapshot JSON exported.'); - } - - function exportAssetBundleData() { - if (!Phase2Sync?.makeAssetBundle) { - exportData(); - return; - } - els.dataBox.value = JSON.stringify(Phase2Sync.makeAssetBundle(state, state.assets.map((asset) => asset.id))); - toast('Asset bundle JSON exported.'); - } - - function prepareImportedAssetForSharedWorld(asset) { - const actor = currentAccountId(); - const existing = state.assets.find((item) => item.id === asset.id); - const existingOwner = normalizeOwnerAccountId(existing?.ownerAccountId); - if (existing && existingOwner && existingOwner !== actor) { - return { - ...asset, - id: uid(), - ownerAccountId: actor, - author: state.authorName || actor || 'Local Artist', - parentAssetId: asset.parentAssetId || asset.id, - originalAssetId: asset.originalAssetId || asset.id, - createdAt: Date.now(), - updatedAt: Date.now(), - version: 1 - }; - } - return { - ...asset, - ownerAccountId: normalizeOwnerAccountId(asset.ownerAccountId, actor), - author: asset.author || state.authorName || actor || 'Local Artist', - version: Number(asset.version) || 1 - }; - } - - function importData() { - try { - const imported = JSON.parse(els.dataBox.value); - if (imported?.format === Phase2Sync?.SNAPSHOT_FORMAT) { - toast('Snapshot import needs matching assets; import a full/compact save or asset bundle instead.'); - return; - } - if (Phase2Sync?.isAssetBundle?.(imported)) { - if (isSharedWorld()) ensureLocalAccount('import'); - const importedAssets = Phase2Sync.unpackAssetBundle(imported) - .map(normalizeAsset) - .map((asset) => isSharedWorld() ? normalizeAsset(prepareImportedAssetForSharedWorld(asset)) : asset) - .filter((asset) => !isAssetTombstoned(asset.id, asset.version)); - const byId = new Map(state.assets.map((asset) => [asset.id, asset])); - importedAssets.forEach((asset) => byId.set(asset.id, asset)); - state.assets = [...byId.values()]; - rebuildWorldIndex(); - saveState(); - spriteCache.clear(); - renderLibrary(); - updateSelectedLabel(); - toast(`${importedAssets.length} asset${importedAssets.length === 1 ? '' : 's'} imported.`); - return; - } - if (isSharedWorld()) { - toast('Shared worlds block full-state imports. Import an asset bundle or use a local sandbox.'); - return; - } - const expanded = Phase2Sync?.isCompactState?.(imported) ? Phase2Sync.expandState(imported) : imported; - state = normalizeState(expanded); - rebuildWorldIndex(); - hydrateAuthorUI(); - selectedAssetId = state.assets[0]?.id ?? null; - saveState(); - spriteCache.clear(); - hydrateRuntime(); - renderLibrary(); - updateSelectedLabel(); - toast('JSON imported.'); - } catch (error) { - toast('Invalid JSON.'); - console.error(error); - } - } - - function resetAll() { - if (!confirm('Reset all local assets and island objects?')) return; - localStorage.removeItem(STORAGE_KEY); - state = seedState(); - rebuildWorldIndex(); - hydrateAuthorUI(); - selectedAssetId = state.assets[0]?.id ?? null; - saveState(); - spriteCache.clear(); - hydrateRuntime(); - renderLibrary(); - updateSelectedLabel(); - toast('Local world reset.'); - } - function recordSyncEvent(event) { if (!event) return; if (isSharedWorld()) { @@ -6881,43 +7074,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { .slice(-PHASE5_GUARDRAILS.maxReports); } - function collectWorldValidation() { - const assetIds = new Set((state.assets || []).map((asset) => asset.id)); - const contentCounts = new Map(); - for (const asset of state.assets || []) { - const key = asset.contentHash || computeAssetContentHash(asset); - contentCounts.set(key, (contentCounts.get(key) || 0) + 1); - } - const orphanStatic = (state.placed || []).filter((item) => !assetIds.has(item.assetId)); - const orphanDynamic = (state.dynamicSummons || []).filter((item) => !assetIds.has(item.assetId)); - const invalidTerrain = []; - for (const item of state.placed || []) { - const asset = findAsset(item.assetId); - const tile = world.get(item.x, item.y); - if (asset && tile && !isTerrainCompatible(asset, tile)) invalidTerrain.push({ kind: 'static', id: item.id, assetId: item.assetId, x: item.x, y: item.y }); - } - for (const item of state.dynamicSummons || []) { - const asset = findAsset(item.assetId); - const tile = world.get(Math.round(item.homeX), Math.round(item.homeY)); - if (asset && tile && !isTerrainCompatible(asset, tile)) invalidTerrain.push({ kind: 'dynamic', id: item.id, assetId: item.assetId, x: item.homeX, y: item.homeY }); - } - const duplicateContent = [...contentCounts.values()].filter((count) => count > 1).reduce((sum, count) => sum + count - 1, 0); - return { - schema: SAVE_SCHEMA, - generatedAt: new Date().toISOString(), - assets: state.assets?.length || 0, - worldObjects: getWorldObjectCount(), - hiddenObjects: Object.keys(state.hiddenObjects || {}).length, - reports: state.moderationReports?.length || 0, - duplicateContent, - orphanStatic: orphanStatic.length, - orphanDynamic: orphanDynamic.length, - invalidTerrain: invalidTerrain.length, - invalidTerrainObjects: invalidTerrain, - limits: state.guardrails || PHASE5_GUARDRAILS - }; - } - function isTerrainCompatible(asset, tile) { if (!asset || !tile) return false; if (asset.category === 'static' && (asset.subtype === 'water' || asset.subtype === 'ship')) return tile.type === 'water'; @@ -6925,65 +7081,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return tile.type !== 'water'; } - function updateGuardrailStats() { - if (!els.guardrailStats) return; - const report = collectWorldValidation(); - const assetText = `${report.assets}/${report.limits.maxAssets}`; - const objectText = `${report.worldObjects}/${report.limits.maxWorldObjects}`; - const warnings = report.orphanStatic + report.orphanDynamic + report.invalidTerrain; - els.guardrailStats.textContent = `Assets ${assetText}, objects ${objectText}, reports ${report.reports}, hidden ${report.hiddenObjects}, duplicate-content copies ${report.duplicateContent}, validation warnings ${warnings}.`; - } - - function validateWorld() { - const report = collectWorldValidation(); - els.dataBox.value = JSON.stringify({ format: 'pixel-island-phase5-validation-v1', ...report }, null, 2); - updateGuardrailStats(); - const warnings = report.orphanStatic + report.orphanDynamic + report.invalidTerrain; - toast(warnings ? `Validation found ${warnings} warning(s).` : 'Validation passed.'); - } - - function exportModerationReport() { - const validation = collectWorldValidation(); - const payload = { - format: 'pixel-island-phase5-moderation-v1', - generatedAt: validation.generatedAt, - authorName: state.authorName || 'Local Artist', - reports: normalizeModerationReports(state.moderationReports || []), - hiddenObjects: state.hiddenObjects || {}, - downvotedObjects: Object.entries(state.objectVotes || {}) - .map(([objectId, votes]) => ({ objectId, up: votes.up || 0, down: votes.down || 0, score: (votes.up || 0) - (votes.down || 0) })) - .filter((item) => item.down > 0), - validation - }; - els.dataBox.value = JSON.stringify(payload, null, 2); - toast('Moderation report exported.'); - } - - function clearModerationReports() { - if (!state.moderationReports?.length) { - toast('No local reports to clear.'); - return; - } - if (!confirm('Clear local moderation reports? Hidden objects stay hidden.')) return; - state.moderationReports = []; - saveState(); - updateGuardrailStats(); - toast('Local reports cleared.'); - } - - function updateSyncStats() { - if (!els.syncStats) return; - const report = Phase2Sync?.compactSizeReport?.(state); - if (!report) { - els.syncStats.textContent = 'Phase 2 codec unavailable.'; - return; - } - const saved = report.savedPercent > 0 ? `${report.savedPercent}% smaller` : 'no saving yet'; - const statsRect = getViewportWorldRect(64); - const visibleChunkCount = terrainCache?.chunks?.filter((chunk) => !(chunk.x + chunk.w < statsRect.left || chunk.x > statsRect.right || chunk.y + chunk.h < statsRect.top || chunk.y > statsRect.bottom)).length || 0; - els.syncStats.textContent = `Compact local save: ${report.compactBytes.toLocaleString()} bytes / full ${report.fullBytes.toLocaleString()} bytes (${saved}). Assets ${report.assets}, objects ${report.objects}, queued events ${state.eventLog?.length || 0}, pending commands ${state.serverSync?.pendingCommands?.length || 0}. Authority: publish ${serverAuthority().publish}, move ${serverAuthority().objectMove}, day/night ${serverAuthority().dayNight}, dynamic ${serverAuthority().dynamicMotion}. Terrain chunks ${visibleChunkCount}/${terrainCache?.chunks?.length || 0}.`; - } - function computeAssetContentHash(asset) { const payload = [ asset.category || '', @@ -7069,13 +7166,23 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { } function nearestPaletteCode(color) { - if (!color || typeof color !== 'string') return null; - if (PALETTE_BY_CODE[color]) return color; + // This can run during loadState()/seedState() before selectedColorCode is initialized. + // Keep the fallback independent from editor state to avoid TDZ startup crashes. + return nearestPaletteCodeFrom(PALETTE, color, DEFAULT_SELECTED_COLOR_CODE); + } + + function nearestBasicPaletteCode(color) { + return nearestPaletteCodeFrom(PALETTE.slice(0, BASIC_PALETTE_COUNT), PALETTE_BY_CODE[color] || color, PALETTE[0].code); + } + + function nearestPaletteCodeFrom(entries, color, fallback) { + if (!color || typeof color !== 'string') return fallback || entries[0]?.code || PALETTE[0].code; + if (entries.some((entry) => entry.code === color)) return color; const rgb = parseHex(color); - if (!rgb) return selectedColorCode; - let best = PALETTE[0].code; + if (!rgb) return fallback || entries[0]?.code || PALETTE[0].code; + let best = entries[0]?.code || PALETTE[0].code; let bestDist = Infinity; - for (const entry of PALETTE) { + for (const entry of entries) { const p = parseHex(entry.color); const dist = (rgb.r - p.r) ** 2 + (rgb.g - p.g) ** 2 + (rgb.b - p.b) ** 2; if (dist < bestDist) { bestDist = dist; best = entry.code; } @@ -7187,43 +7294,45 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { const lc = parseHex(light.c || '#ffd86a'); if (!lc) continue; const isAssetLight = !!light.assetLight; + const isCursorLight = !!light.cursor; const dist = Math.hypot((light.x + 0.5) - (x + 0.5), (light.y + 0.5) - (y + 0.5)); - const reach = isAssetLight ? Math.max(1.85, size * 0.24) : Math.max(2.25, size * 0.46); + const reach = isAssetLight ? Math.max(1.85, size * 0.24) : isCursorLight ? Math.max(2.6, size * 0.52) : Math.max(2.25, size * 0.46); const t = 1 - clamp(dist / reach, 0, 1); const sourceHotspot = Math.pow(clamp(1 - dist / Math.max(0.001, reach * 0.24), 0, 1), 2.8); const assetResponse = clamp(Math.pow(t, 1.45) * 0.52 + sourceHotspot * 1.18, 0, 1.58); - const depthResponse = depth ? (isAssetLight ? assetResponse : depthLocalLightResponse(dist, reach)) : (isAssetLight ? assetResponse : t); + const cursorResponse = Math.pow(clamp(t, 0, 1), 1.58) * (0.12 + sourceHotspot * 0.42); + const depthResponse = depth ? (isAssetLight ? assetResponse : isCursorLight ? cursorResponse : depthLocalLightResponse(dist, reach)) : (isAssetLight ? assetResponse : t); const response = depth ? depthResponse : t; if (response <= 0) continue; const sourceIntensity = clamp(Number(light.intensity ?? 1) || 1, 0.18, 1.85); const edge = getDepthLightFacing(depth, x, y, width, pixels, depths, light.x, light.y, height); const depthEdgePresence = isAssetLight ? (0.18 + Math.pow(Math.max(0, edge), 1.4) * 0.74 + sourceHotspot * 0.7) - : (0.06 + edge * 1.24); + : isCursorLight ? (0.02 + edge * 1.12 + sourceHotspot * 0.16) : (0.06 + edge * 1.24); const edgePresence = depth ? depthEdgePresence : 1; const localDepthMultiplier = depth ? DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : 1.1; const strengthBase = depth - ? (isAssetLight ? (sourceHotspot * 0.34 + depthResponse * 0.045) : (0.12 + depthResponse * 0.22)) + ? (isAssetLight ? (sourceHotspot * 0.34 + depthResponse * 0.045) : isCursorLight ? (sourceHotspot * 0.18 + depthResponse * 0.12) : (0.12 + depthResponse * 0.22)) : t * t; - const strength = strengthBase * (isAssetLight ? (10.5 + sourceHotspot * 18 + Math.abs(depth) * 1.2) : (4.2 + Math.abs(depth) * 2.6)) * edgePresence * sourceIntensity * localDepthMultiplier; - const mix = Math.min(depth ? (isAssetLight ? 0.055 : 0.48) : (isAssetLight ? 0.06 : 0.24), (0.008 + response * (depth ? (isAssetLight ? 0.012 : 0.075) : (isAssetLight ? 0.028 : 0.10))) * sourceIntensity * (depth ? (isAssetLight ? 0.7 : 1.35) : 1)); + const strength = strengthBase * (isAssetLight ? (10.5 + sourceHotspot * 18 + Math.abs(depth) * 1.2) : isCursorLight ? (2.1 + Math.abs(depth) * 1.15) : (4.2 + Math.abs(depth) * 2.6)) * edgePresence * sourceIntensity * localDepthMultiplier; + const mix = Math.min(depth ? (isAssetLight ? 0.055 : isCursorLight ? 0.18 : 0.48) : (isAssetLight ? 0.06 : isCursorLight ? 0.16 : 0.24), (0.008 + response * (depth ? (isAssetLight ? 0.012 : isCursorLight ? 0.038 : 0.075) : (isAssetLight ? 0.028 : isCursorLight ? 0.072 : 0.10))) * sourceIntensity * (depth ? (isAssetLight ? 0.7 : isCursorLight ? 0.85 : 1.35) : 1)); r = lerp(r, lc.r + strength, mix); g = lerp(g, lc.g + strength * 0.82, mix * 0.94); b = lerp(b, lc.b + strength * 0.74, mix * 0.9); - const colorWash = (depth ? (isAssetLight ? 0.004 : 0.045) : (isAssetLight ? 0.006 : 0.03)) * sourceIntensity * edgePresence * (depth ? (isAssetLight ? sourceHotspot : (0.26 + depthResponse * 0.22)) : t); + const colorWash = (depth ? (isAssetLight ? 0.004 : isCursorLight ? 0.012 : 0.045) : (isAssetLight ? 0.006 : isCursorLight ? 0.018 : 0.03)) * sourceIntensity * edgePresence * (depth ? (isAssetLight ? sourceHotspot : isCursorLight ? depthResponse : (0.26 + depthResponse * 0.22)) : t); r += (lc.r - 152) * colorWash; g += (lc.g - 152) * colorWash * 0.96; b += (lc.b - 152) * colorWash * 0.92; - const tintBase = depth ? (isAssetLight ? sourceHotspot * 0.11 : (0.12 + depthResponse * 0.18)) : Math.pow(t, 1.25); - const tintPower = tintBase * sourceIntensity * (depth ? (isAssetLight ? 0.016 : 0.12) * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : (isAssetLight ? 0.012 : 0.03)) * (depth ? edgePresence : 1); + const tintBase = depth ? (isAssetLight ? sourceHotspot * 0.11 : isCursorLight ? depthResponse : (0.12 + depthResponse * 0.18)) : Math.pow(t, 1.25); + const tintPower = tintBase * sourceIntensity * (depth ? (isAssetLight ? 0.016 : isCursorLight ? 0.045 : 0.12) * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : (isAssetLight ? 0.012 : isCursorLight ? 0.018 : 0.03)) * (depth ? edgePresence : 1); localTintR += lc.r * tintPower; localTintG += lc.g * tintPower; localTintB += lc.b * tintPower; localTintWeight += tintPower; - const liftBase = depth ? (isAssetLight ? (sourceHotspot * 0.42 + Math.pow(t, 2.2) * 0.035) : (0.12 + depthResponse * 0.16)) : Math.pow(t, 1.9); - localLift += liftBase * sourceIntensity * (depth ? (isAssetLight ? 0.85 : 2.4) * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : 3.6) * (depth ? edgePresence : 1); + const liftBase = depth ? (isAssetLight ? (sourceHotspot * 0.42 + Math.pow(t, 2.2) * 0.035) : isCursorLight ? depthResponse : (0.12 + depthResponse * 0.16)) : Math.pow(t, 1.9); + localLift += liftBase * sourceIntensity * (depth ? (isAssetLight ? 0.85 : isCursorLight ? 1.05 : 2.4) * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER : 3.6) * (depth ? edgePresence : 1); } } @@ -7377,18 +7486,21 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { for (const light of lights) { if (!light) continue; const d = Math.hypot(light.x - x, light.y - y); - const distanceResponse = depthLocalLightResponse(d, reach); + const isCursor = !!light.cursor; + const lightReach = isCursor ? Math.max(2.6, Math.max(width, height) * 0.52) : reach; + const distanceResponse = isCursor ? Math.pow(clamp(1 - d / lightReach, 0, 1), 1.55) : depthLocalLightResponse(d, lightReach); if (distanceResponse <= 0) continue; const facing = getDepthLightFacing(depth, x, y, width, pixels, depths, light.x, light.y, height); if (!facing) continue; - const sourceIntensity = clamp(Number(light.intensity ?? 1) || 1, 0.25, 1.85); - const nearSource = Math.pow(clamp(1 - d / Math.max(1.2, reach * 0.18), 0, 1), 2.3); - const plateau = 0.74 + distanceResponse * 0.34; - const edgeFocus = 0.4 + facing * 0.95; + const sourceIntensity = clamp(Number(light.intensity ?? 1) || 1, 0.25, isCursor ? 1.25 : 1.85); + const nearSource = Math.pow(clamp(1 - d / Math.max(1.2, lightReach * 0.18), 0, 1), 2.3); + const plateau = isCursor ? distanceResponse : 0.74 + distanceResponse * 0.34; + const edgeFocus = isCursor ? (0.1 + facing * 1.18) : (0.4 + facing * 0.95); const edgeStrength = outerEdge ? 1.08 : 0.94; - const cornerBoost = 1 + cornerStrength * (0.9 + facing * 0.38); - const sourceBoost = 1 + nearSource * 1.12; - total += plateau * edgeFocus * edgeStrength * cornerBoost * sourceBoost * sourceIntensity * (depth > 0 ? 8.2 : 6.4) * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER; + const cornerBoost = 1 + cornerStrength * (isCursor ? 0.36 : 0.9 + facing * 0.38); + const sourceBoost = 1 + nearSource * (isCursor ? 0.46 : 1.12); + const cursorClamp = isCursor ? 0.38 : 1; + total += plateau * edgeFocus * edgeStrength * cornerBoost * sourceBoost * sourceIntensity * (depth > 0 ? 8.2 : 6.4) * DEPTH_LOCAL_LIGHT_RESPONSE_MULTIPLIER * cursorClamp; } return clamp(total, 0, depth > 0 ? 172 : 126); } @@ -7477,30 +7589,32 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { function buildPalette() { - // 62 stable palette slots. The first 48 entries are arranged as four - // top-to-bottom ramps in the 4-column picker: neutral, warm, green/cyan, blue/purple. - // The remaining slots are accents; this avoids the old zig-zag gradient. - const colors = [ - '#fffdf7', '#fff0c7', '#e9fff2', '#eef4ff', - '#eee5d4', '#ffdca0', '#bff6d2', '#cbdcff', - '#d4c8b8', '#ffc071', '#87e79f', '#99b5f3', - '#b8a58f', '#ee9a55', '#57cf78', '#6f8fe2', - '#95806b', '#d6723d', '#35ad5a', '#4f68c5', - '#75614f', '#ad4f2b', '#248443', '#36469a', - '#5a4d43', '#84351f', '#1a6435', '#27306e', - '#454048', '#5e2418', '#124827', '#1b214d', - '#31333d', '#3f1712', '#0b311c', '#111733', - '#222632', '#2a0e0b', '#071f13', '#0a0d20', - '#151923', '#160806', '#04140c', '#050812', - '#090b10', '#090403', '#010804', '#01030a', - '#fff2f5', '#ffe55d', '#aef8ff', '#ead0ff', - '#ffb0c7', '#f2c438', '#60d6e8', '#c58df0', - '#ef6b88', '#e88c3e', '#37b9c9', '#9a56d9', - '#cf263f', '#c55a1c', '#1f7fa2', '#7132ad', - '#18c33a', '#76d322', '#22c78e', '#e247ae', - '#1e79e8', '#8148db' + // 91 one-character palette slots. + // 0-j are the compact basic palette shown in normal draw mode. + // k-Z plus safe ASCII symbols are advanced slots; storage remains one char per pixel. + const basicColors = [ + // Ink, paper, and neutral anchors + '#111827', '#374151', '#6b7280', '#d1d5db', '#fff7ed', + // Common drawing colors arranged from warm to cool + '#7f1d1d', '#dc2626', '#f97316', '#facc15', '#84cc16', + '#16a34a', '#14b8a6', '#06b6d4', '#2563eb', '#4f46e5', + '#7c3aed', '#c026d3', '#ec4899', '#f5d0a9', '#7c4a2d' ]; - return COLOR_CODES.split('').map((code, index) => ({ code, color: colors[index] || '#14151c' })); + const advancedColors = [ + '#fafafa', '#f5f5f4', '#e7e5e4', '#d6d3d1', '#a8a29e', '#78716c', '#57534e', '#292524', '#0c0a09', + '#fef3c7', '#fde68a', '#d6a25f', '#9a6a3a', '#5c3b24', + '#fee2e2', '#fca5a5', '#ef4444', '#b91c1c', '#7f1d1d', + '#ffedd5', '#fdba74', '#f97316', '#c2410c', '#7c2d12', + '#fef9c3', '#fde047', '#eab308', '#a16207', '#713f12', + '#ecfccb', '#bef264', '#84cc16', '#4d7c0f', '#365314', '#dcfce7', '#86efac', '#15803d', + '#ccfbf1', '#5eead4', '#14b8a6', '#0f766e', '#134e4a', + '#cffafe', '#67e8f9', '#22d3ee', '#06b6d4', '#0e7490', '#e0f2fe', '#38bdf8', + '#dbeafe', '#93c5fd', '#3b82f6', '#2563eb', '#1d4ed8', '#1e3a8a', + '#e0e7ff', '#a5b4fc', '#6366f1', '#4f46e5', '#3730a3', '#ede9fe', '#c4b5fd', '#8b5cf6', + '#fae8ff', '#e879f9', '#c026d3', '#86198f', '#fce7f3', '#f9a8d4', '#ec4899', '#be185d' + ]; + const colors = [...basicColors, ...advancedColors]; + return COLOR_CODES.split('').map((code, index) => ({ code, color: colors[index] || hslToHex((index * 47) % 360, 72, 58) })); } function pseudoNoise(value) { diff --git a/assets.json b/assets.json deleted file mode 100644 index ca1edbb..0000000 --- a/assets.json +++ /dev/null @@ -1,2815 +0,0 @@ -[ - { - "id": "dkjphwcy69za", - "name": "Crescent Tea House", - "category": "static", - "subtype": "building", - "width": 16, - "height": 16, - "pixels": ".......NN.............N00N...........N00N.............NN............wwwwwww........wgggggggw......wggNggNggw......ggg0gg0ggg......gggggggggg......gggNAANggg......gggANNAggg......gggANNAggg.......ggAAAAgg.........wwwwww..........ggAAgg.........wwwwwwww.....", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 6, - "y": 5, - "c": "N" - }, - { - "x": 9, - "y": 5, - "c": "N" - }, - { - "x": 7, - "y": 10, - "c": "N" - } - ], - "lightColor": "N", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......11.............1111...........1111.............11............-------........-1111111-......-11111111-......-11111111-......-11111111-......-111--111-......-111--111-......-111--111-.......--------.........------..........--11--.........--------.....", - "door": { - "x": 7, - "y": 14 - } - }, - "contentHash": "fnv1a:122df3c6" - }, - { - "id": "413sk5ay69zk", - "name": "Prism Sakura", - "category": "static", - "subtype": "nature", - "width": 16, - "height": 16, - "pixels": "......................................QQ.Q..........QQUUQQQ........QUUQQQUQ.......QQQUQQUQQQ.......QQUUUQQ...........QUQ..............k..............eke............ekkke..........eekkkee...........kkk.............ksk............sssss..........sssssss......", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": true, - "particlePixels": [ - { - "x": 5, - "y": 4, - "c": "Q", - "dir": "up" - }, - { - "x": 10, - "y": 5, - "c": "P", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "Q", - "dir": "up" - }, - "depthPixels": "......................11.1..........1111111........11111111.......1111111111.......1111111...........111..............-..............---............-----..........-------...........---.............---............-----..........-------......................", - "door": null - }, - "contentHash": "fnv1a:729945aa" - }, - { - "id": "njlfq4gy69zo", - "name": "Clockwork Whale", - "category": "static", - "subtype": "ship", - "width": 16, - "height": 16, - "pixels": ".......................................................................................................N..............NN...........nnSSSSSn.......nSSSSSSSSn.....nSSS0SSS0Sn.....nSSSSSSSSSn.N....nSSSSSSSn.NN.....nnnnnnn.NN.......oooooo...........oooo.......", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......................................1..............11...........11111111.......1111111111.....11111111111.....11111111111.1....111111111.11.....-------.11.......------...........----.......................................................................", - "door": null - }, - "contentHash": "fnv1a:64c5bc09" - }, - { - "id": "s4e450ky69zt", - "name": "Lantern Cat", - "category": "dynamic", - "subtype": "animal", - "width": 16, - "height": 16, - "pixels": ".....................................................................................................................E..E...........E9999E..........90909...........99w99..........ddddddd........dddNNdddN.......dddddddNN........d..ddd.........EE..E.E.......", - "faces": { - "right": ".....................................................................................................................E..E...........E9999E..........90909...........99w99..........ddddddd........dddNNdddN.......dddddddNN........d..ddd.........EE..E.E.......", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...................................1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111...................................................", - "door": null - }, - "contentHash": "fnv1a:ae4c582f" - }, - { - "id": "i1hkr1dy69zw", - "name": "Cloud Koi", - "category": "dynamic", - "subtype": "fish", - "width": 16, - "height": 16, - "pixels": ".....................................................................................................................................O..............OOO............OSSSOOO........OSSSSSSSO......OSS0SSS0SSO......OSSSSSSSO..Q.....OSSSOOO..QQ.......O......Q...", - "faces": { - "right": ".....................................................................................................................................O..............OOO............OSSSOOO........OSSSSSSSO......OSS0SSS0SSO......OSSSSSSSO..Q.....OSSSOOO..QQ.......O......Q...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": true, - "particlePixels": [ - { - "x": 5, - "y": 8, - "c": "O", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "O", - "dir": "up" - }, - "depthPixels": null, - "door": null - }, - "contentHash": "fnv1a:ee34e211" - }, - { - "id": "do3eje4y69zy", - "name": "Paper Crane", - "category": "dynamic", - "subtype": "bird", - "width": 16, - "height": 16, - "pixels": ".......................................................................................................................3..............333............33733..........3377333........3377E7733.........37E3.............EE.............E..E...........E....E......", - "faces": { - "right": ".......................................................................................................................3..............333............33733..........3377333........3377E7733.........37E3.............EE.............E..E...........E....E......", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": null, - "door": null - }, - "contentHash": "fnv1a:b14c3672" - }, - { - "id": "4ymi1wqy6a00", - "name": "Moon Lantern", - "category": "static", - "subtype": "building", - "width": 16, - "height": 16, - "pixels": ".......5..............555.............505............55555..........sssssss........skkkkkkks.......kbbkkbbk.......skk5555kks......kkkk55kkkk......kkkE55Ekkk......kkkEEEEkkk......kkkUEEUUkk.......kkUEEUk.........kkkkkkkk.........kkkkkk..........ssssss......", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 6, - "y": 7, - "c": "5" - }, - { - "x": 7, - "y": 7, - "c": "5" - }, - { - "x": 8, - "y": 7, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......1..............111.............111............11111..........-------........-1111111-.......-111111-.......-11111111-......-11111111-......-111--111-......-111--111-......-111--111-.......-111111-........--------.........------..........------......", - "door": { - "x": 7, - "y": 14 - } - }, - "contentHash": "fnv1a:68b9107e" - }, - { - "id": "s74ryg2y6a08", - "name": "Glass Fern", - "category": "static", - "subtype": "nature", - "width": 16, - "height": 16, - "pixels": ".......................................i..............i6i............i666i........O..i6i6i..O......O.i6i6i.O........Oi666iO..........i6i6i..........i66666.........i666i6i........i6i6i6i6i.........ppppp...........pkkkp..........ppkkkpp.........kkkkkkk......", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......................1..............111............11111........1..11111..1......1.11111.1........1111111..........11111..........111111.........1111111........111111111.........11111...........11111..........1111111.........1111111......................", - "door": null - }, - "contentHash": "fnv1a:b3bc2051" - }, - { - "id": "bpl16ouy6a0f", - "name": "Linen Cottage", - "category": "static", - "subtype": "building", - "width": 16, - "height": 16, - "pixels": "......................................................................................kkkk...........kkkkkk.........kkkkkkkk.......kkkkkkkkkk......kkkkkkkkkk.......hh9hhhhh........hddddd55........hddddd55........hdwwwddh........hdwwwddk........hhwwwhhh....", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 10, - "y": 9, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "......................................................1111...........111111.........11111111.......1111111111......1111111111.......11111111........11111111........11111111........11111111........11111111........11111111....................................", - "door": { - "x": 7, - "y": 13 - } - }, - "contentHash": "fnv1a:4fc8c8e4" - }, - { - "id": "kfci33qy6a0n", - "name": "Pine Cluster", - "category": "static", - "subtype": "nature", - "width": 16, - "height": 16, - "pixels": "......................................................................iii............iiiii...........aiiii..........iiiiiimmm.......iiiiiiaiii.....iiiiiimmmmm.....iiiiiiiiiiii...iiiiiimmmmmmm...iiiiiiiiiiiiii......kmmmmmmmmm......kk..kk..........kk..kk....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "......................................111............11111...........11111..........111111111.......1111111111.....11111111111.....111111111111...1111111111111...11111111111111......1111111111......11..11..........11..11....................................", - "door": null - }, - "contentHash": "fnv1a:420bbd1e" - }, - { - "id": "6vyaizcy6a0w", - "name": "Coral Skiff", - "category": "static", - "subtype": "ship", - "width": 16, - "height": 16, - "pixels": "........................................................o...............o..............05o............005o...........0055o..........05555o.............ooo.........V...oo...V.....VVVllllllVV.....VlllllllllV......llllllll.........xxxxxx...........xxxx.......", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........................1...............1..............11.............111............1111...........11111..............---.........1...--...1.....111-------1.....-----------......--------.........------...........----.......................................", - "door": null - }, - "contentHash": "fnv1a:7d1af0a3" - }, - { - "id": "h4vumx6y6a0z", - "name": "Sprout Fox", - "category": "dynamic", - "subtype": "animal", - "width": 16, - "height": 16, - "pixels": ".........................................................................................................................i..............i6i.........999..i.........90099..........99900999........9l9ll9099.......9999999..........9..9.9.........xx..xxx.......", - "faces": { - "right": ".........................................................................................................................i..............i6i.........999..i.........90099..........99900999........9l9ll9099.......9999999..........9..9.9.........xx..xxx.......", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...................................1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111...................................................", - "door": null - }, - "contentHash": "fnv1a:1ee0e8e9" - }, - { - "id": "n2rre9uy6a13", - "name": "Violet Moth", - "category": "dynamic", - "subtype": "animal", - "width": 16, - "height": 16, - "pixels": ".......................................................................................................................................T.............TTXTT..........TTXrXTT........TTXXrXXTT.........TXrXT............ErE............P.E.P..........P.....P.....", - "faces": { - "right": ".......................................................................................................................................T.............TTXTT..........TTXrXTT........TTXXrXXTT.........TXrXT............ErE............P.E.P..........P.....P.....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": null, - "door": null - }, - "contentHash": "fnv1a:17684793" - }, - { - "id": "qpbwzi9y6a15", - "name": "Lantern Walker", - "category": "dynamic", - "subtype": "human", - "width": 16, - "height": 16, - "pixels": "......................................................................................EEE............E555E...........E5s5E............555............UUUUU..........UUUU5U5.........UUUU.55..........nnnU............n.nn............n..n...........EE..EE......", - "faces": { - "right": "......................................................................................EEE............E555E...........E5s5E............555............UUUUU..........UUUU5U5.........UUUU.55..........nnnU............n.nn............n..n...........EE..EE......", - "left": "mirror" - }, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 9, - "y": 7, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...................................1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111...................................................", - "door": null - }, - "contentHash": "fnv1a:32eb2b41" - }, - { - "id": "diitnqry6a19", - "name": "Wildflower Patch", - "category": "static", - "subtype": "nature", - "width": 8, - "height": 8, - "pixels": ".........eQeNe..e0eSeQe.eeeeeee.eNeQe0e.eeeeeee..ieiei....kk....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".........11111..1111111.1111111.1111111.1111111..11111....11....", - "door": null - }, - "contentHash": "fnv1a:4a7f5581" - }, - { - "id": "f5wcp4iy6a1b", - "name": "River Stones", - "category": "static", - "subtype": "nature", - "width": 8, - "height": 8, - "pixels": "..................ff.....f88f...f8cc8f...f8888f...feif.....ff...", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..........11.....1111...111111...111111...1111.....11...........", - "door": null - }, - "contentHash": "fnv1a:52d1f905" - }, - { - "id": "s7k786dy6a1d", - "name": "Maple Canopy", - "category": "static", - "subtype": "nature", - "width": 16, - "height": 16, - "pixels": "......................VV............VVhhVV.........VVhhNNVV.......VVhhNNNNVV......VhhNNeeNNV.....VhhNNeeeeNNV....VhNNeeeeeeNV....VNNee11eeeNV.....VNNeeeeeeNV.....VVNeeeeNNV.......VVNNNNNV..........pppp...........pppppp..........pppppp.........tttttttt.....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "......11............111111.........11111111.......1111111111......1111111111.....111111111111....111111111111....111111111111.....1111111111......1111111111.......11111111..........----...........------..........------.........--------.....................", - "door": null - }, - "contentHash": "fnv1a:1ad36d0b" - }, - { - "id": "zxghctgy6a1i", - "name": "Misty Falls", - "category": "static", - "subtype": "nature", - "width": 16, - "height": 16, - "pixels": "....................eeeeee.........egggggge.......eggOOOOgge......egO3333Oge......egO3SS3Oge......egO3SS3Oge......egO3SS3Oge......egO3SS3Oge......egO3333Oge.......eO3333Oe.........OO33OO..........333333.........3bbbbbb3.......3bbbbbbbb3......3333333333....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": true, - "particlePixels": [ - { - "x": 6, - "y": 8, - "c": "3", - "dir": "up" - }, - { - "x": 10, - "y": 8, - "c": "3", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "3", - "dir": "up" - }, - "depthPixels": "....111111.........11111111.......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111.......11111111.........111111..........------.........--------.......----------......----------....................", - "door": null - }, - "contentHash": "fnv1a:8ac4c6c5" - }, - { - "id": "1rfqz3ay6a1n", - "name": "Lotus Pond", - "category": "static", - "subtype": "nature", - "width": 16, - "height": 12, - "pixels": "....................................ei....ei......eiiie..eiie.....iiQQiiiiQMi....iiQQMQQiiQQii...iiSSSSSSSSSSi...iSSSSSSSSSSSi...iiSSSSSSSSSii....iiiSSSSiii.......iiiiiiii.........o....o......", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": true, - "particlePixels": [ - { - "x": 6, - "y": 6, - "c": "M", - "dir": "up" - }, - { - "x": 11, - "y": 5, - "c": "0", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "M", - "dir": "up" - }, - "depthPixels": "....................11....11......11111..1111.....11111111111....1111111111111...1111----11111...111-------111...1111----11111....1111111111.......11111111.........-....-......................", - "door": null - }, - "contentHash": "fnv1a:0f18431e" - }, - { - "id": "koiuytsy6a1s", - "name": "Mossy Arch", - "category": "static", - "subtype": "nature", - "width": 16, - "height": 16, - "pixels": ".....................................eeee...........egggge.........egggggge.......eggeeeegge......ege....ege......ege....ege......ege....ege......ege....ege......ege....ege......eggeeeegge......eeecccceee........cccccc.........ccc..ccc.......oooo..oooo....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".....................1111...........111111.........11111111.......1111..1111......111....111......111....111......111....111......111....111......111....111......1111..1111......1111111111........------.........---..---.......----..----....................", - "door": null - }, - "contentHash": "fnv1a:d89452f4" - }, - { - "id": "pj50yr5y6a20", - "name": "Sunflower Grove", - "category": "static", - "subtype": "nature", - "width": 12, - "height": 16, - "pixels": ".........................NN..NN..NN.NVVN.VVN.NVVNVpVNVpVNVpV.NVV..VV..VV..ee..ee..ee..eaeeeaeeae.aeeeeeeeeea.aeaeaeaeaea..e..e..e..e..k..k..k..k..k..k..k..k.kkkkkkkkkk..eaeaeaeaea...kkkkkkkk..", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".............11..11..11.1111.111.111111111111111.111..11..11..11..11..11..1111111111.11111111111.11111111111..1..1..1..1..1..1..1..1..1..1..1..1.1111111111..1111111111...11111111..............", - "door": null - }, - "contentHash": "fnv1a:66efaabf" - }, - { - "id": "5nzjl30y6a28", - "name": "Reed Bed", - "category": "static", - "subtype": "nature", - "width": 12, - "height": 12, - "pixels": "................ccc.......c.ccg.c.....gcccgcg.....gcccgcc.....gcccgcc....33333SS33..3SSSSSWWSS3.3SWWWWWWWS3..33SSSSSS3....eeeeee.......kkkk.....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "................111.......1.111.1.....1111111.....1111111.....1111111....111111111..11111111111.11111111111..111111111....111111.......1111.....", - "door": null - }, - "contentHash": "fnv1a:d4c76094" - }, - { - "id": "jxztkudy6a2e", - "name": "Meadow Hare", - "category": "dynamic", - "subtype": "animal", - "width": 12, - "height": 12, - "pixels": "........................................cc.........cddc........cdddc......cddMMdc.....cddoodc.....cdddddc.....cddddd.......d.dd.......kk.k.k....", - "faces": { - "right": "........................................cc.........cddc........cdddc......cddMMdc.....cddoodc.....cdddddc.....cddddd.......d.dd.......kk.k.k....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "................11.........1111........11111......1111111.....1111111.....1111111.....111111.......1.11.......11.1.1............................", - "door": null - }, - "contentHash": "fnv1a:2520de66" - }, - { - "id": "lb3ho6xy6a2h", - "name": "Brook Turtle", - "category": "dynamic", - "subtype": "animal", - "width": 12, - "height": 10, - "pixels": "........................................eee.......eaaaae.....eaa11aae....eaaeeaaee....eaaaaae......eeee..S......o..oSS..", - "faces": { - "right": "........................................eee.......eaaaae.....eaa11aae....eaaeeaaee....eaaaaae......eeee..S......o..oSS..", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "............................111.......111111.....11111111....111111111....1111111......1111..1......1..111..............", - "door": null - }, - "contentHash": "fnv1a:d8ac1b11" - }, - { - "id": "wyf8x9gy6a2n", - "name": "Red Panda", - "category": "dynamic", - "subtype": "animal", - "width": 12, - "height": 12, - "pixels": "...........................MM..MM.....MhMhMhhM....MhhMMhhM....MhhtthhM.....hZZZZh.....hZZZZZZh....hZZMMZZh....hZZZZZZh.....Z..Z.Z.....k...k.k...", - "faces": { - "right": "...........................MM..MM.....MhMhMhhM....MhhMMhhM....MhhtthhM.....hZZZZh.....hZZZZZZh....hZZMMZZh....hZZZZZZh.....Z..Z.Z.....k...k.k...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...............11..11.....11111111....11111111....11111111.....111111.....11111111....11111111....11111111.....1..1.1.....1...1.1...............", - "door": null - }, - "contentHash": "fnv1a:d2fd3caf" - }, - { - "id": "eg4ckyiy6a2t", - "name": "River Otter", - "category": "dynamic", - "subtype": "animal", - "width": 14, - "height": 8, - "pixels": ".................kkk..........kggggkk......kgg44gggkk....kggxxgggggk...kgggggggggk....kgggg..gg......ooo...o....", - "faces": { - "right": ".................kkk..........kggggkk......kgg44gggkk....kggxxgggggk...kgggggggggk....kgggg..gg......ooo...o....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".................111..........1111111......1111111111....11111111111...11111111111....11111..11......111...1....", - "door": null - }, - "contentHash": "fnv1a:bd0000ef" - }, - { - "id": "xwr5wr6y6a2x", - "name": "Amber Deer", - "category": "dynamic", - "subtype": "animal", - "width": 14, - "height": 14, - "pixels": ".................................V............V.V..........VV9VV.........V999VV.......VV9xx9VV......V999999V......V999999VV.....V9999999V......V99999V.......V.VV.V.......k..VV..k......k..VV..k....", - "faces": { - "right": ".................................V............V.V..........VV9VV.........V999VV.......VV9xx9VV......V999999V......V999999VV.....V9999999V......V99999V.......V.VV.V.......k..VV..k......k..VV..k....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...................1............1.1..........11111.........111111.......11111111......11111111......111111111.....111111111......1111111.......1.11.1.......1..11..1......1..11..1..................", - "door": null - }, - "contentHash": "fnv1a:b497b00e" - }, - { - "id": "l2antp0y6a33", - "name": "Forest Owl", - "category": "dynamic", - "subtype": "bird", - "width": 10, - "height": 12, - "pixels": ".......................dd.d.....dkkkkd....k4444k...k44ww44k..k444444k..kk4444kk...kggggk....kggggk....k.kk.k.....k..k...", - "faces": { - "right": ".......................dd.d.....dkkkkd....k4444k...k44ww44k..k444444k..kk4444kk...kggggk....kggggk....k.kk.k.....k..k...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".............11.1.....111111....111111...11111111..11111111..11111111...111111....111111....1.11.1.....1..1.............", - "door": null - }, - "contentHash": "fnv1a:6e50e7b9" - }, - { - "id": "byljnniy6a36", - "name": "Kingfisher", - "category": "dynamic", - "subtype": "bird", - "width": 10, - "height": 8, - "pixels": "........................W........WWWM.....WnMMMMd..WWnMMssd...Wnnnnd......o.o...", - "faces": { - "right": "........................W........WWWM.....WnMMMMd..WWnMMssd...Wnnnnd......o.o...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..............1........1111.....1111111..11111111...111111......1.1.............", - "door": null - }, - "contentHash": "fnv1a:5eaf945f" - }, - { - "id": "ybigmijy6a39", - "name": "Pond Duck", - "category": "dynamic", - "subtype": "bird", - "width": 12, - "height": 10, - "pixels": "........................................ii.........i4444......i444iiid....i4444444d...iii4444d......4..4.......k....k...", - "faces": { - "right": "........................................ii.........i4444......i444iiid....i4444444d...iii4444d......4..4.......k....k...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "............................11.........11111......11111111....111111111...11111111......1..1.......1....1...............", - "door": null - }, - "contentHash": "fnv1a:aebe8348" - }, - { - "id": "uvnxnbmy6a3c", - "name": "Heron", - "category": "dynamic", - "subtype": "bird", - "width": 10, - "height": 16, - "pixels": "...................................3........333........33c......3333.....333ss.....33333......3333.......33........33........33........33.......k..k......k..k..", - "faces": { - "right": "...................................3........333........33c......3333.....333ss.....33333......3333.......33........33........33........33.......k..k......k..k..", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...............1........111........111......1111.....11111.....11111......1111.......11........11........11........11.......1..1......1..1......................", - "door": null - }, - "contentHash": "fnv1a:8a2d5c57" - }, - { - "id": "lg1jnrjy6a3f", - "name": "Sunset Koi", - "category": "dynamic", - "subtype": "fish", - "width": 12, - "height": 8, - "pixels": "........................................d.........ddhhhhdd...dhhMMMhhhR...dhhhhhhdd.....d.d.....", - "faces": { - "right": "........................................d.........ddhhhhdd...dhhMMMhhhR...dhhhhhhdd.....d.d.....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": true, - "particlePixels": [ - { - "x": 2, - "y": 4, - "c": "7", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "7", - "dir": "up" - }, - "depthPixels": "............................1.........11111111...1111111111...111111111.....1.1.................", - "door": null - }, - "contentHash": "fnv1a:ba7fa061" - }, - { - "id": "j3lfyriy6a3l", - "name": "Town Gardener", - "category": "dynamic", - "subtype": "human", - "width": 12, - "height": 16, - "pixels": "................................................................AA.........A55A........5MM5........55o55........oee........oeeee.......oeeee........ooo........oo.oo.......o...o......AA...AA...", - "faces": { - "right": "................................................................AA.........A55A........5MM5........55o55........oee........oeeee.......oeeee........ooo........oo.oo.......o...o......AA...AA...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "................11.........1111........1111........11111........111........11111.......11111........111........11.11.......1...1......11...11...................................................", - "door": null - }, - "contentHash": "fnv1a:5b105e7b" - }, - { - "id": "4jtf6ioy6a3q", - "name": "Lantern Courier", - "category": "dynamic", - "subtype": "human", - "width": 12, - "height": 16, - "pixels": "................................................................AA.........A55A........5MM5........5555.........hhhh.......hhhhh5......hhhh.5.......nnnn.......nn.nn.......n...n......AA...AA...", - "faces": { - "right": "................................................................AA.........A55A........5MM5........5555.........hhhh.......hhhhh5......hhhh.5.......nnnn.......nn.nn.......n...n......AA...AA...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "................11.........1111........1111........1111.........1111.......111111......1111.1.......1111.......11.11.......1...1......11...11...................................................", - "door": null - }, - "contentHash": "fnv1a:25a7719d" - }, - { - "id": "ix1a44vy6a3u", - "name": "Plaza Musician", - "category": "dynamic", - "subtype": "human", - "width": 14, - "height": 16, - "pixels": "...........................................................................AA...........A55A..........5MM5..........5555.........XXXXXX........Xttddd........Xttddd.........dkkd.........dk..kd........A....A.......AA....AA....", - "faces": { - "right": "...........................................................................AA...........A55A..........5MM5..........5555.........XXXXXX........Xttddd........Xttddd.........dkkd.........dk..kd........A....A.......AA....AA....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...................11...........1111..........1111..........1111.........111111........111111........111111.........1111.........11..11........1....1.......11....11............................................................", - "door": null - }, - "contentHash": "fnv1a:0c84b262" - }, - { - "id": "2igldehy6a3z", - "name": "Bridge Mechanic", - "category": "dynamic", - "subtype": "human", - "width": 14, - "height": 16, - "pixels": "...........................................................................AA...........A55A..........5MM5..........5555.........jjjjjj........jjwjjj........jjwjjd.........oooo.........oo.oo.........o...o........AA...AA.....", - "faces": { - "right": "...........................................................................AA...........A55A..........5MM5..........5555.........jjjjjj........jjwjjj........jjwjjd.........oooo.........oo.oo.........o...o........AA...AA.....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...................11...........1111..........1111..........1111.........111111........111111........111111.........1111.........11.11.........1...1........11...11.............................................................", - "door": null - }, - "contentHash": "fnv1a:48ca7020" - }, - { - "id": "ds7gpmty6a4a", - "name": "Harbor Clocktower", - "category": "static", - "subtype": "building", - "width": 32, - "height": 40, - "pixels": ".............................................................................................................................................................................................................................................................................oooooo.........................kkkkkkkk.......................kkkkkkkkkk........................kkkkkk..........................kkkkkk..........................kkkkkk.........................gggggggg........................gggggggg........................gggggggg........................gggggggg........................g444444g........................g400004g........................g400o04g........................g40oo04g........................g400004g........................g444444g........................gggggggg........................gggggggg........................gggggggg........................g55gg55g........................g55gg55g........................g55gg55g........................gggggggg........................gggggggg........................gggggggg........................gg55g55g........................gg55g55g........................gg55g55g........................gggggggg........................gggggggg.......................gggggggggg......................gggggggggg...........", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 15, - "y": 12, - "c": "5" - }, - { - "x": 16, - "y": 12, - "c": "5" - }, - { - "x": 13, - "y": 22, - "c": "5" - }, - { - "x": 18, - "y": 22, - "c": "5" - }, - { - "x": 14, - "y": 28, - "c": "5" - }, - { - "x": 17, - "y": 28, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".............111111.........................11111111.......................1111111111........................111111..........................111111..........................111111.........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111........................11111111.......................1111111111......................1111111111...........................................................................................................................................................................................................................................................................", - "door": { - "x": 15, - "y": 38 - } - }, - "contentHash": "fnv1a:84e7cd5a" - }, - { - "id": "fbd7df9y6a4u", - "name": "Glass Greenhouse", - "category": "static", - "subtype": "building", - "width": 28, - "height": 20, - "pixels": ".............................................................................................................................33.........................OOOO.......................333333.....................OOOOOOOO..............cccccccccccccccccccc........c333c333c333c333c333........c333c333c333c333c333........cccccccccccccccccccc........c333c333c333c3aac333........c333aa331111c3aac333........ccccaacc1111cccccccc........c33eee331111c3eee333........c33eee331111c3eee333........cccccccc1111cccccccc...............kkkkkk......................kkkkkk...........", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 10, - "y": 10, - "c": "1" - }, - { - "x": 13, - "y": 10, - "c": "1" - }, - { - "x": 16, - "y": 10, - "c": "1" - } - ], - "lightColor": "1", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".................................................................................................11.........................1111.......................111111.....................11111111..............11111111111111111111........11111111111111111111........11111111111111111111........11111111111111111111........11111111111111111111........11111111111111111111........11111111111111111111........11111111111111111111........11111111111111111111........11111111111111111111...............111111......................111111.......................................", - "door": { - "x": 13, - "y": 18 - } - }, - "contentHash": "fnv1a:2a1a7c75" - }, - { - "id": "170nhhsy6a5q", - "name": "Steam Workshop", - "category": "static", - "subtype": "building", - "width": 40, - "height": 24, - "pixels": "..................................................kbbk.............kbbbk..................kbbk.............kbbbk..................kbbk.............kbbbk..................kkkk.............kkkkk..................kkkk.............kkkkk..................kkkk....ssssssssskkkkksss...........ooookkkkoooossssssssskkkkksss...........ooookkkkoooossssssssskkkkksss.........kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk........kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk........kkggggggggggkkggggggggggggggggkk........kkggg999gg999kgggggg999gg999ggkk........kkggg999gg999kgggggg999gg999ggkk........kkggg999gg999kgggggg999gg999ggkk........kkggggggggggkkggggggggggggggggkk........kkggggggggggkkwwwwwwggggggggggkk........kkggggggggggkkwwwwwwggggggggggkk........kkggggggggggkkwwwwwwggggggggggkk........kkkkkkkkkkkkkkwwwwwwkkkkkkkkkkkk........kkkkkkkkkkkkkkwwwwwwkkkkkkkkkkkk......oooooooooooooooooooooooooooooooooooo....oooooooooooooooooooooooooooooooooooo....oooooooooooooooooooooooooooooooooooo..", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 13, - "y": 14, - "c": "9" - }, - { - "x": 18, - "y": 14, - "c": "9" - }, - { - "x": 27, - "y": 14, - "c": "9" - }, - { - "x": 32, - "y": 14, - "c": "9" - } - ], - "lightColor": "9", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..........1111.............11111..................1111.............11111..................1111.............11111..................1111.............11111..................1111.............11111..................1111....11111111111111111...........11111111111111111111111111111...........11111111111111111111111111111.........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111........11111111111111111111111111111111......111111111111111111111111111111111111....111111111111111111111111111111111111....111111111111111111111111111111111111..........................................", - "door": { - "x": 20, - "y": 22 - } - }, - "contentHash": "fnv1a:eb5424f2" - }, - { - "id": "k18uukoy6a6o", - "name": "Canal Bridge", - "category": "static", - "subtype": "building", - "width": 48, - "height": 16, - "pixels": "...........................................................................................................................................................................................................................................................................................................88..88..88..88..88..88..88..88.................8...8...8...8...8...8...8...8...................8...8...8...8...8...8...8...8...................8...8...8...8...8...8...8...8.................gg8ggg8ggg8ggg8ggg8ggg8ggg8ggg8g................gg8ggg8ggg8ggg8ggg8ggg8ggg8ggg8g..............ggggggggggggjjjjjjjjjjjjgggggggggggg............ggggggggggjjjjjjjjjjjjjjjjgggggggggg.........ooooooooooooooojjjjjjjjjjjjooooooooooooooo......ooooooooooooooooojjjjjjjjooooooooooooooooo...", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...........................................................................................................................................................................................................11..11..11..11..11..11..11..11.................1...1...1...1...1...1...1...1...................1...1...1...1...1...1...1...1...................1...1...1...1...1...1...1...1.................11111111111111111111111111111111................11111111111111111111111111111111..............111111111111111111111111111111111111............111111111111111111111111111111111111.........111111111111111111111111111111111111111111......111111111111111111111111111111111111111111...................................................................................................", - "door": null - }, - "contentHash": "fnv1a:508d58cb" - }, - { - "id": "sbl2ma4y6a78", - "name": "Grand Fountain", - "category": "static", - "subtype": "building", - "width": 24, - "height": 24, - "pixels": "...........................................................33.....................3333...................3SSSS3...................3SS3...................33SS33.................3SSSSSS3.................3SSSS3................ggg3333ggg.............gOOOggggOOOg...........gOOSSSSSSOOOg..........gOOSSSSSSSSOOOg.........gOSSSS33SSSSOOg.........gOOSSSSSSSSOOOg..........gOOSSSSSSOOOg............gOOOggggOOg..............ggggggggg...............kkkkkkkkk..............kkkggggkkkk............kkggggggggkk...........kkkkkkkkkkkkkk..........kkSSSSSSSSSSkk...........kkkkkkkkkkkk.......", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 11, - "y": 7, - "c": "3" - }, - { - "x": 12, - "y": 7, - "c": "3" - }, - { - "x": 9, - "y": 12, - "c": "3" - }, - { - "x": 14, - "y": 12, - "c": "3" - } - ], - "lightColor": "3", - "hasParticles": true, - "particlePixels": [ - { - "x": 11, - "y": 5, - "c": "3", - "dir": "up" - }, - { - "x": 12, - "y": 5, - "c": "3", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "3", - "dir": "up" - }, - "depthPixels": "...................................11.....................1111...................111111...................1111...................111111.................11111111.................111111................1111111111.............111111111111...........1111111111111..........111111111111111.........111111111111111.........111111111111111..........1111111111111............11111111111..............111111111...............111111111..............11111111111............111111111111...........11111111111111..........11111111111111...........111111111111...............................", - "door": null - }, - "contentHash": "fnv1a:664416a8" - }, - { - "id": "965de5py6a7s", - "name": "Rocket Monument", - "category": "static", - "subtype": "building", - "width": 20, - "height": 32, - "pixels": ".............................hh.................h33h...............h3333h..............h3333h.............hh3bb3hh............h33bb33h............h333333h............h33kk33h............h33kk33h............h333333h...........hh333333hh..........h33333333h..........h33333333h..........h33hh3333h..........h33hh3333h..........h33333333h..........hh333333hh...........h333333h...........hh333333hh.........hhh333333hhh........h3333333333h........h3333333333h........hhhh3333hhhh.........kkk3333kkk..........kk999999kk..........kkdd99ddkk..........kk999999kk..........kkkkkkkkkk...........ooo..ooo...........ooo....ooo..........oo......oo.....", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 9, - "y": 8, - "c": "5" - }, - { - "x": 10, - "y": 8, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": true, - "particlePixels": [ - { - "x": 8, - "y": 27, - "c": "9", - "dir": "up" - }, - { - "x": 11, - "y": 27, - "c": "d", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "9", - "dir": "up" - }, - "depthPixels": ".........11.................1111...............111111..............111111.............11111111............11111111............11111111............11111111............11111111............11111111...........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111...........11111111...........1111111111.........111111111111........111111111111........111111111111........111111111111.........1111111111..........1111111111..........1111111111..........1111111111..........1111111111...........111..111...........111....111..........11......11.........................", - "door": { - "x": 9, - "y": 30 - } - }, - "contentHash": "fnv1a:bed0e859" - }, - { - "id": "j2lv8uvy6a8c", - "name": "Arcade Booth", - "category": "static", - "subtype": "building", - "width": 24, - "height": 20, - "pixels": "....................................................................................................XXXXXXXXXXXXXXXX........XXXXXXNXXQXXOXXX.......sXXXXXXXXXXXXXXXXs......ssssssssssssssssss......ssszzzzzzzzzzzzsss......ssszWWWWWWWWWWzsss......ssszWWWWWWWWWWzsss......ssszWWWWWWWWWWzsss......ssszWWWWWWWWWWzsss......ssszzzzzzzzzzzzsss......ssssssssssssssssss......sssssswwwwwwssssss......sssssswwwwwwssssss...........oooooooo................oooooooo..........wwwwwwwwwwwwwwwwwwww..", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 7, - "y": 10, - "c": "b" - }, - { - "x": 16, - "y": 10, - "c": "b" - }, - { - "x": 11, - "y": 6, - "c": "b" - } - ], - "lightColor": "b", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "............................................................................1111111111111111........1111111111111111.......111111111111111111......111111111111111111......111111111111111111......111111111111111111......111111111111111111......111111111111111111......111111111111111111......111111111111111111......111111111111111111......111111111111111111......111111111111111111...........11111111................11111111..........11111111111111111111..........................", - "door": { - "x": 11, - "y": 18 - } - }, - "contentHash": "fnv1a:7553c015" - }, - { - "id": "qamstg6y6a8u", - "name": "Chess Knight Statue", - "category": "static", - "subtype": "building", - "width": 16, - "height": 24, - "pixels": "......................................88.............8008...........800008..........800f008.........8000088........80000008........80080008........80088008.........800008..........800008.........88000088.......8880000888......8000000008......8888008888........kk00kk.........kkk00kkk.......kkkkkkkkkk......kkkggggkkk......kkggggggkk.....kkkkkkkkkkkk....kffffffffffk.....kkkkkkkkkk....", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 7, - "y": 8, - "c": "3" - }, - { - "x": 8, - "y": 8, - "c": "3" - } - ], - "lightColor": "3", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "......................11.............1111...........111111..........1111111.........1111111........11111111........11111111........11111111.........111111..........111111.........11111111.......1111111111......1111111111......1111111111........111111.........11111111.......1111111111......1111111111......1111111111.....111111111111....111111111111.....1111111111....................", - "door": null - }, - "contentHash": "fnv1a:04de304a" - }, - { - "id": "cthsuy3y6a9h", - "name": "Desert Train", - "category": "static", - "subtype": "building", - "width": 44, - "height": 16, - "pixels": "................................................................................................33........................................77..........................................77.........................................kkkk...kkkkkkkkkk...kkkkkkkkkk..............kkkk...kkkkkkkkkk...kkkkkkkkkk..............kk55..hhhhhhhhhhhhhhhhhhhhhhhhhh...........kkk55kkhh999hh999hhhhhh999hh999hhkkkkk......kkkkkkkhh999hh999hhhhhh999hh999hhkkkkk......kkkkkkkhhhhhhhhhhhhhhhhhhhhhhhhhhkkkkk......kkAkkkkhhhhAhhhhhAhhhhhhhAhhhhhAhkkkAk.......AAA...oooAAAoooAAAoooooAAAoooAAA..AAA......AAgAA....AAgAA.AAgAA...AAgAA.AAgAAAAgAA...gggggggggggggggggggggggggggggggggggggggg..oooooooooooooooooooooooooooooooooooooooooooo", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 7, - "y": 7, - "c": "9" - }, - { - "x": 13, - "y": 8, - "c": "9" - }, - { - "x": 19, - "y": 8, - "c": "9" - }, - { - "x": 28, - "y": 8, - "c": "9" - }, - { - "x": 35, - "y": 8, - "c": "9" - } - ], - "lightColor": "9", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "................................................................................................11........................................11..........................................11.........................................1111...1111111111...1111111111..............1111...1111111111...1111111111..............1111..11111111111111111111111111...........11111111111111111111111111111111111111......11111111111111111111111111111111111111......11111111111111111111111111111111111111......11111111111111111111111111111111111111.......111...11111111111111111111111111..111......11111....11111.11111...11111.1111111111...1111111111111111111111111111111111111111..11111111111111111111111111111111111111111111", - "door": null - }, - "contentHash": "fnv1a:8604fff2" - }, - { - "id": "zl4ia2fy6a9y", - "name": "Tea Robot", - "category": "dynamic", - "subtype": "human", - "width": 12, - "height": 16, - "pixels": ".................................................................bb.........b33b........b3s3b.......b333b........ddd........dkkkd.......dkOkdO.......dkkd.......g.ggg......gg..gg......s....s...", - "faces": { - "right": ".................................................................bb.........b33b........b3s3b.......b333b........ddd........dkkkd.......dkOkdO.......dkkd.......g.ggg......gg..gg......s....s...", - "left": "mirror" - }, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 8, - "y": 8, - "c": "O" - } - ], - "lightColor": "O", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".................11.........1111........11111.......11111........111........11111.......111111.......1111.......1.111......11..11......1....1...................................................", - "door": null - }, - "contentHash": "fnv1a:e65ea2ff" - }, - { - "id": "bomdmjgy6aa2", - "name": "Balloon Vendor", - "category": "dynamic", - "subtype": "human", - "width": 14, - "height": 18, - "pixels": "..........................................................Q..N..b.......Q..N..b.......QQ.NN.bb.........k.............A............AkkA..........kMMk..........kkkk.........hhhhhh........hhhhhh.........bb.bb........bb...b........A....A.......AA....AA....", - "faces": { - "right": "..........................................................Q..N..b.......Q..N..b.......QQ.NN.bb.........k.............A............AkkA..........kMMk..........kkkk.........hhhhhh........hhhhhh.........bb.bb........bb...b........A....A.......AA....AA....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..1..1..1.......1..1..1.......11.11.11.........1.............1............1111..........1111..........1111.........111111........111111.........11.11........11...1........1....1.......11....11............................................................", - "door": null - }, - "contentHash": "fnv1a:d3a90d8d" - }, - { - "id": "tfwksj6y6aa6", - "name": "Jelly Comet", - "category": "dynamic", - "subtype": "animal", - "width": 12, - "height": 12, - "pixels": "..........................................b.........bbbbb.....bb33333bb...b33nnn33b...b3333333b....b33333b......bbbbb........7.7........7...7...", - "faces": { - "right": "..........................................b.........bbbbb.....bb33333bb...b33nnn33b...b3333333b....b33333b......bbbbb........7.7........7...7...", - "left": "mirror" - }, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 7, - "y": 4, - "c": "7" - } - ], - "lightColor": "7", - "hasParticles": true, - "particlePixels": [ - { - "x": 2, - "y": 6, - "c": "3", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "3", - "dir": "up" - }, - "depthPixels": "..................1.........11111.....111111111...111111111...111111111....1111111......11111........1.1........1...1...........................", - "door": null - }, - "contentHash": "fnv1a:4b26e093" - }, - { - "id": "p3gxv6py6aaa", - "name": "Courier Bike", - "category": "dynamic", - "subtype": "human", - "width": 16, - "height": 12, - "pixels": "......................AA.............A55A............5MM5............5555...........WWWWWW..........Wooo.W..........W.oooW...........s.s...........44...44........44M4444M4........44...44......", - "faces": { - "right": "......................AA.............A55A............5MM5............5555...........WWWWWW..........Wooo.W..........W.oooW...........s.s...........44...44........44M4444M4........44...44......", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "......................11.............1111............1111............1111...........111111..........1111.1..........1.1111...........1.1...........11...11........111111111........11...11......", - "door": null - }, - "contentHash": "fnv1a:01dfc817" - }, - { - "id": "x451owfy6aad", - "name": "Great Cedar", - "category": "static", - "subtype": "nature", - "width": 24, - "height": 32, - "pixels": "..................................iiii..................iieeeeii..............iieee66eeii............ieee6666eeeii..........ieee66ii66eeei.........ieee66iiii66eeei........iee66iiiiii66eei.......iee66iimmmmii66eei......iee6iimmmmmmii6eei.....iee66immiiiimmi66eei....iee6iimiiiiiimii6eei....ie66iiiiiiiiiiii66ei....ie6iiiiiiiiiiiiii6ei...ie66iiiiiiiiiiiiii66ei..ie6iiiiiiiiiiiiiiii6ei..ie6iiiiiiiiiiiiiiii6ei...ie6iiiiiiiiiiiiii6ei....ie66iiiiiiiiiiii66ei....iee6iiiiiiiiiiii6eei....iee66iiiiiiiiii66eei.....iee6iiiiiiiiii6eei......iee66iiiiiiii66eei.......iee66iiiiii66eei.........ieee66ii66eeei...........ieee6666eeei.............ieeeppeeei...............ieeppeei................pppppppp...............pppppppppp..............ppppddpppp.............tttttttttttt.......", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..........1111..................11111111..............11111111111............11111111111111.........111111111111111........11111111111111111.......11111111111111111......1111111111111111111.....1111111111111111111....111111111111111111111...111111111111111111111...111111111111111111111...111111111111111111111..11111111111111111111111.11111111111111111111111.11111111111111111111111..111111111111111111111...111111111111111111111...111111111111111111111...111111111111111111111....1111111111111111111.....1111111111111111111......11111111111111111........111111111111111..........1111111111111............1111----111..............111----11...............--------...............----------..............----------.............------------...............................", - "door": null - }, - "contentHash": "fnv1a:47b0b007" - }, - { - "id": "1rmrha4y6aaw", - "name": "Moonfall Cascade", - "category": "static", - "subtype": "nature", - "width": 24, - "height": 32, - "pixels": "..............................iiiiiiiiiiii..........iiggggggggggggii.......iggg77333377ggggi......igg773333333377gggi.....ig73333SSSS33337gi.....ig7333SSSSSSSS337gi.....ig333SSSffffSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSSf33fSSS33gi.....ig333SSS3333SSS33gi.....ig333333333333333gi......ig3333773377333gi.......ig3337733337733gi........ig33333333333gi..........ig3333333333gi..........ii3333333333ii...........33ffffffff33...........33ffffffffff33..........3ffffffffffff3..........33333333333333..........SSSSSSSSSSSSSS......", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": true, - "particlePixels": [ - { - "x": 9, - "y": 20, - "c": "3", - "dir": "up" - }, - { - "x": 14, - "y": 20, - "c": "3", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "3", - "dir": "up" - }, - "depthPixels": "......111111111111..........1111111111111111.......11111111111111111......1111111111111111111.....1111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....1111111----1111111.......111111----111111........111111----111111.........11111----11111...........1111----1111............1111----1111.............------------...........--------------..........--------------..........--------------..........--------------..............................", - "door": null - }, - "contentHash": "fnv1a:836a9d7d" - }, - { - "id": "omsnif2y6abh", - "name": "Sunblossom Gate", - "category": "static", - "subtype": "nature", - "width": 24, - "height": 24, - "pixels": "................................NNNN..................NNVVVVNN...............NVVVeeVVVN.............NVVVeeeeVVVN...........NNVVee11eeVVNN.........NNVVee1111eeVVNN........NVVVe111111eVVVN.......NVVVe11....11eVVVN......NVVVe1......1eVVVN......NVVVe1......1eVVVN......NVVVe1......1eVVVN......NVVVe1......1eVVVN......NVVVe1......1eVVVN......NVVVe1......1eVVVN.......NVVVee....eeVVVN........NNVVVeeeeeeVVNN..........NNVVkkkkkkVNN............kkkkooookkk.............kkkkooookkk.............kkkkooookkk.............kkkkkkkkkkk..............eeeeeeee................eeeeeeee...........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........1111..................11111111...............1111111111.............111111111111...........11111111111111.........1111111111111111........1111111111111111.......1111111....1111111......111111......111111......111111......111111......111111......111111......111111......111111......111111......111111......111111......111111.......111111....111111........1111111111111111.........11111------111...........----1111----............----1111----............----1111----............-------------............--------................--------...................................", - "door": null - }, - "contentHash": "fnv1a:b728a136" - }, - { - "id": "kd1fy1my6abs", - "name": "Echo Cavern", - "category": "static", - "subtype": "nature", - "width": 32, - "height": 20, - "pixels": "..........................................gggggggggg...................ggggiiiiiiiigggg..............ggiiiiiAAAAiiiiiigg............giiiiAAAAAAAAAAiiiig...........giiiAAAAAAAAAAAAAAiiig.........giiAAAAAAAA..AAAAAAAAiig.......giiAAAAAA......AAAAAAAiig.......giAAAAA..........AAAAAAig.......giAAAA............AAAAAig.......giAAA..............AAAAig.......giAAA..............AAAAig.......giAAAA............AAAAAig.......giAAAAA..........AAAAAAig.......giiAAAAAA......AAAAAAAiig........giiAAAAAAAA..AAAAAAAAiig.........giiiAAAAAAAAAAAAAAiiig...........giiiiAAAAAAAAAAiiiig.............ggiiiiiAAAAiiiiigg...............oooooooooooooooo..........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..........................................1111111111...................1111111111111111..............11111111111111111111...........111111111111111111111..........11111111111111111111111........111111111111..1111111111.......1111111111......111111111.......11111111..........11111111......1111111............1111111......111111..............111111......111111..............111111......1111111............1111111......11111111..........11111111......1111111111......111111111........111111111111..1111111111.........11111111111111111111111..........111111111111111111111............1111111111111111111..............----------------..........", - "door": null - }, - "contentHash": "fnv1a:fb73fb21" - }, - { - "id": "shtpgwfy6ac5", - "name": "Worldroot Shrine", - "category": "static", - "subtype": "nature", - "width": 28, - "height": 28, - "pixels": "............iiii......................iieeeeii..................iieee66eeeii...............ieee666666eeei.............iee66iiiiii66eei...........iee6iiiiiiiiii6eei.........iee6iiiikkiiiiii6eei........iee6iiikkkkiiiii6eei.......iee6iikkkkkkkkiii6eei.......iee6iikkkoookkkii6eei......iee6iikkkoo66ookkii6eei.....iee6iikkoo6666ookii6eei.....iee6iikkoo6556ookii6eei.....iee6iikkoo5555ookii6eei.....iee6iikkoo6556ookii6eei.....iee6iikkoo6666ookii6eei.....iee6iikkkoo66ookkii6eei......iee6iikkkoookkkii6eei.......iee6iikkkkkkkkiii6eei........iee6iiikkkkiiiii6eei........iee6iiiikkiiiiii6eei.........iee6iiiiiiiiii6eei...........iee66iiiiii66eei.............ieee66kk66eeei...............ieeekkkkeeii.................kkkkkkkkkk.................kkkkcckkkkkk...............oooooooooooooo.......", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 13, - "y": 12, - "c": "5" - }, - { - "x": 14, - "y": 12, - "c": "5" - }, - { - "x": 12, - "y": 13, - "c": "5" - }, - { - "x": 15, - "y": 13, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "............1111......................11111111..................111111111111...............11111111111111.............1111111111111111...........111111111111111111.........11111111111111111111........11111111111111111111.......1111111111111111111111......1111111111111111111111.....111111111111111111111111....111111111111111111111111....111111111111111111111111....111111111111111111111111....111111111111111111111111....111111111111111111111111....111111111111111111111111.....1111111111111111111111......1111111111111111111111.......11111111111111111111........11111111111111111111.........111111111111111111...........1111111111111111.............11111111111111...............1111----1111.................----------.................------------...............----------------.....", - "door": null - }, - "contentHash": "fnv1a:4a077f44" - }, - { - "id": "kmjtd3ly6acp", - "name": "Moon Jelly Aquarium", - "category": "static", - "subtype": "building", - "width": 24, - "height": 28, - "pixels": "...................................55.....................5005...................5A00A5.................ATSSSSTA...............ATSOOOOSTA.............ATSOO00OOSTA............ASOOOOOOOOSA...........ATSOOQQQQOOSTA..........ASOOQQ55QQOOSA.........ATSOOQ5555QQOOSTA.......ASOOQ555555QQOOSA.......ASOOQ555555QQOOSA......ATSOOQ555555QQOOSTA.....A.SOOQTTTTTTQQOOS.A.....A.A55TAAAAAAT55A.A......A.kg5T5....5T5gk.A.......Akkg5......5gkkA........Aggg5......5gggA........Aggg5..ww..5gggA........Aggg5.wwww.5gggA........Aggg55555555gggA........AgggkkkkkkkkgggA........AkkkkgwwwwgkkkkA.........Akkkgw...wgkkkA.........Awwwgg...ggwwwA..........AwwwwAAAwwwwA............AAAAAAAAAAA......", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 10, - "y": 10, - "c": "5" - }, - { - "x": 13, - "y": 10, - "c": "5" - }, - { - "x": 11, - "y": 20, - "c": "5" - }, - { - "x": 12, - "y": 20, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...........11.....................1111...................111111.................11111111...............1111111111.............111111111111............111111111111...........111111..111111..........11111....11111.........111111....111111........111111....111111........111111....111111.......1111111....1111111......111111......111111......11..11......11..11......11..11......11..11.......11..1......1..11........11..1......1..11........11111..--..11111........11111.----.11111........1111111111111111........11111------11111........11111------11111.........1111------1111..........11111....11111...........11111..11111.............1111111111...............................", - "door": { - "x": 11, - "y": 24 - } - }, - "contentHash": "fnv1a:2ce3fef7" - }, - { - "id": "uzvrmc4y6adk", - "name": "Aurora Observatory", - "category": "static", - "subtype": "building", - "width": 28, - "height": 28, - "pixels": "........................................AAAA......................AAXXXXAA...................AXX7777XXA.................AXX777777XXA...............AXX77bbbb77XXA.............AXX7bbbbbbbb7XXA............AXXbbbbbbbbbbXXA...........AXXbbbbbbbbbbbbXXA..........AXXbbbbbbbbbbbbXXA..........AXXXXXXXXXXXXXXXXA.........AAkkkkkkAAkkkkkkkkAA........AkkkkkkA..AkkkkkkkkA.......AAkkk11kAAAAkk11kkkAA.......Akkk1111kkkk1111kkkkA.......Akk111111kk111111kkkA.......Akk11ww11kk11ww11kkkA.......Akk111111kk111111kkkA.......AkkkkkkkkkkkkkkkkkkkA.......AkkcccccccccccccckkkA.......AkkckkkcckkkkcckkckkA.......AkkckkkcckkkkcckkckkA.......AkkcccccccccccccckkkA.......AkkcckkkcccccckkckkkA.......AkkcckkkccwwcckkckkkA.......AkkccccccwwcccccckkkA........AkkkkkkkkwwkkkkkkkA.........AAAAAAAAAAAAAAAAAA......", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 13, - "y": 2, - "c": "3" - }, - { - "x": 14, - "y": 2, - "c": "3" - }, - { - "x": 9, - "y": 13, - "c": "3" - }, - { - "x": 18, - "y": 13, - "c": "3" - }, - { - "x": 13, - "y": 23, - "c": "3" - } - ], - "lightColor": "3", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "............1111......................11111111...................1111111111.................111111111111...............11111111111111.............1111111111111111............1111111111111111...........111111111111111111..........111111111111111111..........111111111111111111.........11111111111111111111........11111111..1111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111.......111111111111111111111........1111111111111111111.........111111111111111111..................................", - "door": { - "x": 13, - "y": 24 - } - }, - "contentHash": "fnv1a:6316cef7" - }, - { - "id": "h82at0uy6ael", - "name": "Hearthwind Mill", - "category": "static", - "subtype": "building", - "width": 24, - "height": 28, - "pixels": "..................................AAAA...................A1111A..................A1bb1A.................A11bb11A................AAAbbAAA...............A..AbbA..A..........AAAAA..AbbA..AAAAA.....AkkkkAAAAbbAAAAkkkkA....AkkkkkkAAbbAAkkkkkkA....AkkkkkkkAAAAkkkkkkkA....AkkkkkkkkkkkkkkkkkkA....AccccccccccccccccccA....AccccccccccccccccccA....Acc55cccccccccc55ccA....Ac5555cccAAcc5555ccA....Acc55ccccAAcccc55ccA....Accccc888888cccccccA....Acc888kk888888kk8ccA....Acc8kkkk88ww88kkk8cA....Acc8kkkk88ww88kkk8cA....Acc8888888ww888888cA....Accc888888ww8888cccA....AcccccccccwwcccccccA....AcccccccccwwcccccccA....AcccccccccwwcccccccA....AcccccccAAAAcccccccA.....AAAAAAAA..AAAAAAAA...", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 8, - "y": 13, - "c": "5" - }, - { - "x": 15, - "y": 13, - "c": "5" - }, - { - "x": 11, - "y": 18, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..........1111...................111111..................111111.................11111111................11111111...............1..1111..1..........11111..1111..11111.....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111.....11111111..11111111...........................", - "door": { - "x": 11, - "y": 22 - } - }, - "contentHash": "fnv1a:3c976c7b" - }, - { - "id": "adu51aey6afj", - "name": "Tideglass Lighthouse", - "category": "static", - "subtype": "building", - "width": 20, - "height": 36, - "pixels": "........AAAA...............A5555A..............A5335A.............A553355A............AAA33AAA.............A.33.A..............A.bb.A.............AA.bb.AA............AMMMMMMA............AMM11MMA...........AAMM11MMAA..........AMMMMMMMMA..........AMM8888MMA..........AMM8kk8MMA..........AMM8888MMA.........AAMMMMMMMMAA........AMMMM11MMMMA........AMMMM11MMMMA........AMMMMMMMMMMA........AMMM8888MMMA........AMMM8kk8MMMA........AMMM8888MMMA.......AAMMMMMMMMMMAA......AMMMMMMMMMMMMA......AMMM88MM88MMMA......AMMMkkMMkkMMMA......AMMM88MM88MMMA.....AAMMMMMMMMMMMMAA....AMMMMMMMMMMMMMMA....AMMMMMMwwMMMMMMA....AMMMMMMwwMMMMMMA....AMMMM88ww88MMMMA....AMMMMkkwwkkMMMMA....AMMMM88ww88MMMMA.....AAAAAAwwAAAAAA...........AAAA........", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 8, - "y": 2, - "c": "1" - }, - { - "x": 9, - "y": 2, - "c": "1" - }, - { - "x": 8, - "y": 16, - "c": "1" - } - ], - "lightColor": "1", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........1111...............111111..............111111.............11111111............11111111.............1.11.1..............1.11.1.............11.11.11............11111111............11111111...........1111111111..........1111111111..........1111111111..........1111111111..........1111111111.........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111.......11111111111111......11111111111111......11111111111111......11111111111111......11111111111111.....1111111111111111....1111111111111111....1111111111111111....1111111111111111....1111111111111111....1111111111111111....1111111111111111.....11111111111111...........1111........", - "door": { - "x": 8, - "y": 33 - } - }, - "contentHash": "fnv1a:a36dc353" - }, - { - "id": "yz6h7yry6agj", - "name": "Mossrail Depot", - "category": "static", - "subtype": "building", - "width": 32, - "height": 20, - "pixels": "....................................AAAAAAAAAAAAAAAAAAAAAAAA.......A555555555555555555555555A.....A55AAAAAAAAAAAAAAAAAAAAAA55A....A55AkkkkkkkkkkkkkkkkkkkkA55A....A55AkkkkkkkkkkkkkkkkkkkkA55A....A55AAAAAAAAAAAAAAAAAAAAAA55A...AA88888888888888888888888888AA..A8888888888888888888888888888A..A885555888855558888555588888A...A885555888855558888555588888A...A888888888888888888888888888A...A88cccccccccccccccccccccc888A...A88ckkkcccccccccccccckkkc888A...A88cccccccccAAAAccccccccc888A...A88ckkkcccccAwwAccccckkkc888A...A88cccccccccAwwAccccccccc888A...A888888888888ww8888888888888A...A888888888888ww8888888888888A...AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 6, - "y": 8, - "c": "5" - }, - { - "x": 13, - "y": 8, - "c": "5" - }, - { - "x": 20, - "y": 8, - "c": "5" - }, - { - "x": 26, - "y": 8, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "....111111111111111111111111.......11111111111111111111111111.....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111...111111111111111111111111111111..111111111111111111111111111111..11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111...111111111111111111111111111111.................................", - "door": { - "x": 15, - "y": 16 - } - }, - "contentHash": "fnv1a:b930d92b" - }, - { - "id": "rc1vh22y6ahk", - "name": "Lantern Library", - "category": "static", - "subtype": "building", - "width": 28, - "height": 24, - "pixels": "...............................................................AAAAAAAAAAAAAA.............A99999999999999A...........A99AAAAAAAAAAAA99A..........A9AooooooooooooA9A.........AAAAAAAAAAAAAAAAAAAA........AkkkkkkkkkkkkkkkkkkA.......AAkkk111kkAAkk111kkkAA......Akkk1111kkkkkk1111kkkA......Akkkk11kkkkkkkk11kkkkA......AkkkkkkkkkkkkkkkkkkkkA......Akk8888888888888888kkA......Akk8ooo88oo88oo88oo8kA......Akk8888888888888888kkA......Akk8ooo88oo88oo88oo8kA......Akk8888888888888888kkA......Akk88oo88888888oo88kkA......Akk88oo888AAw88oo88kkA......Akk8888888AAw888888kkA......AkkkkkkkkkkwwkkkkkkkkA......AkkkkkkkkkkwwkkkkkkkkA.......AkkkkkkkkAAAAkkkkkkA........AAAAAAAAA....AAAAAA.....", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 8, - "y": 7, - "c": "5" - }, - { - "x": 19, - "y": 7, - "c": "5" - }, - { - "x": 13, - "y": 16, - "c": "5" - }, - { - "x": 14, - "y": 16, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......11111111111111.............1111111111111111...........111111111111111111..........111111111111111111.........11111111111111111111........11111111111111111111.......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111.......11111111111111111111........111111111....111111.............................................................", - "door": { - "x": 13, - "y": 18 - } - }, - "contentHash": "fnv1a:ae675812" - }, - { - "id": "zzcxm83y6aij", - "name": "Velvet Theater", - "category": "static", - "subtype": "building", - "width": 28, - "height": 20, - "pixels": "............................................................AAAAAAAAAAAAAAAAAAAA.......A99999999999999999999A.....A99AAAAAAAAAAAAAAAAAA99A....A9AppppppppppppppppppA9A...AAAAAAAAAAAAAAAAAAAAAAAAAA..AooooooooooooooooooooooooA..Aoo55555555oooo55555555ooA..Aoo5QQQQQ55oooo55QQQQQ5ooA..Aoo55555555oooo55555555ooA..AooooooooooooooooooooooooA..AooccccccccccccccccccccooA..AoocooccooccccccooccoocooA..AoocccccccccAAcccccccccooA..AoocooccooccAAAAccooccocooA.AoocccccccccAAAAccccccccooA.AoooooooooooAAAooooooooooA..AooooooooooAAAAAoooooooooA..AAAAAAAAAAAAAAAAAAAAAAAAAA.", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 13, - "y": 14, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "....11111111111111111111.......1111111111111111111111.....111111111111111111111111....111111111111111111111111...11111111111111111111111111..11111111111111111111111111..11111111111111111111111111..11111111111111111111111111..11111111111111111111111111..11111111111111111111111111..11111111111111111111111111..11111111111111111111111111..11111111111111111111111111..111111111111111111111111111.111111111111111111111111111.11111111111111111111111111..11111111111111111111111111..11111111111111111111111111.........................................................", - "door": { - "x": 13, - "y": 16 - } - }, - "contentHash": "fnv1a:835a5a33" - }, - { - "id": "5n309pry6ajd", - "name": "Bloom Bridge Gate", - "category": "static", - "subtype": "nature", - "width": 24, - "height": 24, - "pixels": "..........AAAA..................AA9999AA...............A99QQQQ99A.............A99Q9999Q99A...........A99Q99AA99Q99A.........AA9999A..A9999AA.......AAkkkkkA..AkkkkkAA.....AAkkkAAkkAAkkAAkkkAA....AkkkA..AkkkkA..AkkkA....AkkA....AkkA....AkkA....AkkA....AkkA....AkkA....AkkA....AkkA....AkkA....AkkAAAAAAkkAAAAAAkkA....AkkaaaaaakkaaaaaakkA....AkkaiiiaakkaaiiiakkA....AkkaaaaaakkaaaaaakkA....AkkQQQQQQkkQQQQQQkkA....AkkQQ99QQkkQQ99QQkkA....AkkQQQQQQkkQQQQQQkkA.....AAkkkkkkkkkkkkkkAA.......AAAAAAkkAAAAAAAA............AAkkAA..................AkkkkA..................AAAAAA..........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": true, - "particlePixels": [ - { - "x": 7, - "y": 2, - "c": "P", - "dir": "up" - }, - { - "x": 16, - "y": 2, - "c": "P", - "dir": "up" - } - ], - "particleConfig": { - "enabled": true, - "c": "P", - "dir": "up" - }, - "depthPixels": "..........1111..................11111111...............1111111111.............111111111111...........11111111111111.........1111111..1111111.......11111111..11111111.....11111111111111111111....11111..111111..11111....1111....1111....1111....1111....1111....1111....1111....1111....1111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111.....111111111111111111.......1111111111111111............111111..................111111..................111111..........", - "door": null - }, - "contentHash": "fnv1a:27609372" - }, - { - "id": "dlz3yc3y6ak1", - "name": "Starreef Submarine", - "category": "static", - "subtype": "ship", - "width": 32, - "height": 16, - "pixels": ".............................................................................................................AAAAAA.......................AAA999999AAA..................AA997777777799AA..............AA9977WWWWWWWW7799AA...........A9977WWWWWWWWWWWW7799A.........A997WWWWWAAAAWWWWWW799A........A997WWWWWA3333AWWWWW799A.......AA99WWWWWWA3bb3AWWWWWW99AA.....AA999999999A3333A999999999AA....AkkkkkkkkkkAAAAAAkkkkkkkkkkkA...Akkkk11kkkkkkkkkkkk11kkkkkkkA...AAkkkkkkkkkkkkkkkkkkkkkkkkkAA....AAAAAAAAAAAAAAAAAAAAAAAAAA....", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 15, - "y": 7, - "c": "7" - }, - { - "x": 20, - "y": 7, - "c": "7" - } - ], - "lightColor": "7", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".............................................111111.......................111111111111..................1111111111111111..............11111111111111111111...........1111111111111111111111.........11111111111111111111111........111111111111111111111111.......11111111111111111111111111.....1111111111111111111111111111....11111111111111111111111111111...11111111111111111111111111111...11111111111111111111111111111....11111111111111111111111111....................................................................", - "door": null - }, - "contentHash": "fnv1a:0d858047" - }, - { - "id": "3na95zgy6akh", - "name": "Astrolabe Courier", - "category": "dynamic", - "subtype": "human", - "width": 12, - "height": 16, - "pixels": "....AAAA.......AccccA......AccccA......AAAAAA.....AAnnnnAA....Ann33nnA....Annn3nnA.....AnnnnA.....AAnnnnAA...AAnnAAnnAA..AnnA..AnnA..AnnAAAAnnA..AnnnggnnnA..AnnAggAnnA...AAA..AAA.....A....A...", - "faces": { - "right": "....AAAA.......AccccA......AccccA......AAAAAA.....AAnnnnAA....Ann33nnA....Annn3nnA.....AnnnnA.....AAnnnnAA...AAnnAAnnAA..AnnA..AnnA..AnnAAAAnnA..AnnnggnnnA..AnnAggAnnA...AAA..AAA.....A....A...", - "left": "mirror" - }, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 6, - "y": 5, - "c": "3" - } - ], - "lightColor": "3", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "....1111.......111111......111111......111111.....11111111....11111111....11111111.....111111.....11111111...1111111111..1111..1111..1111111111..1111111111..1111111111...111..111.....1....1...", - "door": null - }, - "contentHash": "fnv1a:0e69132c" - }, - { - "id": "has1j9ty6ako", - "name": "Puddle Toad", - "category": "dynamic", - "subtype": "animal", - "width": 12, - "height": 10, - "pixels": "...........................AA..AA.....AeeAAeeA...AeeeeeeeeA..AeeAeeAeeA..AeeeeeeeeA..A33eeee33A...A333333A.....AAAAAA...", - "faces": { - "right": "...........................AA..AA.....AeeAAeeA...AeeeeeeeeA..AeeAeeAeeA..AeeeeeeeeA..A33eeee33A...A333333A.....AAAAAA...", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...............11..11.....11111111...1111111111..1111111111..1111111111..1111111111...11111111.....111111...............", - "door": null - }, - "contentHash": "fnv1a:b7d83351" - }, - { - "id": "cvcojfvy6aku", - "name": "Ribbon Swan", - "category": "dynamic", - "subtype": "bird", - "width": 12, - "height": 12, - "pixels": "..............................AA.........A44A.......A4444A.....A44AA44A..AAA44AA44A.A333344444A.A33333333AA..A3333333A....A33333A......AAAAA....", - "faces": { - "right": "..............................AA.........A44A.......A4444A.....A44AA44A..AAA44AA44A.A333344444A.A33333333AA..A3333333A....A33333A......AAAAA....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "..................11.........1111.......111111.....11111111..1111111111.11111111111.11111111111..111111111....1111111......11111................", - "door": null - }, - "contentHash": "fnv1a:679a02b3" - }, - { - "id": "fjxfboey6al0", - "name": "Lantern Beetle", - "category": "dynamic", - "subtype": "animal", - "width": 12, - "height": 12, - "pixels": "............................AAAA.......ANNNNA.....ANN11NNA....ANN11NNA...AANNAANNAA..AppNNNNppA...AppppppA....ApA..ApA...AAA....AAA..A........A.", - "faces": { - "right": "............................AAAA.......ANNNNA.....ANN11NNA....ANN11NNA...AANNAANNAA..AppNNNNppA...AppppppA....ApA..ApA...AAA....AAA..A........A.", - "left": "mirror" - }, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 5, - "y": 3, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "................1111.......111111.....11111111....11111111...1111111111..1111111111...11111111....111..111...111....111..1........1.............", - "door": null - }, - "contentHash": "fnv1a:4dc817f4" - }, - { - "id": "dvrle37y6alj", - "name": "Smile Portrait", - "category": "static", - "subtype": "building", - "width": 24, - "height": 32, - "pixels": "......................................................llllllllllll...........lAAAAAAAAAAAAl.........lAkkkkkkkkkkkkAl........lAkttttttttttkAl........lAktttcccctttkAl........lAkttccccccttkAl........lAkttccccccttkAl........lAkttcAAActttkAl........lAkttccccccttkAl........lAktttcooccttkAl........lAkoooccccoookAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkoossoooosokAl........lAkossssoossskAl........lAkossssoossskAl........lAkoosssoossokAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkooooooooookAl........lAkkkkkkkkkkkkAl........lAAAAAAAAAAAAAAl.........llllllllllllll.....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "......111111111111...........11111111111111.........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111.........11111111111111.....................................................", - "door": null - }, - "contentHash": "fnv1a:fb140a94" - }, - { - "id": "6yh56c9y6amq", - "name": "Great Wave Panel", - "category": "static", - "subtype": "nature", - "width": 32, - "height": 24, - "pixels": "....llllllllllllllllllllllll.......lAAAAAAAAAAAAAAAAAAAAAAAAl.....lA777777777777777777777777Al....lA7ffffffffffffffffff77777Al....lA7fffffffffffffffff777777Al....lA7ffffffffffffff777779977Al....lA7ffffffffffff777777777kkAl....lA7ffffffffff777770007777kAl....lA7ffffffff77777000000777kAl....lA7ffffff77777000fff00077kAl....lA7ffff77777000ffffff0077kAl....lA7fff7770007fffffffff077kAl....lA77f7700007fffffffffff77kAl....lA777000007fffffffffffff7kAl....lA77000007ffffffffffffffkAl.....lA77000077ffffffffffffffkAl.....lA77700777ffffffffffffffkAl.....lA77777777ffffffffffffffkAl.....lA77kkkkk77777kkkkkkk777kAl.....lA77kkkkkkk77kkkkkkkk777kAl.....lA77kkkkkkkkkkkkkkkkk777kAl.....lA77kkkkkkkkkkkkkkkkk777kAl.....lA77777777777777777777777Al.....lAAAAAAAAAAAAAAAAAAAAAAAAAl...", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "....111111111111111111111111.......11111111111111111111111111.....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111.....111111111111111111111111111...", - "door": null - }, - "contentHash": "fnv1a:70c41074" - }, - { - "id": "gpvjlwqy6ano", - "name": "Sunflower Still Life", - "category": "static", - "subtype": "nature", - "width": 24, - "height": 32, - "pixels": "................................................................................AA..AA.................ANNAANNA...............ANNNNNNNNA...............ANNAANNA.................AA..AA....................kk.................AA..AA..AA..AA.........ANNAANNAANNAANNA.......ANNNNNNNNNNNNNNNNA.......ANNAANNAANNAANNA.........AA..AA..AA..AA................kk.................AA..AA..AA.............ANNAANNAANNA...........ANNNNNNNNNNNNA...........ANNAANNAANNA.............AA..AA..AA................kkkk....................kkkk...................AggggggA...............AggggggggA..............AggggggggA..............AggggggggA...............AggggggA.................AggggA..................AggggA.................AkkkkkkA...............AkkkkkkkkA..............AAAAAAAAAA........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........11..11.................11111111...............1111111111...............11111111.................11..11....................11.................11..11..11..11.........1111111111111111.......111111111111111111.......1111111111111111.........11..11..11..11................11.................11..11..11.............111111111111...........11111111111111...........111111111111.............11..11..11................1111....................1111...................11111111...............1111111111..............1111111111..............1111111111...............11111111.................111111..................111111.................11111111...............1111111111..............1111111111................................................................................", - "door": null - }, - "contentHash": "fnv1a:212abf57" - }, - { - "id": "z0bmedey6aoc", - "name": "Rosetta Stela", - "category": "static", - "subtype": "building", - "width": 24, - "height": 32, - "pixels": "................................AAAAAAAA...............AggggggggA.............AggggggggggA............AggggggggggA............AggoooggoogA............AggggggggggA............AggooggooggA............AggggggggggA............AgggoooggggA............AggggggggggA............AggooggggogA............AggggggggggA............AggoooggoogA............AggggggggggA............AggooggooggA............AggggggggggA............AgggoooggggA............AggggggggggA............AggooggggogA............AggggggggggA............AggoooggoogA............AggggggggggA............AggooggooggA............AggggggggggA............AgggoooggggA............AggggggggggA............AggggkkggggA............AggggkkggggA............AggggkkggggA.............AgggkkgggA...............AAAAAAAA........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........11111111...............1111111111.............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111.............1111111111...............11111111................................", - "door": null - }, - "contentHash": "fnv1a:1e1362b6" - }, - { - "id": "7ykwughy6ap4", - "name": "Pharaoh Mask", - "category": "static", - "subtype": "building", - "width": 24, - "height": 32, - "pixels": ".................................AAAAAA.................ARRRRRRA...............ARRjjjjRRA.............ARRjjjjjjRRA............ARRjccccjRRA............ARRjccccjRRA............ARRjckkcjRRA............ARRjcAAcjRRA............ARRjccccjRRA............ARRjckkcjRRA............ARRjccccjRRA............ARRjccccjRRA............ARRjckkcjRRA............ARRjccccjRRA............ARRjccccjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA............ARRjjccjjRRA.............ARRjccjRRA...............ARRccRRA................ARRccRRA.................AAAAAA.........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".........111111.................11111111...............1111111111.............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111.............1111111111...............11111111................11111111.................111111.................................", - "door": null - }, - "contentHash": "fnv1a:aa991de2" - }, - { - "id": "mtd2aazy6apv", - "name": "Stone Circle", - "category": "static", - "subtype": "nature", - "width": 32, - "height": 24, - "pixels": "............................................................................AA..AA........................AAggAAggAA....................AAggggggggggAA.................AggggggggggggggA...............AggggggggggggggggA.............AgggAgggA..AgggAgggA...........AgggAggggA..AggggAgggA..........AgggAggggA..AggggAgggA..........AgggAggggA..AggggAgggA..........AgggAggggA..AggggAgggA..........AgggAggggAAAAggggAgggA..........AgggAggggggggggggAgggA..........AgggAggggggggggggAgggA...........AgggAggggggggggAgggA.............AggggggggggggggggA...............AggggggggggggggA.................AAAAAAAAAAAAAA..................cccccccccccccc.................cckkcccccccckkcc................ccccccccoocccccc................ccccccccoocccccc.................AAAAAAAAAAAAAA..........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "............................................................................11..11........................1111111111....................11111111111111.................1111111111111111...............111111111111111111.............111111111..111111111...........1111111111..1111111111..........1111111111..1111111111..........1111111111..1111111111..........1111111111..1111111111..........1111111111111111111111..........1111111111111111111111..........1111111111111111111111...........11111111111111111111.............111111111111111111...............1111111111111111.................11111111111111..................11111111111111.................1111111111111111................1111111111111111................1111111111111111.................11111111111111..........", - "door": null - }, - "contentHash": "fnv1a:4c5c232d" - }, - { - "id": "s9oeu1ay6aql", - "name": "Terracotta Sentinel", - "category": "static", - "subtype": "building", - "width": 20, - "height": 32, - "pixels": "...........................AAAAAA.............AggggggA...........AggggggggA..........AggggggggA..........AgggoogggA..........AgggAAgggA..........AggggggggA..........AggggggggA...........AggggggA............AggggggA............AggggggA............AggggggA............AggggggA...........AggggggggA..........AgggkkgggA..........AgggkkgggA..........AgggkkgggA..........AgggkkgggA..........AgggkkgggA..........AggggggggA..........AggAAAAggA..........AgA....AgA.........AggA....AggA........AggA....AggA........AggA....AggA........AggA....AggA........AggA....AggA........AggA....AggA........AooA....AooA........AooA....AooA.........AA......AA.....", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......111111.............11111111...........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111...........11111111............11111111............11111111............11111111............11111111...........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........111....111.........1111....1111........1111....1111........1111....1111........1111....1111........1111....1111........1111....1111........1111....1111........1111....1111.........11......11.........................", - "door": null - }, - "contentHash": "fnv1a:d073e29a" - }, - { - "id": "sby5exdy6ar8", - "name": "Rain Bell Tower", - "category": "static", - "subtype": "building", - "width": 20, - "height": 36, - "pixels": "............................AAAA...............A9999A.............A999999A............A99AA99A............AAA44AAA.............A4444A..............A4444A.............AA4444AA............A44cc44A............A44cc44A............A44cc44A............A441144A............A44cc44A...........AA444444AA..........A44cccc44A..........A44cooc44A..........A44cccc44A..........A44444444A..........A44411444A..........A444cc444A..........A444cc444A..........A444cc444A.........AA444cc444AA........A4444444444A........A444cccc444A........A444cooc444A........A444cccc444A........A4444444444A........A4444ww4444A........A4444ww4444A........A44ccwwcc44A........A44ccwwcc44A........A4444ww4444A.........AAAAwwAAAA.............AAAA........", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 9, - "y": 11, - "c": "1" - }, - { - "x": 9, - "y": 19, - "c": "1" - } - ], - "lightColor": "1", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........1111...............111111.............11111111............11111111............11111111.............111111..............111111.............11111111............11111111............11111111............11111111............11111111............11111111...........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111.........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111.........1111111111.............1111............................", - "door": { - "x": 9, - "y": 33 - } - }, - "contentHash": "fnv1a:3d2b4a8d" - }, - { - "id": "z3d6hrgy6as7", - "name": "Crimson Pagoda", - "category": "static", - "subtype": "building", - "width": 24, - "height": 32, - "pixels": ".........................................................AAAAAA................AA999999AA.............A9999999999A............AAAAssAAAAAA...........AAllllllllllAA..........AsllllllllllsA.........AAssssssssssssAA........AssssssssssssssA.......AAllllllllllllllAA......Asll55llllll55llsA......AssssssssssssssssA.....AAssssssssssssssssAA....AsllllllllllllllllsA....Asll55lllAAlll55llsA....AssssssssAAssssssssA...AAssssssssssssssssssAA..AsllllllllllllllllllsA..Asll55llllllllll55llsA..AssssssssssssssssssssA..AssccccccccccccccccssA..AsscoooccooooccooocssA..AssccccccccccccccccssA..AssssssssssssssssssssA..AssssssssssssssssssssA..AssccccccccAAccccccssA..AsscoooccccAAcccoocssA..AssccccccccAAccccccssA..AssssssssssAAssssssssA..AssssssssssAAssssssssA...AAAAAAAAAAAAAAAAAAAA..", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 8, - "y": 12, - "c": "5" - }, - { - "x": 15, - "y": 12, - "c": "5" - }, - { - "x": 11, - "y": 22, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".........111111................1111111111.............111111111111............111111111111...........11111111111111..........11111111111111.........1111111111111111........1111111111111111.......111111111111111111......111111111111111111......111111111111111111.....11111111111111111111....11111111111111111111....11111111111111111111....11111111111111111111...1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111..1111111111111111111111...11111111111111111111..................................................", - "door": { - "x": 11, - "y": 28 - } - }, - "contentHash": "fnv1a:30852f2b" - }, - { - "id": "65mnzeoy6atb", - "name": "Meteor Forge", - "category": "static", - "subtype": "building", - "width": 32, - "height": 24, - "pixels": "........................................................................AAAAAAAAAAAA..................AAffffffffffffAA...............AffAAAAAAAAAAAAffA.............AAAAAAAAAAAAAAAAAAAA...........AooooooooooooooooooooA.........AAooo5555oooooo5555oooAA........Aooo555555oooo555555oooA........Aoooo55555oooo55555ooooA........AooooooooooooooooooooooA.......AAooooooooooooooooooooooAA......AooggggggggggggggggggggooA......AoogddgggddggggggddgggdgoA......AoogggggggggAAAAggggggggoA......AoogddggggggAAAAgggggddgoA......AoogggggggggAAAAggggggggoA......AooooooooooooAAooooooooooA......Aoooo55ooooooAAoooooo55ooA......AooooooooooooAAooooooooooA......AooggggggggggAAggggggggooA......AoogdddggggggAAgggggdddgoA......AooggggggggggAAggggggggooA......AAAAAAAAAAAAAAAAAAAAAAAAAA.....", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 8, - "y": 10, - "c": "9" - }, - { - "x": 23, - "y": 10, - "c": "9" - }, - { - "x": 15, - "y": 16, - "c": "9" - } - ], - "lightColor": "9", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........................................111111111111..................1111111111111111...............111111111111111111.............11111111111111111111...........1111111111111111111111.........111111111111111111111111........111111111111111111111111........111111111111111111111111........111111111111111111111111.......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111.....................................", - "door": { - "x": 15, - "y": 19 - } - }, - "contentHash": "fnv1a:9e1eeec0" - }, - { - "id": "1bvcy1sy6aum", - "name": "Ink Garden Screen", - "category": "static", - "subtype": "nature", - "width": 32, - "height": 24, - "pixels": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA444444444444444444444444444444AA4qqq4444444444444444444444qqq4AA4qqqq44444444444444444444qqqq4AA4qqqqq4444444eeee444444qqqqq4A.A4qqqqqq44444eeeeee4444qqqqqq4A.A4qqqqqqq4444eeeeee4444qqqqqq4A.A4qqqqqq44444eeeeee4444qqqqqq4A.A4qqqqq4444444eeee4444qqqqqq4A..A44444444444444444444444444444A.A4444444444444AAAA444444444444A.A444444444444AkkkkA44444444444A.A444444444444AkkkkA44444444444A.A444444444444AAAAAA44444444444A.A44444444444444444444444444444A.A4eee44444444444444444444eee44A.A4eie44444444444444444444eie44A.A4eee44444444444444444444eee44A.A44444444444444444444444444444A.A44444444444444444444444444444A.A44444444444444444444444444444A.A44444444444444444444444444444A.A44444444444444444444444444444A.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.111111111111111111111111111111..1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.1111111111111111111111111111111.11111111111111111111111111111111", - "door": null - }, - "contentHash": "fnv1a:a406b639" - }, - { - "id": "6mumquiy6avl", - "name": "Meadow Totem", - "category": "static", - "subtype": "nature", - "width": 20, - "height": 28, - "pixels": "............................AAAA...............AggggA.............AggaaggA............AggaaggA............AggAAggA............Agg55ggA............Agg55ggA............AggAAggA............AggaaggA............AggaaggA............Agg55ggA............Agg55ggA............AggAAggA............AggggggA............AggggggA............AggggggA............AggggggA............AggggggA............AggggggA............AggggggA............AggggggA............AggooggA............AggooggA............AggooggA............AggooggA.............AooooA...............AAAA........", - "faces": null, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "........1111...............111111.............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111.............111111...............1111............................", - "door": null - }, - "contentHash": "fnv1a:c59114cf" - }, - { - "id": "ttfx2j0y6aw4", - "name": "Twilight Caravan", - "category": "static", - "subtype": "ship", - "width": 32, - "height": 16, - "pixels": "...........................................AAAAAAAA.....................AAAddddddddAA.................AAddXXXXXXXXddAA...............AddXXXXXXXXXXXXddA.............AddXXXXXXXXXXXXXXddA...........AAkkkkkkkkkkkkkkkkkkAA..........Akkkk55kkkk55kkkk55kkA..........AkkkkkkkkkkkkkkkkkkkkA..........AkkcccccccccccccccckkA..........AkkckkkcckkcckkcckkckA..........AkkcccccccccccccccckkA.........AAkkkkkkkkkkkkkkkkkkkkAA........AkkkkAAkkkkAAkkkkAAkkkkA........AkkkA..AkkkA..AkkkA..AkkA.......AAAA....AAAA....AAAA..AA......", - "faces": null, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 9, - "y": 7, - "c": "5" - }, - { - "x": 15, - "y": 7, - "c": "5" - }, - { - "x": 22, - "y": 7, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "...........................................11111111.....................1111111111111.................1111111111111111...............111111111111111111.............11111111111111111111...........1111111111111111111111..........1111111111111111111111..........1111111111111111111111..........1111111111111111111111..........1111111111111111111111..........1111111111111111111111.........111111111111111111111111........111111111111111111111111........11111..11111..11111..1111.......1111....1111....1111..11......", - "door": null - }, - "contentHash": "fnv1a:d8d15418" - }, - { - "id": "bln8teny6awj", - "name": "Pepper Fox", - "category": "dynamic", - "subtype": "animal", - "width": 16, - "height": 16, - "pixels": ".......................................................AA.............AhhA...........AhhhhAA........AhhhhhhA.......AhhhhhhhhA......AhhhhAhhhA.....AhhhhhhhA1A.....Ahhhhhhh11A.....Ahhhhhh11A.......Ahhhh11A.........Ahh111AA........A111111A.........AA..AA.....", - "faces": { - "right": ".......................................................AA.............AhhA...........AhhhhAA........AhhhhhhA.......AhhhhhhhhA......AhhhhAhhhA.....AhhhhhhhA1A.....Ahhhhhhh11A.....Ahhhhhh11A.......Ahhhh11A.........Ahh111AA........A111111A.........AA..AA.....", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......................................11.............1111...........1111111........11111111.......1111111111......1111111111.....11111111111.....11111111111.....1111111111.......11111111.........11111111........11111111.........11..11.....................", - "door": null - }, - "contentHash": "fnv1a:221bfcd2" - }, - { - "id": "7f885l2y6awr", - "name": "Glass Manta", - "category": "dynamic", - "subtype": "fish", - "width": 16, - "height": 12, - "pixels": ".......................AA............AASSAA........AASSSSSSAA.....ASSSSSSSSSSA...ASSSSSSSSSSSSA.AASSSSSSSSSSSSAA.ASSSSSSSSSSSSA...AASSSSSSSSAA......AASSSSAA..........ASSA.............AA.......", - "faces": { - "right": ".......................AA............AASSAA........AASSSSSSAA.....ASSSSSSSSSSA...ASSSSSSSSSSSSA.AASSSSSSSSSSSSAA.ASSSSSSSSSSSSA...AASSSSSSSSAA......AASSSSAA..........ASSA.............AA.......", - "left": "mirror" - }, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 7, - "y": 5, - "c": "O" - } - ], - "lightColor": "O", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".......................11............111111........1111111111.....111111111111...11111111111111.1111111111111111.11111111111111...111111111111......11111111..........1111.............11.......", - "door": null - }, - "contentHash": "fnv1a:ed8b326c" - }, - { - "id": "vuo4tnuy6ax0", - "name": "Festival Drummer", - "category": "dynamic", - "subtype": "human", - "width": 14, - "height": 16, - "pixels": "...................AAAA.........AccccA........AccccA........AAAAAA.......AAhhhhAA......AhhjjhhA......AhhjjhhA.......AjjjjA......AAAjjjjAA....AAjjAAAAjjA...AjjA.ggAjjA...AjjAAAAAjjA...AjjA....AjA...AjjA....AjA....AA......AA..", - "faces": { - "right": "...................AAAA.........AccccA........AccccA........AAAAAA.......AAhhhhAA......AhhjjhhA......AhhjjhhA.......AjjjjA......AAAjjjjAA....AAjjAAAAjjA...AjjA.ggAjjA...AjjAAAAAjjA...AjjA....AjA...AjjA....AjA....AA......AA..", - "left": "mirror" - }, - "meta": { - "hasLight": false, - "lightPixels": [], - "lightColor": null, - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": ".....1111.........111111........111111........111111.......11111111......11111111......11111111.......111111......111111111....11111111111...1111.111111...11111111111...1111....111...1111....111....11......11................", - "door": null - }, - "contentHash": "fnv1a:5cc2b1fb" - }, - { - "id": "hnlnlzwy6ax7", - "name": "Bloom Sprite", - "category": "dynamic", - "subtype": "animal", - "width": 12, - "height": 12, - "pixels": "........................................AAAA.......A5555A.....A55QQ55A....A55QQ55A...AA555555AA..Aee5555eeA...AeeeeeeA....AeA..AeA...AA....AAA..", - "faces": { - "right": "........................................AAAA.......A5555A.....A55QQ55A....A55QQ55A...AA555555AA..Aee5555eeA...AeeeeeeA....AeA..AeA...AA....AAA..", - "left": "mirror" - }, - "meta": { - "hasLight": true, - "lightPixels": [ - { - "x": 5, - "y": 3, - "c": "5" - } - ], - "lightColor": "5", - "hasParticles": false, - "particlePixels": [], - "particleConfig": { - "enabled": false, - "c": "0", - "dir": "up" - }, - "depthPixels": "................1111.......111111.....11111111....11111111...1111111111..1111111111...11111111....111..111...11....111..........................", - "door": null - }, - "contentHash": "fnv1a:92282a25" - } -] \ No newline at end of file diff --git a/default_assets_sheet.png b/default_assets_sheet.png deleted file mode 100644 index 1edf6fd..0000000 Binary files a/default_assets_sheet.png and /dev/null differ diff --git a/extract_assets.js b/extract_assets.js deleted file mode 100644 index 8b82082..0000000 --- a/extract_assets.js +++ /dev/null @@ -1,24 +0,0 @@ -const fs = require('fs'); -const vm = require('vm'); -const path = require('path'); -let code = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); -code = code.replace(/\n\s*bootstrap\(\);\s*\n\}\)\(\);\s*$/, `\n window.__PIXEL_DEBUG__ = { state, buildDefaultGalleryPack, PALETTE, PALETTE_BY_CODE, assetWidth, assetHeight };\n})();`); -function makeCtx(){ - const noop=()=>{}; - return new Proxy({imageSmoothingEnabled:false, canvas:{width:1,height:1}}, {get(t,p){ - if(p in t) return t[p]; - if(p==='measureText') return () => ({width:0}); - return noop; - }, set(t,p,v){ t[p]=v; return true; }}); -} -function makeEl(id){ return new Proxy({ id, width:640, height:480, style:{}, classList:{add(){},remove(){},toggle(){},contains(){return false;}}, dataset:{}, value:'', checked:false, textContent:'', innerHTML:'', append(){}, appendChild(){}, addEventListener(){}, removeEventListener(){}, setAttribute(){}, removeAttribute(){}, focus(){}, blur(){}, getBoundingClientRect(){return {left:0,top:0,width:640,height:480};}, getContext(){return makeCtx();}, querySelector(){return makeEl('q');}, querySelectorAll(){return [];}}, {get(t,p){ if(p in t) return t[p]; if(p==='children') return []; return undefined; }, set(t,p,v){ t[p]=v; return true; }}); } -const localStorage = { getItem(){return null;}, setItem(){}, removeItem(){} }; -const sandbox = { console, window: {}, document: { getElementById: makeEl, createElement: makeEl, querySelector(){return makeEl('q')}, querySelectorAll(){return []}, addEventListener(){}, body: makeEl('body')}, localStorage, performance: { now: () => 0 }, Math, Date, Map, Set, Array, Object, JSON, Number, String, Boolean, RegExp, Error, Uint8Array, Int8Array, Uint16Array, URL: {createObjectURL(){return ''}, revokeObjectURL(){}}, Blob: function(){}, Image: function(){}, FileReader: function(){}, requestAnimationFrame(){}, cancelAnimationFrame(){}, setTimeout, clearTimeout, structuredClone: global.structuredClone }; -sandbox.window = Object.assign(sandbox.window, { PixelIslandModules:{}, devicePixelRatio:1, addEventListener(){}, removeEventListener(){}, matchMedia(){return {matches:false, addEventListener(){}, removeEventListener(){}}}, requestAnimationFrame(){}, cancelAnimationFrame(){}, document: sandbox.document, localStorage: sandbox.localStorage }); -vm.createContext(sandbox); -vm.runInContext(code, sandbox, {filename:'app.js'}); -const debug = sandbox.window.__PIXEL_DEBUG__; -const assets = debug.state.assets.map(a => ({id:a.id,name:a.name,category:a.category,subtype:a.subtype,width:a.width,height:a.height,pixels:a.pixels,faces:a.faces,meta:a.meta,contentHash:a.contentHash})); -fs.writeFileSync(path.join(__dirname,'assets.json'), JSON.stringify(assets,null,2)); -console.log('assets', assets.length); -console.log(assets.map(a=>`${a.name} ${a.width}x${a.height} ${a.category}/${a.subtype}`).join('\n')); diff --git a/index.html b/index.html index 8f47eeb..5afd5ff 100644 --- a/index.html +++ b/index.html @@ -34,7 +34,7 @@ - Save + Place creates a local account for island publishing. + Publish creates a local account for island publishing. @@ -59,7 +59,6 @@ -
@@ -75,6 +74,7 @@ + @@ -91,7 +91,12 @@
-
+
+
+
+
+
+
@@ -102,56 +107,53 @@
-
-
-
-
-
-
-
Advanced settings
-
Finish
- +
@@ -164,7 +166,7 @@
Collection
-

Your works are permanent here. The island is a temporary exhibition: place your own new or past works; remix only copies lineage and pixels to the canvas.

+

Your works are permanent here. The island is a temporary exhibition.

-
-
-
Local save
-

Works are durable collection works; island placements are temporary exhibition objects. Export JSON if you want to move or test a local world.

-
- - - - - - - - - -
-
- -
- -
-
Phase 5 guardrails
-

Local pre-server checks for object volume, orphaned placements, terrain compatibility, hidden objects, and report logs.

-
-
- - - -
-
-
@@ -245,6 +217,17 @@ + +