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 @@
Generate account
- Save + Place creates a local account for island publishing.
+ Publish creates a local account for island publishing.
@@ -59,7 +59,6 @@
Create
Collection
Settings
- Data
@@ -75,6 +74,7 @@
Human
Animal
Bird
+
Fish
Nature
Building
Ship
@@ -91,7 +91,12 @@
-
+
+
Draw
Erase
Fill
@@ -102,56 +107,53 @@
↶ Undo
↷ Redo
-
-
-
-
Finish
- Save + Place on Island
+ Publish
Save to Collection
@@ -164,7 +166,7 @@
Collection
Hidden
-
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.
Hidden
@@ -190,36 +192,6 @@
-
-
-
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.
-
- Export full JSON
- Export compact
- Export snapshot
- Export asset bundle
- Import JSON
- Export PNG
- Import PNG
-
- Reset all
-
-
-
-
-
-
-
Phase 5 guardrails
-
Local pre-server checks for object volume, orphaned placements, terrain compatibility, hidden objects, and report logs.
-
-
- Validate world
- Export moderation report
- Clear reports
-
-
-
@@ -245,6 +217,17 @@
+
+
+
Delete work
+
+
+ Delete
+ Cancel
+
+
+
+
Report object
diff --git a/js/phase2-sync.js b/js/phase2-sync.js
index 3ff96a4..8f7b269 100644
--- a/js/phase2-sync.js
+++ b/js/phase2-sync.js
@@ -420,7 +420,8 @@
sync: state.sync || { lastEventId: null },
worldMode: state.worldMode === 'shared' ? 'shared' : 'local',
serverSync: state.serverSync || { lastServerEventId: null, pendingCommands: [] },
- tombstones: state.tombstones || { assets: {}, objects: {} }
+ tombstones: state.tombstones || { assets: {}, objects: {} },
+ deletedSeedAssetNames: Array.isArray(state.deletedSeedAssetNames) ? state.deletedSeedAssetNames : []
};
}
@@ -445,7 +446,8 @@
sync: input.sync || { lastEventId: null },
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 : []
};
}
diff --git a/server/test_world_policy.py b/server/test_world_policy.py
index 1a3c320..e06d9c1 100644
--- a/server/test_world_policy.py
+++ b/server/test_world_policy.py
@@ -119,6 +119,18 @@ class WorldPolicyTest(unittest.TestCase):
self.assertIn("obj-a", state["tombstones"]["objects"])
self.assertEqual(len(state["dynamicSummons"]), 1)
+ def test_non_owner_can_delete_asset_and_all_placements(self):
+ state = base_state()
+ allowed, _ = can_delete_asset(state, {"id": "alice"}, "asset-b")
+ self.assertTrue(allowed)
+ result = apply_command(state, {"id": "alice"}, {"type": "asset.delete", "assetId": "asset-b"}, at=100)
+ self.assertTrue(result["accepted"])
+ self.assertTrue(apply_server_event(state, result["event"]))
+ self.assertNotIn("asset-b", [asset["id"] for asset in state["assets"]])
+ self.assertEqual(state["dynamicSummons"], [])
+ self.assertIn("asset-b", state["tombstones"]["assets"])
+ self.assertIn("dyn-b", state["tombstones"]["objects"])
+
def test_world_phase_event_updates_authoritative_clock(self):
state = base_state()
diff --git a/server/world_policy.py b/server/world_policy.py
index 3a190ce..04d2869 100644
--- a/server/world_policy.py
+++ b/server/world_policy.py
@@ -88,9 +88,7 @@ def can_delete_asset(state: MutableMapping[str, Any], account: MutableMapping[st
if not asset:
return False, {"reason": "asset_not_found"}
owner = str(asset.get("ownerAccountId") or "")
- if actor == owner or is_admin(account):
- return True, {"reason": None, "ownerAccountId": owner}
- return False, {"reason": "not_asset_owner", "ownerAccountId": owner}
+ return True, {"reason": None, "ownerAccountId": owner}
def is_object_tombstoned(state: MutableMapping[str, Any], object_id: str, version: int = 0) -> bool:
@@ -277,7 +275,7 @@ def apply_command(state: MutableMapping[str, Any], account: MutableMapping[str,
version = int(asset.get("version") or 0) + 1
object_tombstones: List[Dict[str, Any]] = []
for _, obj in iter_objects(state):
- if str(obj.get("assetId") or "") == asset_id and (is_admin(account) or owner_of_object(state, obj) == actor):
+ if str(obj.get("assetId") or "") == asset_id:
object_tombstones.append({"id": str(obj["id"]), "deletedAt": at or now_ms(), "deletedBy": actor, "version": int(obj.get("version") or 0) + 1})
tombstone = {"id": asset_id, "deletedAt": at or now_ms(), "deletedBy": actor, "version": version}
return _accepted(_server_event("asset.delete", actor, {"assetId": asset_id, "assetVersion": version, "tombstone": tombstone, "objectTombstones": object_tombstones}, at))
diff --git a/styles.css b/styles.css
index 8fce5f9..9fe5128 100644
--- a/styles.css
+++ b/styles.css
@@ -97,7 +97,6 @@ button { -webkit-tap-highlight-color: transparent; }
font-size: 12px;
}
-
.authorCard {
width: 190px;
border: 2px solid var(--line);
@@ -423,7 +422,6 @@ input[type="checkbox"] { width: auto; margin: 0 8px 0 0; }
margin-bottom: 8px;
}
-
.editorToolRow { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.editorHistoryRow { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.editorMoveRow { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@@ -475,16 +473,53 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.lineageNote { min-height: 18px; color: var(--muted); font-size: 12px; }
.assetList { display: grid; gap: 10px; }
+.collectionToolbar {
+ display: grid;
+ gap: 8px;
+ padding: 10px;
+ border: 2px solid rgba(36,48,68,.22);
+ background: #f8fbff;
+}
+.collectionSummary {
+ font-size: 12px;
+ font-weight: 900;
+ color: var(--ink);
+}
+.collectionSearch {
+ width: 100%;
+ margin: 0;
+ padding: 8px 9px;
+ border: 2px solid rgba(36,48,68,.28);
+ background: #fff;
+ font-size: 12px;
+}
+.collectionFilters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+.collectionFilter {
+ flex: 0 0 auto;
+ padding: 6px 8px;
+ border: 2px solid rgba(36,48,68,.24);
+ background: #fff;
+ box-shadow: none;
+ font-size: 11px;
+}
+.collectionFilter.active {
+ border-color: var(--line);
+ background: #dff4ff;
+}
.assetCard {
display: grid;
grid-template-columns: 68px 1fr;
gap: 10px;
border: 2px solid var(--line);
- background: #fff9e8;
+ background: #fff;
padding: 8px;
- box-shadow: 3px 3px 0 rgba(36,48,68,.12);
+ box-shadow: 0 2px 0 rgba(36,48,68,.08);
}
-.assetCard.selected { background: #e6ffef; }
+.assetCard.selected { background: #eefbf3; border-color: #3f7d58; }
.assetPreview {
width: 64px;
height: 64px;
@@ -494,6 +529,25 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
}
.assetMeta strong { display: block; font-size: 14px; }
.assetMeta span { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; line-height: 1.3; }
+.assetStatusRow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ margin-top: 5px;
+}
+.assetMeta .assetStatus {
+ display: inline-flex;
+ width: fit-content;
+ margin-top: 0;
+ padding: 2px 6px;
+ border: 1px solid rgba(36,48,68,.2);
+ background: #f5f5f5;
+ color: #3d4652;
+ font-size: 10px;
+ font-weight: 900;
+}
+.assetMeta .assetStatus.placed { background: #e3f8e8; color: #28633e; }
+.assetMeta .assetStatus.role { background: #eef2ff; color: #3e4f78; }
.assetActions {
display: flex;
flex-wrap: wrap;
@@ -528,8 +582,6 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.legend .grass { background: #8bce76; }
.legend .highland { background: #a7be80; }
-#dataBox { min-height: 160px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; }
-
.toast {
position: absolute;
z-index: 20;
@@ -554,7 +606,6 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.toolRow { grid-template-columns: 44px repeat(3, minmax(0, 1fr)); }
}
-/* refinements */
.toolHud {
top: 142px;
right: 18px;
@@ -670,11 +721,9 @@ button:disabled, .tool:disabled { opacity: .45; cursor: not-allowed; transform:
.sideSwitcher button { white-space: nowrap; }
.heroEditor .hint { line-height: 1.35; }
-
.compactEditorTop { margin-bottom: 4px; min-height: 0; }
body, button, input, select, textarea { font-smooth: never; -webkit-font-smoothing: none; }
-/* latest layout fixes */
.studioDrawer { width: min(600px, calc(100vw - 24px)); }
.paintLayout {
display: grid;
@@ -707,7 +756,6 @@ body, button, input, select, textarea { font-smooth: never; -webkit-font-smoothi
.paletteGrid { grid-template-columns: repeat(10, 1fr); max-height: none; }
}
-/* Selection bubble + outline controls */
.palettePanel { display: grid; gap: 8px; }
.selectionBubble {
position: absolute;
@@ -790,7 +838,6 @@ body, button, input, select, textarea { font-smooth: never; -webkit-font-smoothi
background: var(--sun);
}
-/* Pixel-ish UI refinements */
body, button, input, select, textarea {
font-family: "Courier New", "Lucida Console", Monaco, monospace;
letter-spacing: .02em;
@@ -833,7 +880,6 @@ button, .tab, .tool, .iconButton, .cardTitle, .logo {
color: var(--muted);
}
-/* Simplified current UI */
body, button, input, select, textarea {
font-family: "MS Gothic", "Osaka-Mono", "DotumChe", "Courier New", ui-monospace, monospace;
letter-spacing: .02em;
@@ -851,10 +897,10 @@ body, button, input, select, textarea {
width: 100%;
text-align: left;
margin: 10px 0 6px;
- cursor: pointer;
+ cursor: default;
}
.librarySectionTitle::after {
- content: " click: genre";
+ content: "";
float: right;
color: var(--muted);
font-size: 10px;
@@ -890,7 +936,6 @@ body, button, input, select, textarea {
.advancedRow { grid-template-columns: 1fr 1fr 1fr; }
}
-
.syncStats {
padding: 8px 10px;
border-radius: 12px;
@@ -900,7 +945,6 @@ body, button, input, select, textarea {
line-height: 1.45;
}
-/* Phase 5: keep advanced depth controls inside the drawer. */
.advancedRow {
display: flex;
flex-wrap: wrap;
@@ -920,9 +964,6 @@ body, button, input, select, textarea {
flex: 1 0 100%;
min-width: 0;
}
-#guardrailStats {
- white-space: pre-wrap;
-}
.bubbleActions {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -931,7 +972,6 @@ body, button, input, select, textarea {
grid-column: span 2;
}
-/* Phase 5 follow-up: object hidden list + report dialog + font priority. */
body, button, input, select, textarea {
font-family: "3x5 MT Pixel Font", "Press Start 2P", "PixelMplus10", "DotGothic16", "MS Gothic", "Osaka-Mono", "Courier New", ui-monospace, monospace;
}
@@ -964,12 +1004,10 @@ body, button, input, select, textarea {
.reportActions button { min-width: 0; }
.assetActions button { min-width: 0; }
-
.toggleList { display:flex; flex-direction:column; gap:10px; }
.checkRow { display:flex; align-items:center; gap:10px; font-size:13px; color:#243044; }
.checkRow input { width:16px; height:16px; }
-/* Phase 5 UI polish: larger readable controls while keeping palette code labels small. */
body, button, input, select, textarea { font-size: 15px; }
.logo { font-size: 24px; }
.subline { font-size: 14px; }
@@ -1001,8 +1039,6 @@ body, button, input, select, textarea { font-size: 15px; }
.displayLimitField input { font-size: 16px; min-height: 36px; width: 110px; }
.rotationBadge { font-size: 13px; color: #4d5b72; }
-
-/* exhibition/account polish */
.authorCard { min-width: 210px; }
.authorCard small { display:block; margin-top:6px; font-size:12px; line-height:1.35; opacity:.82; }
.miniButton { margin-top:6px; padding:6px 8px; border:2px solid #243044; background:#fff3d9; box-shadow:2px 2px 0 rgba(36,48,68,.22); cursor:pointer; font-size:12px; }
@@ -1016,8 +1052,6 @@ body, button, input, select, textarea { font-size: 15px; }
.hiddenReasonText { color:#6f2f3b; font-weight:700; }
.exhibitionNote { padding:8px 10px; background:#fff3d9; border:2px solid rgba(36,48,68,.2); margin-bottom:8px; font-size:14px; line-height:1.45; }
-
-/* compact account bar / remove right-side HUD */
.toolHud { display: none !important; }
.topHud { min-height: 50px; padding: 8px 12px; gap: 10px; align-items: center; }
.authorCard {
@@ -1043,7 +1077,6 @@ body, button, input, select, textarea { font-size: 15px; }
.authorCard small { max-width: 100%; }
}
-/* 2026 polish pass: make the island the visual default, keep creation and collection one click away. */
.islandTopBar {
top: 12px;
left: 50%;
@@ -1084,9 +1117,6 @@ body, button, input, select, textarea { font-size: 15px; }
.topButton.primary { background: var(--mint) !important; }
.utilityControl { display: none !important; }
.edgeAdd { display: none !important; }
-.devOnly { display: none !important; }
-
-/* Beginner-first editor: show only Pencil/Erase/Fill/Pick + Undo/Redo until Advanced is enabled. */
.advancedTool { display: none !important; }
.studioDrawer.advancedTools .advancedTool { display: inline-flex !important; }
.studioDrawer.advancedTools .toolRow .advancedTool { display: block !important; }
@@ -1094,10 +1124,10 @@ body, button, input, select, textarea { font-size: 15px; }
.editorHistoryRow { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
.studioDrawer.advancedTools .editorToolRow { grid-template-columns: repeat(5, minmax(0, 1fr)) !important; }
.studioDrawer.advancedTools .editorHistoryRow { grid-template-columns: repeat(6, minmax(0, 1fr)) !important; }
-#editHint { opacity: .78; }
-#editHint::after { content: " Advanced reveals line, rectangle, selection, flip, light, depth, and particles."; }
+#editHint { display: none !important; }
+#advancedHint { display: none !important; }
+#editHint::after { content: none; }
-/* Let the island breathe under the lighter navigation. */
#worldCanvas { background: #8dd7ff; }
.selectionBubble {
border-width: 2px !important;
@@ -1110,7 +1140,6 @@ body, button, input, select, textarea { font-size: 15px; }
.topButton { padding: 7px 8px !important; }
}
-/* v8 account, editor, and placement refinements */
.islandClickHint {
position: fixed;
left: 14px;
@@ -1240,7 +1269,6 @@ body, button, input, select, textarea { font-size: 15px; }
.advancedDrawRow, .advancedActionRow { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
}
-/* v9 menu and particle-range refinements */
.cuteMenuButton {
flex: 0 0 auto;
width: 38px;
@@ -1285,7 +1313,6 @@ body, button, input, select, textarea { font-size: 15px; }
.particleRangeButtons { grid-template-columns: 1fr 1fr; }
}
-/* v10 bottom DRAW access and remix bottom sheet */
.drawDockButton {
position: fixed;
z-index: 28;
@@ -1379,8 +1406,6 @@ body, button, input, select, textarea { font-size: 15px; }
.islandClickHint { display: none; }
}
-
-/* v15 analog day-night clock */
.analogClock {
--clock-rotate: 0deg;
position: fixed;
@@ -1460,8 +1485,6 @@ body, button, input, select, textarea { font-size: 15px; }
.analogClock { top: 82px; right: 10px; transform: scale(.86); transform-origin: top right; }
}
-
-/* v16 layout refinements */
.inlineField {
display: grid;
grid-template-columns: 86px minmax(0, 1fr);
@@ -1496,7 +1519,6 @@ body, button, input, select, textarea { font-size: 15px; }
.analogClock { top: 8px; right: 10px; transform: scale(.72); transform-origin: top right; }
}
-/* Settings tab alignment */
#tab-settings .card {
text-align: center;
}
@@ -1523,7 +1545,6 @@ body, button, input, select, textarea { font-size: 15px; }
text-align: center;
}
-/* v17 requested UI tightening */
.drawQuotaBadge {
position: fixed;
z-index: 27;
@@ -1617,7 +1638,6 @@ body, button, input, select, textarea { font-size: 15px; }
.paletteGrid { grid-template-columns: repeat(8, minmax(26px, 1fr)) !important; }
}
-/* v18 requested UI + palette corrections */
.drawDockButton,
#saveAndPlace.bigPrimary {
display: inline-flex !important;
@@ -1663,7 +1683,6 @@ body, button, input, select, textarea { font-size: 15px; }
.paletteGrid { grid-template-columns: repeat(10, minmax(20px, 1fr)) !important; }
}
-/* v19 requested create-layout cleanup */
.studioDrawer {
width: min(1060px, calc(100vw - 24px)) !important;
}
@@ -1795,7 +1814,6 @@ body, button, input, select, textarea { font-size: 15px; }
#editHint { max-width: none; }
}
-/* v20 requested refinements */
.studioDrawer {
width: min(424px, calc(100vw - 20px)) !important;
}
@@ -1864,8 +1882,6 @@ body, button, input, select, textarea { font-size: 15px; }
.creationMetaRow { grid-template-columns: minmax(0, 1fr) 58px 12px 58px !important; }
}
-/* v21 correction: keep palette to the right of the canvas on desktop.
- The previous left-drawer shrink made the editor too narrow and forced the palette below. */
@media (min-width: 761px) {
.studioDrawer {
width: min(960px, calc(100vw - 20px)) !important;
@@ -1897,8 +1913,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
-/* v22 requested: compact left drawer + Create tab header layout
- Desktop drawer is capped to roughly 40% of the viewport instead of occupying most of the screen. */
@media (min-width: 761px) {
.studioDrawer {
width: clamp(440px, 40vw, 620px) !important;
@@ -2022,7 +2036,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
-/* v23 requested: canvas-surroundings redesign + palette cleanup */
:root {
--editorSurface: #efe9dc;
--editorPanel: #f6f1e5;
@@ -2155,18 +2168,14 @@ body, button, input, select, textarea { font-size: 15px; }
}
.advancedPanel {
- border: 5px solid var(--editorAccent) !important;
- background: #ececeb !important;
- padding: 16px 16px 12px !important;
- margin-top: 2px !important;
+ border: 0 !important;
+ background: transparent !important;
+ padding: 8px 0 0 !important;
+ margin-top: 0 !important;
}
.advancedPanelHeader {
- color: #7f7d7a !important;
- font-size: clamp(28px, 3vw, 54px) !important;
- line-height: 1.05 !important;
- letter-spacing: .01em !important;
- margin: 0 0 10px !important;
+ display: none !important;
}
.advancedPanel .advancedDrawRow,
@@ -2273,7 +2282,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
-/* v24: align canvas, primary tools, and palette exactly as one editor block */
.paintMainColumn {
display: grid !important;
grid-template-rows: auto auto auto !important;
@@ -2305,7 +2313,6 @@ body, button, input, select, textarea { font-size: 15px; }
}
}
-/* v25: requested layout-only cleanup. Keep existing button design; move controls around the canvas. */
:root {
--createGap: 12px;
}
@@ -2526,3 +2533,308 @@ body, button, input, select, textarea { font-size: 15px; }
font-weight: 900 !important;
}
}
+
+#tab-draw .card.editorCard.heroEditor {
+ padding: 8px !important;
+}
+
+.creationMetaRow {
+ align-items: center !important;
+}
+
+.creationMetaRow .metaInput,
+.creationMetaRow select.metaInput {
+ height: 28px !important;
+ min-height: 28px !important;
+ padding: 3px 7px !important;
+ font-size: 12px !important;
+ line-height: 1.1 !important;
+ border-width: 2px !important;
+}
+
+.creationMetaRow .dimensionSeparator {
+ font-size: 15px !important;
+}
+
+.paintLayout {
+ align-items: start !important;
+}
+
+.drawControlPanel {
+ display: grid;
+ gap: 8px;
+ align-self: start;
+ min-width: 0;
+}
+
+.drawControlPanel .basicEditorTools,
+.drawControlPanel .editorUtilityRow {
+ margin: 0 !important;
+}
+
+.drawControlPanel .advancedPanel {
+ margin: 0 !important;
+}
+
+#saveAndPlace.bigPrimary {
+ min-width: 0 !important;
+ max-width: 100% !important;
+ white-space: normal !important;
+ overflow-wrap: anywhere !important;
+ line-height: 1.15 !important;
+ flex-wrap: wrap !important;
+ row-gap: 3px !important;
+ padding-left: 8px !important;
+ padding-right: 8px !important;
+}
+
+#saveAndPlace.bigPrimary > span:first-child {
+ min-width: 0 !important;
+}
+
+#saveAndPlace .finishQuotaBadge {
+ max-width: 100% !important;
+ white-space: normal !important;
+ overflow-wrap: anywhere !important;
+ line-height: 1.1 !important;
+ font-size: 9px !important;
+ padding: 2px 5px !important;
+}
+
+.collectionViewTabs.tabs {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ border: 2px solid var(--line);
+ box-shadow: 3px 3px 0 rgba(36,48,68,.12);
+ margin: 2px 0 0;
+ padding: 6px !important;
+ gap: 6px !important;
+}
+
+.collectionViewTab.tab {
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 7px 5px !important;
+ font-size: 12px !important;
+}
+
+@media (min-width: 761px) {
+ .studioDrawer {
+ width: clamp(520px, 42vw, 760px) !important;
+ }
+
+ .creationMetaRow {
+ grid-template-columns: minmax(120px, 1.35fr) clamp(42px, 4vw, 58px) 12px clamp(42px, 4vw, 58px) minmax(82px, .9fr) !important;
+ gap: 8px !important;
+ margin-bottom: 8px !important;
+ }
+
+ .paintLayout {
+ grid-template-columns: minmax(0, 1fr) clamp(148px, 12vw, 210px) !important;
+ gap: 12px !important;
+ margin-bottom: 10px !important;
+ }
+
+ .paintMainColumn {
+ grid-template-rows: auto auto !important;
+ gap: 8px !important;
+ }
+
+ .paintMainColumn #paintCanvas {
+ max-height: min(58vh, 560px) !important;
+ }
+
+ .paintMainColumn .palettePanel {
+ min-height: 0 !important;
+ align-self: start !important;
+ padding: 7px !important;
+ }
+
+ .paintMainColumn .paletteGrid {
+ grid-template-columns: repeat(10, minmax(0, 1fr)) !important;
+ gap: 3px !important;
+ }
+
+ .paintMainColumn .paletteSwatch,
+ .paintMainColumn .paletteGrid button {
+ border-width: 2px !important;
+ }
+
+ .drawControlPanel .basicEditorTools {
+ grid-template-columns: 1fr !important;
+ gap: 6px !important;
+ }
+
+ .drawControlPanel .editorUtilityRow {
+ grid-template-columns: 1fr !important;
+ gap: 6px !important;
+ }
+
+ .drawControlPanel .basicEditorTools .tool,
+ .drawControlPanel .editorUtilityRow .tool {
+ min-height: 36px !important;
+ padding: 7px 6px !important;
+ font-size: 12px !important;
+ }
+
+ .drawControlPanel .advancedPanel {
+ padding: 8px !important;
+ max-height: calc(100vh - 330px);
+ overflow: auto;
+ }
+
+ .drawControlPanel .advancedPanelHeader {
+ display: block !important;
+ font-size: 12px !important;
+ margin-bottom: 6px !important;
+ }
+
+ .advancedPanel .advancedDrawRow,
+ .advancedPanel .advancedActionRow,
+ .advancedPanel .extraAdvancedRow {
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
+ gap: 5px !important;
+ margin-bottom: 6px !important;
+ }
+
+ .advancedPanel .tool {
+ min-height: 30px !important;
+ padding: 5px 4px !important;
+ font-size: 10px !important;
+ }
+
+ .advancedPanel .particleControlGroup {
+ grid-column: 1 / -1;
+ }
+}
+
+@media (min-width: 761px) and (max-height: 780px) {
+ .drawerHead {
+ padding-top: 4px !important;
+ padding-bottom: 3px !important;
+ }
+ .drawerHead p {
+ display: none;
+ }
+ .tabs {
+ padding-top: 4px !important;
+ padding-bottom: 4px !important;
+ }
+ .drawControlPanel .advancedPanel {
+ max-height: calc(100vh - 285px);
+ }
+}
+
+@media (max-width: 760px) {
+ .creationMetaRow .metaInput,
+ .creationMetaRow select.metaInput {
+ height: 26px !important;
+ min-height: 26px !important;
+ font-size: 12px !important;
+ padding: 3px 6px !important;
+ }
+
+ .paintLayout {
+ grid-template-columns: 1fr !important;
+ gap: 10px !important;
+ }
+
+ .paintMainColumn {
+ grid-template-rows: auto auto !important;
+ }
+
+ .paintMainColumn .paletteGrid {
+ grid-template-columns: repeat(8, minmax(0, 1fr)) !important;
+ }
+
+ .drawControlPanel {
+ gap: 8px;
+ }
+
+ .drawControlPanel .advancedPanel {
+ margin-top: 0 !important;
+ }
+
+ #saveAndPlace.bigPrimary {
+ flex-direction: column;
+ }
+
+ .collectionViewTab.tab {
+ font-size: 11px !important;
+ }
+}
+
+@media (min-width: 761px) {
+ .studioDrawer.advancedTools .drawControlPanel .editorToolRow,
+ .studioDrawer.advancedTools .drawControlPanel .basicEditorTools,
+ .studioDrawer.advancedTools .drawControlPanel .editorUtilityRow {
+ grid-template-columns: 1fr !important;
+ }
+}
+
+:root { --drawer-z: 70; --bubble-z: 34; }
+[hidden], .advancedToolGroup[hidden], .advancedOnly[hidden], #particleDirectionWrap[hidden] { display: none !important; }
+.studioDrawer { z-index: var(--drawer-z) !important; }
+.selectionBubble { z-index: var(--bubble-z) !important; }
+.studioDrawer:not(.advancedTools) .advancedPanel { display: none !important; }
+
+.tool {
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ gap: 6px !important;
+ border: 2px solid var(--line, #243044) !important;
+ color: var(--line, #243044) !important;
+ background: var(--toolBg, #f7f4ea) !important;
+ box-shadow: 2px 2px 0 rgba(36,48,68,.14) !important;
+}
+.tool:where(:not(:disabled)):hover { transform: translate(-1px, -1px); box-shadow: 3px 3px 0 rgba(36,48,68,.16) !important; }
+.tool.active { color: #fffdf5 !important; background: var(--toolActiveBg, var(--toolAccent, #243044)) !important; }
+.toolIcon, .toolPixelIcon, .tool .pixelIcon { display:inline-block; width:14px; height:14px; flex:0 0 14px; image-rendering:pixelated; }
+.toolBrush { --toolBg:#dcfce7; --toolAccent:#16a34a; --toolActiveBg:#15803d; }
+.toolErase { --toolBg:#fee2e2; --toolAccent:#ef4444; --toolActiveBg:#dc2626; }
+.toolFill { --toolBg:#fef3c7; --toolAccent:#f59e0b; --toolActiveBg:#d97706; }
+.toolPick { --toolBg:#e0f2fe; --toolAccent:#0284c7; --toolActiveBg:#0369a1; }
+.toolAdvanced { --toolBg:#ede9fe; --toolAccent:#7c3aed; --toolActiveBg:#6d28d9; }
+.toolUndo, .toolRedo { --toolBg:#f3f4f6; --toolAccent:#64748b; --toolActiveBg:#475569; }
+.toolLine, .toolDepth { --toolBg:#dbeafe; --toolAccent:#2563eb; --toolActiveBg:#1d4ed8; }
+.toolRect, .toolParticle { --toolBg:#ccfbf1; --toolAccent:#0d9488; --toolActiveBg:#0f766e; }
+.toolSelect { --toolBg:#fae8ff; --toolAccent:#c026d3; --toolActiveBg:#a21caf; }
+.toolDoor { --toolBg:#ffedd5; --toolAccent:#c2410c; --toolActiveBg:#9a3412; }
+.toolOutline { --toolBg:#e0e7ff; --toolAccent:#4f46e5; --toolActiveBg:#4338ca; }
+.toolFlipH, .toolFlipV { --toolBg:#fce7f3; --toolAccent:#db2777; --toolActiveBg:#be185d; }
+.toolClearSel { --toolBg:#f1f5f9; --toolAccent:#475569; --toolActiveBg:#334155; }
+.toolLight { --toolBg:#fef9c3; --toolAccent:#ca8a04; --toolActiveBg:#a16207; }
+.tool.danger, #clearPaint { --toolBg:#ffe4e6; --toolAccent:#e11d48; --toolActiveBg:#be123c; }
+#toolPick.toolPick, #redoPaint.toolRedo { border-right: 2px solid var(--line, #243044) !important; }
+
+@media (min-width: 761px) {
+ .studioDrawer.advancedTools { width: clamp(560px, 48vw, 720px) !important; max-width: calc(100vw - 20px) !important; }
+ .studioDrawer.advancedTools .paintLayout { grid-template-columns: minmax(0, 1fr) clamp(220px, 18vw, 270px) !important; gap: 10px !important; }
+ .studioDrawer.advancedTools .paintMainColumn #paintCanvas { max-height: min(58vh, 560px) !important; max-width: none !important; justify-self: stretch !important; }
+ .studioDrawer.advancedTools .palettePanel { width: min(100%, 620px) !important; padding: 6px !important; justify-self: stretch !important; }
+ .studioDrawer.advancedTools .paletteGrid { grid-template-columns: repeat(14, minmax(16px, 1fr)) !important; gap: 3px !important; }
+ .studioDrawer.advancedTools .drawControlPanel { display:grid !important; grid-template-columns: minmax(76px, 90px) minmax(120px, 1fr) !important; grid-template-rows:auto auto !important; gap:7px 8px !important; }
+ .studioDrawer.advancedTools .basicEditorTools { grid-column:1 !important; grid-row:1 !important; }
+ .studioDrawer.advancedTools .editorUtilityRow { grid-column:1 !important; grid-row:2 !important; }
+ .studioDrawer.advancedTools .advancedPanel { grid-column:2 !important; grid-row:1 / span 2 !important; display:grid !important; padding:7px !important; overflow:visible !important; }
+ .studioDrawer.advancedTools .advancedDrawRow,
+ .studioDrawer.advancedTools .advancedActionRow,
+ .studioDrawer.advancedTools .extraAdvancedRow { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; gap:5px !important; }
+ .studioDrawer.advancedTools .advancedPanel :is(.tool, select, .particleControl) { min-height:30px !important; padding:5px !important; font-size:10px !important; }
+ .studioDrawer.advancedTools .particleControlGroup { grid-column:1 / -1 !important; }
+}
+
+.collectionToolbar { display:flex !important; align-items:center !important; gap:6px !important; flex-wrap:nowrap !important; padding:7px !important; overflow:hidden !important; }
+.collectionSummary { flex:0 0 auto !important; white-space:nowrap !important; font-size:11px !important; }
+.collectionSearch { flex:0 0 108px !important; width:108px !important; min-width:92px !important; min-height:26px !important; padding:3px 6px !important; font-size:10px !important; }
+.collectionFilters { flex:1 1 auto !important; min-width:0 !important; display:flex !important; flex-wrap:nowrap !important; gap:5px !important; overflow-x:auto !important; overflow-y:hidden !important; scrollbar-width:thin; }
+.collectionFilter { flex:0 0 auto !important; padding:4px 7px !important; font-size:10px !important; white-space:nowrap !important; }
+
+@media (max-width: 760px) {
+ .collectionToolbar { align-items:stretch !important; overflow-x:auto !important; }
+ .collectionSummary { display:none !important; }
+ .collectionSearch { flex-basis:104px !important; width:104px !important; }
+ .studioDrawer { z-index: var(--drawer-z) !important; }
+}