diff --git a/DEPLOY_SHARED_JA.md b/DEPLOY_SHARED_JA.md deleted file mode 100644 index 975bf99..0000000 --- a/DEPLOY_SHARED_JA.md +++ /dev/null @@ -1,109 +0,0 @@ -# Pixel Island shared test build - -この ZIP は `public_html` 直下に置く試験公開用です。HTTPS 前提で、同一フォルダ配下に DB を作成します。 - -## 置き方 - -`public_html` 直下が次の構成になるようにアップロードしてください。 - -```text -public_html/ - index.html - app.js - styles.css - js/ - api/ - index.php - config.php - filedb.php - admin/ - index.php - _data/ - .htaccess - web.config - README.txt -``` - -## DB の場所 - -標準は SQLite です。 - -```text -public_html/_data/pixel_island.sqlite -``` - -サーバーの PHP に `pdo_sqlite` が無い場合は、自動でロック付き JSON ファイル DB に切り替わります。 - -```text -public_html/_data/pixel_island_filedb.json -``` - -`_data` は Web から直接読まれないよう、Apache 用 `.htaccess` と IIS 用 `web.config` を同梱しています。サーバー設定によって効かない場合があるため、公開後に `https://あなたのURL/_data/` や `https://あなたのURL/_data/pixel_island_filedb.json` が直接見えないことを確認してください。 - -## 必要なサーバー機能 - -| 機能 | 必須度 | 用途 | -|---|---:|---| -| HTTPS | 必須 | アカウントID・パスワード送信保護 | -| PHP 8.x | 必須 | API 実行 | -| 書き込み権限 | 必須 | `_data` に DB を作成・更新 | -| `pdo_sqlite` | 推奨 | SQLite DB 使用 | -| `.htaccess` または `web.config` の deny | 推奨 | `_data` 直アクセス防止 | - -## 動作確認 URL - -```text -https://あなたのURL/api/index.php?action=health -``` - -正常例: - -```json -{"ok":true,"service":"pixel-island-api","sqlite":true} -``` - -`sqlite:false, fileDb:true` の場合、SQLite が使えず JSON ファイル DB で動いています。少人数テストなら可、本公開では SQLite 以上を推奨します。 - -## 今回実装した段階 - -### Stage 1: 共有バックエンド - -- `api/index.php` を追加 -- `_data` 配下に DB を作成 -- 作品・配置・投票・通報をサーバー保存 -- フロントエンドは起動時と約5秒ごとに共有データを同期 -- API が使えない場合はローカル専用にフォールバック - -### Stage 2: アカウント・所有者検証 - -- ブラウザ側のローカルアカウントをサーバー登録 -- パスワードはサーバー側でハッシュ保存 -- 作品作成・削除・配置・移動は所有者のみ許可 -- 1時間あたりの投稿数をサーバー側で制限 - -### Stage 3: 通報・管理画面 - -- 通報を DB に保存 -- `admin/index.php` を追加 -- SQLite / JSON fallback の両方で管理画面が動作 -- 管理画面からオブジェクトを非表示 / 復元 / 通報クローズ可能 - -管理画面を使う場合は `api/config.php` の `admin_token` に長いランダム文字列を設定し、次の形式で開きます。 - -```text -https://あなたのURL/admin/?token=設定したトークン -``` - -## 未実装・今後の段階 - -| 段階 | 内容 | -|---|---| -| Stage 4 | 管理者ログイン、管理者ロール、CSRF 対策強化、操作監査ログ | -| Stage 5 | 画像/投稿のより厳密なサーバー側バリデーション | -| Stage 6 | 差分同期、ページング、重い島での高速化 | -| Stage 7 | 自動モデレーション、NGワード、IP/端末単位レート制限 | -| Stage 8 | バックアップ、エクスポート、DB復旧手順 | - -## 注意 - -このビルドは「試験公開」用です。本公開する場合は少なくとも SQLite が使える状態にし、`_data` の直アクセス不可確認、定期バックアップ、管理トークン設定を行ってください。SQLite が無い環境でも JSON fallback で少人数テストは可能ですが、同時アクセスやデータ破損リスクを考えると長期運用には向きません。 diff --git a/README.md b/README.md index 5456d68..a97485c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Pixel Island Summoner +# Pixel Island Browser-only prototype for a shared pixel-art island. diff --git a/_data/default_gallery_seed.json b/_data/default_gallery_seed.json new file mode 100644 index 0000000..145b743 --- /dev/null +++ b/_data/default_gallery_seed.json @@ -0,0 +1 @@ +{"assets":[{"id":"n2hvqzh0n69d","name":"Pine Cluster","category":"static","subtype":"nature","size":16,"width":16,"height":16,"blobId":"blob:793c0d9e","pixels":"......................................................................aaa............aaaaa...........Taaaa..........aaaaaaUUU.......aaaaaaTaaa.....aaaaaaUUUUU.....aaaaaaaaaaaa...aaaaaaUUUUUUU...aaaaaaaaaaaaaa......jUUUUUUUUU......jj..jj..........jj..jj....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565874,"updatedAt":1780548565874,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"......................................111............11111...........11111..........111111111.......1111111111.....11111111111.....111111111111...1111111111111...11111111111111......1111111111......11..11..........11..11....................................","door":null},"contentHash":"fnv1a:653a0bc5"},{"id":"3qlcofe0n69f","name":"Coral Skiff","category":"static","subtype":"ship","size":16,"width":16,"height":16,"blobId":"blob:04ed9b16","pixels":"........................................................q...............q..............kuq............kkuq...........kkuuq..........kuuuuq.............qqq.........A...qq...A.....AAAwwwwwwAA.....AwwwwwwwwwA......wwwwwwww.........rrrrrr...........rrrr.......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565876,"updatedAt":1780548565876,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"........................1...............1..............11.............111............1111...........11111..............---.........1...--...1.....111-------1.....-----------......--------.........------...........----.......................................","door":null},"contentHash":"fnv1a:15b27b06"},{"id":"9cs2zey0n69h","name":"Lantern Walker","category":"dynamic","subtype":"human","size":16,"width":16,"height":16,"blobId":"blob:b6f5e31d","pixels":"......................................................................................000............0iii0...........0i1i0............iii............AAAAA..........AAAAuAu.........AAAA.uu..........>>>A............>.>>............>..>...........00..00......","faces":{"right":"......................................................................................000............0iii0...........0i1i0............iii............AAAAA..........AAAAuAu.........AAAA.uu..........>>>A............>.>>............>..>...........00..00......","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565877,"updatedAt":1780548565877,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":9,"y":7,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...................................1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111......1111111111...................................................","door":null},"contentHash":"fnv1a:26cdf94f"},{"id":"kbpnn6s0n69i","name":"Wildflower Patch","category":"static","subtype":"nature","size":8,"width":8,"height":8,"blobId":"blob:67c60875","pixels":".........TzTJT..TkT#TzT.TTTTTTT.TJTzTkT.TTTTTTT..aTaTa....jj....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565878,"updatedAt":1780548565878,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".........11111..1111111.1111111.1111111.1111111..11111....11....","door":null},"contentHash":"fnv1a:76ecdc0f"},{"id":"0sf0j050n6a1","name":"Town Gardener","category":"dynamic","subtype":"human","size":16,"width":12,"height":16,"blobId":"blob:1df4391f","pixels":"................................................................rr.........riir........i44i........iiqii........qOO........qOOOO.......qOOOO........jjj........jj.jj.......j...j......rr...rr...","faces":{"right":"................................................................rr.........riir........i44i........iiqii........qOO........qOOOO.......qOOOO........jjj........jj.jj.......j...j......rr...rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565897,"updatedAt":1780548565897,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................11.........1111........1111........11111........111........11111.......11111........111........11.11.......1...1......11...11...................................................","door":null},"contentHash":"fnv1a:2aa287bb"},{"id":"62fma7d0n6a1","name":"Lantern Courier","category":"dynamic","subtype":"human","size":16,"width":12,"height":16,"blobId":"blob:5d09ebee","pixels":"................................................................rr.........riir........i44i........iiii.........AAAA.......AAAAAu......AAAA.u.......1111.......11.11.......1...1......rr...rr...","faces":{"right":"................................................................rr.........riir........i44i........iiii.........AAAA.......AAAAAu......AAAA.u.......1111.......11.11.......1...1......rr...rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565898,"updatedAt":1780548565898,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................11.........1111........1111........1111.........1111.......111111......1111.1.......1111.......11.11.......1...1......11...11...................................................","door":null},"contentHash":"fnv1a:19e811ae"},{"id":"kq4qsga0n6a2","name":"Plaza Musician","category":"dynamic","subtype":"human","size":16,"width":14,"height":16,"blobId":"blob:8ed2ab35","pixels":"...........................................................................rr...........riir..........i44i..........iiii.........222222........2xxvvv........2xxvvv.........vqqv.........vq..qv........r....r.......rr....rr....","faces":{"right":"...........................................................................rr...........riir..........i44i..........iiii.........222222........2xxvvv........2xxvvv.........vqqv.........vq..qv........r....r.......rr....rr....","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565899,"updatedAt":1780548565899,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...................11...........1111..........1111..........1111.........111111........111111........111111.........1111.........11..11........1....1.......11....11............................................................","door":null},"contentHash":"fnv1a:015eb262"},{"id":"fctctzb0n6a3","name":"Bridge Mechanic","category":"dynamic","subtype":"human","size":16,"width":14,"height":16,"blobId":"blob:3605a1bd","pixels":"...........................................................................rr...........riir..........i44i..........iiii.........222222........221222........22122v.........qqqq.........qq.qq.........q...q........rr...rr.....","faces":{"right":"...........................................................................rr...........riir..........i44i..........iiii.........222222........221222........22122v.........qqqq.........qq.qq.........q...q........rr...rr.....","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565899,"updatedAt":1780548565899,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...................11...........1111..........1111..........1111.........111111........111111........111111.........1111.........11.11.........1...1........11...11.............................................................","door":null},"contentHash":"fnv1a:b27bb3f4"},{"id":"yrg1s9d0n6b6","name":"Desert Train","category":"static","subtype":"building","size":44,"width":44,"height":16,"blobId":"blob:cf06fa34","pixels":"................................................................................................ll........................................mm..........................................mm.........................................qqqq...jjjjjjjjjj...jjjjjjjjjj..............qqqq...jjjjjjjjjj...jjjjjjjjjj..............qqii..wwwwwwwwwwwwwvvvvvvvvvvvvv...........pppiippwwuuuwwuuuwwwvvvuuuvvuuuvvwwwww......pppppppwwuuuwwuuuwwwvvvuuuvvuuuvvwwwww......pppppppwwwwwwwwwwwwwvvvvvvvvvvvvvwwwww......pprppppwwwwrwwwwwrwwvvvvvrvvvvvrvwwwrw.......rrr...qqqrrrqqqrrrqqqqqrrrqqqrrr..rrr......rrprr....rrprr.rrprr...rrprr.rrprrrrprr...vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv..qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565941,"updatedAt":1780548565941,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":7,"y":7,"c":"u"},{"x":13,"y":8,"c":"u"},{"x":19,"y":8,"c":"u"},{"x":28,"y":8,"c":"u"},{"x":35,"y":8,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","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:ebde0b11"},{"id":"jzwj2cr0n6cf","name":"Tideglass Lighthouse","category":"static","subtype":"building","size":36,"width":20,"height":36,"blobId":"blob:5ae88912","pixels":"........rrrr...............rIIIIr..............rIllIr.............rIIllIIr............rrrllrrr.............r.ll.r..............r.**.r.............rr.**.rr............rllllllr............rllIIllr...........rrllIIllrr..........rllllllllr..........rlliiiillr..........rlliwwillr..........rlliiiillr.........rrllllllllrr........rllllIIllllr........rllllIIllllr........rllllllllllr........rllliiiilllr........rllliwwilllr........rllliiiilllr.......rrllllllllllrr......rllllllllllllr......rllliilliilllr......rlllwwllwwlllr......rllliilliilllr.....rrllllllllllllrr....rllllllllllllllr....rllllllrrllllllr....rllllllrrllllllr....rlllliirriillllr....rllllwwrrwwllllr....rlllliirriillllr.....rrrrrrrrrrrrrr...........rrrr........","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548565986,"updatedAt":1780548565986,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":2,"c":"I"},{"x":9,"y":2,"c":"I"},{"x":8,"y":16,"c":"I"}],"lightColor":"I","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","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:8a4bcc50"},{"id":"mivaahd0n6dh","name":"Puddle Toad","category":"dynamic","subtype":"animal","size":12,"width":12,"height":10,"blobId":"blob:099171bf","pixels":"...........................rr..rr.....rpprrppr...rppppppppr..rpprpprppr..rppppppppr..r))pppp))r...r))))))r.....rrrrrr...","faces":{"right":"...........................rr..rr.....rpprrppr...rppppppppr..rpprpprppr..rppppppppr..r))pppp))r...r))))))r.....rrrrrr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566022,"updatedAt":1780548566022,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...............11..11.....11111111...1111111111..1111111111..1111111111..1111111111...11111111.....111111...............","door":null},"contentHash":"fnv1a:cb2f39b6"},{"id":"4j8cpe50n6e8","name":"Rosetta Stela","category":"static","subtype":"building","size":32,"width":24,"height":32,"blobId":"blob:26ba34e0","pixels":"................................rrrrrrrr...............rppppppppr.............rppppppppppr............rppppppppppr............rppqqqppqqpr............rppppppppppr............rppqqppqqppr............rppppppppppr............rpppqqqppppr............rppppppppppr............rppqqppppqpr............rppppppppppr............rppqqqppqqpr............rppppppppppr............rppqqppqqppr............rppppppppppr............rpppqqqppppr............rppppppppppr............rppqqppppqpr............rppppppppppr............rppqqqppqqpr............rppppppppppr............rppqqppqqppr............rppppppppppr............rpppqqqppppr............rppppppppppr............rppppqqppppr............rppppqqppppr............rppppqqppppr.............rpppqqpppr...............rrrrrrrr........","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566051,"updatedAt":1780548566051,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","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:60d3d86f"},{"id":"sb4s8ot0n6ev","name":"Rain Bell Tower","category":"static","subtype":"building","size":36,"width":20,"height":36,"blobId":"blob:072b4156","pixels":"............................rrrr...............rEEEEr.............rEEEEEEr............rEErrEEr............rrr33rrr.............r3333r..............r3333r.............rr3333rr............r33oo33r............r33oo33r............r33oo33r............r33II33r............r33oo33r...........rr333333rr..........r33oooo33r..........r33oqqo33r..........r33oooo33r..........r33333333r..........r333II333r..........r333oo333r..........r333oo333r..........r333oo333r.........rr333oo333rr........r3333333333r........r333oooo333r........r333oqqo333r........r333oooo333r........r3333333333r........r3333rr3333r........r3333rr3333r........r33oorroo33r........r33oorroo33r........r3333rr3333r.........rrrrrrrrrr.............rrrr........","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566074,"updatedAt":1780548566074,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":9,"y":11,"c":"I"},{"x":9,"y":19,"c":"I"}],"lightColor":"I","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","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:7c4e5c26"},{"id":"g55cv650n6g7","name":"Aurora Bathhouse","category":"static","subtype":"building","size":28,"width":28,"height":28,"blobId":"blob:65fd5e55","pixels":"...............................................................................................'.....'..................'.!...'.!...'..............!.....!.....!...............................................r[[[[r.....................r[[[[[[r...................r[[[[[[[[r.................r[[[[[[[[[[r...............r[[[[[[[[[[[[r.............r[[[[[[[[[[[[[[r...........r[[[[[[[[[[[[[[[[r..........r1111111111111111r..........rrrrrrrrrrrrrrrrrr...........rrrrrrrrrrrrrrrr............r**************r............r**************r............r**'''****'''**r............r**'''****'''**r............r**'''****'''**r............r*****1uu1*****r............r*****1uu1*****r............r*****1uu1*****r..........rrrrrrrrrrrrrrrrrrrr........rr1111111111111111rr..........1111111111111111......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566122,"updatedAt":1780548566122,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":10,"y":17,"c":"!"},{"x":17,"y":17,"c":"!"},{"x":14,"y":22,"c":"!"}],"lightColor":"!","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".......................................1.....1..................1.1...1.1...1..............1.....1.....1...............................................111111.....................11111111...................1111111111.................111111111111...............11111111111111.............1111111111111111...........111111111111111111..........111111111111111111..........111111111111111111...........1111111111111111............1111111111111111............1111111111111111............1111111111111111............1111111111111111............1111111111111111............1111111111111111............1111111111111111............1111111111111111..........11111111111111111111........11111111111111111111..........1111111111111111..............................................................","door":{"x":14,"y":24}},"contentHash":"fnv1a:23700d0b"},{"id":"phyuejx0n6gc","name":"Copper Windmill","category":"static","subtype":"building","size":32,"width":24,"height":32,"blobId":"blob:2164c2a6","pixels":"....................................................................................r.......................w.......................w.......................w.......................w.......................w...............rw......r......wr.........wwr...r...rww.............wwrrrrrww.................rrDrr.................rEErrrEr................rErErErr................rrwEEEwr................rwDEEDDw................rwDEEDDwr..............rwDDEEDDrwr............rwEEEEEEEErwr............rEEEEEEEEr..............rEEEEEEEEr..............rEEEEEEEEr..............rEErxxrEEr..............rEErxxrEEr.............rEEErxxrEEEr............rEEErxxrEEEr............rEEErxxrEEEr............rEEErxxrEEEr............rxxxrxxrxxxr............rxxxxxxxxxxr..........oooooooooooooooo....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566126,"updatedAt":1780548566126,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":11,"y":21,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"............................................................1.......................1.......................1.......................1.......................1.......................1...............11......1......11.........111...1...111.............111111111.................11111.................11111111................11111111................11111111................11111111................111111111..............11111111111............1111111111111............1111111111..............1111111111..............1111111111..............1111111111..............1111111111.............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111..........1111111111111111............................","door":{"x":11,"y":28}},"contentHash":"fnv1a:d46de7aa"},{"id":"bnw4oof0n6gg","name":"Moon Gate Bridge","category":"static","subtype":"nature","size":32,"width":32,"height":20,"blobId":"blob:11ad7c52","pixels":"................................................................................r...........................rrrrrrrrr......................rrrrrirrrrr....................rrriii'iiirrr..................rrriiiiiiiiirrr................rrriiiii*iiiiirrr...............rriiii*****iiiirr...............rIiii*******iiIrr...............rrii*********iirr..............rriii*********iiirr..........i..irrii*********iirri..i.......i..irri***********irri..i.....rpippirrii*********iirrippir....rpippirrri*********irrrippir....rrrrrrrrrr*********rrrrrrrrr............rrr.......rrr....................rrr.....rrr......................rrrr.rrrr...........","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566131,"updatedAt":1780548566131,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":9,"y":9,"c":"'"},{"x":22,"y":9,"c":"'"}],"lightColor":"'","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................................................................................1...........................111111111......................11111111111....................1111111111111..................111111111111111................11111111111111111...............11111111111111111...............11111111111111111...............11111111111111111..............1111111111111111111..........1..1111111111111111111..1.......1..1111111111111111111..1.....1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111............111.......111....................111.....111......................1111.1111...........","door":null},"contentHash":"fnv1a:02dc18c4"},{"id":"t9fn2tv0n6gl","name":"Starlit Bookshop","category":"static","subtype":"building","size":28,"width":24,"height":28,"blobId":"blob:d0c9aec7","pixels":"....................................................................................................................................u.......................u...................rruuDuur................rooouoor................rooouoor............r111rrrrrrrr111r........r11111111111111r........rrrrrrrrrrrrrrrr.........rppppppppppppr..........rppppppppppppr..........rppppppppppppr..........rp****pp****pr..........rp****pp****pr..........rp****pp****pr..........rp****pp****pr..........rp****pp****pr..........rppppppppppppr..........rppppppppppppr..........rpppp1uu1ppppr..........rpppp1uu1ppppr..........rpppp1uu1ppppr..........rpppp1uu1ppppr..........rrrrr1uu1rrrrr.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566135,"updatedAt":1780548566135,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":14,"c":"u"},{"x":15,"y":14,"c":"u"},{"x":12,"y":7,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"............................................................1.......................1...................11111111................11111111................11111111............1111111111111111........1111111111111111........1111111111111111.........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111..........11111111111111.............................................................................","door":{"x":12,"y":24}},"contentHash":"fnv1a:8b2786aa"},{"id":"pmnlpo50n6gp","name":"Crystal Bakery","category":"static","subtype":"building","size":24,"width":24,"height":24,"blobId":"blob:3bb15ec5","pixels":"..........................................................................................................*......|...............*.*....|.|..............*.*....|.|.............*...*..|...|................................rr||rr||rr||rr||........rr||rr||rr||rr||........rr||rr||rr||rr||........r{{{{{{{{{{{{{{r........r{{{{{{{{{{{{{{r........r{{{{{{{{{{{{{{r........r{*****{{*****{r........r{*****{{*****{r........r{*I***{{***I*{r........r{*****{{*****{r........r{{{{{1111{{{{{r........r{{{{{1111{{{{{r........r{{{{{1111{{{{{r........rrrrrr1111rrrrrr........2222222222222222....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566139,"updatedAt":1780548566139,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":7,"y":14,"c":"{"},{"x":16,"y":14,"c":"{"}],"lightColor":"{","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..........................................................1......1...............1.1....1.1..............1.1....1.1.............1...1..1...1................................1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111........1111111111111111....................................................","door":{"x":12,"y":21}},"contentHash":"fnv1a:f8fe7fec"},{"id":"hwk6lbf0n6gu","name":"Kelp Observatory","category":"static","subtype":"building","size":32,"width":28,"height":32,"blobId":"blob:4b8821e5","pixels":"..................................................................................................r.......................rrrrrrrrr.................rrrrrr*rrrrrr..............rrr*********rrr............rrr***********rrr...........rr*************rr...........rr*****k*******rr..........rr*******u*******rr..........rr*************rr...........rr*************rr...........rrr***********rrr..........rrrrrrrrrrrrrrrrrr..........rr22222222222222rr..........rr22222222222222rr............rrrrrrrrrrrrrr..............r111111111111r..............r111111111111r..............r11***111***1r............2.r11***111*2*1r............2.r12***111*2*1r2...........2.r12***211*2*1r2..........2..r121112111211r2..........2..r121112111211r.2.........2..r121122221211r.2.........2..r211122221121r.2.........2..r211122221121r.2........2...r2rrr2222rr2rr..2.......2....2....2....2....2.......2....2....2....2....2....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566145,"updatedAt":1780548566145,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":13,"y":9,"c":"V"},{"x":14,"y":9,"c":"V"},{"x":9,"y":19,"c":"V"},{"x":18,"y":19,"c":"V"}],"lightColor":"V","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"......................................................................1.......................111111111.................1111111111111..............111111111111111............11111111111111111...........11111111111111111...........11111111111111111..........1111111111111111111..........11111111111111111...........11111111111111111...........11111111111111111..........111111111111111111..........111111111111111111..........111111111111111111............11111111111111..............11111111111111..............11111111111111..............11111111111111............1.11111111111111............1.111111111111111...........1.111111111111111..........1..111111111111111..........1..11111111111111.1.........1..11111111111111.1.........1..11111111111111.1.........1..11111111111111.1........1...11111111111111..1.......1....1....1....1....1.......1....1....1....1....1................................","door":{"x":14,"y":28}},"contentHash":"fnv1a:d5177db2"},{"id":"z2z8kii0n6gz","name":"Ember Shrine","category":"static","subtype":"building","size":28,"width":20,"height":28,"blobId":"blob:d15d7e34","pixels":"...................................................................................................................................................................rwwwwwwwwwwwwr......rrrrrrrrrrrrrr........rr......rr..........rr...v..rr..........rr.vvvvvrr..........rrvvvvvvvr..........rrvvvEvvvr..........rrvvEEEvvr..........rvvvEEEvvv..........rrvEEEEEvr..........rrvvEEEvvr..........rrvvEEEvvr.........rrrrvvEvvrrr........rxxxxxvxxxxr........rxxxDDDDxxxr........rxxxDDDDxxxr........rxxxDDDDxxxr........rxxxDDDDxxxr........rrrrrrrrrrrr..........rrrrrrrr......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566149,"updatedAt":1780548566149,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":10,"y":13,"c":"E"}],"lightColor":"E","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...........................................................................................................................11111111111111......11111111111111........11......11..........11...1..11..........11.1111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111..........1111111111.........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111........111111111111..........11111111..............................................","door":{"x":10,"y":24}},"contentHash":"fnv1a:a1262b2d"},{"id":"w88g0wn0n6h4","name":"Honeycomb Greenhouse","category":"static","subtype":"building","size":28,"width":28,"height":24,"blobId":"blob:a1e22ebd","pixels":"..........................................................................................................................................................r......................rrrrrrrrrrr...............rrrrrrrSrrrrrrr............rrrSSSSSSSSSSSrrr..........rrrSSSSSSSSSSSSSrrr........rrSSSSSSSSSSSSSSSSSrr......rrSpSSSppSSuppSSSppSSrp.....rrSSpSpSSpSpSSpSpSSpSpr.....rrSSSpSSSSpSSSSpSSSSprr....rrrrrprrrrprrrrprrrrprrrr....rSSpSSSppSSSppSSSppSSrp.....rSSSpSpSSpSpSSpSpSSpSpr.....rSSSSpSSSSpSSSSpSSSSprr.....rSSSpSSSSpSSSSpSSSSpSr......rSSpSSSpppppppSSSppSSrp.....rSSSpSpSpppppppSpSSpSp......rrrrrprrpppppprprrrrpr..........p.rrrprrrrprr..p...........p....p..r.p....p......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566155,"updatedAt":1780548566155,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":13,"c":"u"},{"x":14,"y":10,"c":"u"},{"x":20,"y":13,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..............................................................................................................................1......................11111111111...............111111111111111............11111111111111111..........1111111111111111111........111111111111111111111......11111111111111111111111.....11111111111111111111111.....11111111111111111111111....1111111111111111111111111....11111111111111111111111.....11111111111111111111111.....11111111111111111111111.....1111111111111111111111......11111111111111111111111.....1111111111111111111111......1111111111111111111111..........1.11111111111..1...........1....1..1.1....1..................................","door":{"x":14,"y":21}},"contentHash":"fnv1a:fd002e43"},{"id":"gvm0fvw0n6hb","name":"Celadon Pagoda","category":"static","subtype":"building","size":36,"width":24,"height":36,"blobId":"blob:05eac9f0","pixels":"............................................................................................................I.......................r.......................r.......................r...............roooooooroooooor........rrrrrrrrrrrrrrrr............rrrrrrrr................riiiiiir................riiIIiir................riiIIiir................riiiiiir................rrrrrrrr..........roooooooooooooooooor....rrrrrrrrrrrrrrrrrrrr..........rrrrrrrr................riiiiiir................riiIIiir................riiIIiir................riiiiiir................rrrrrrrr...........roooooooooooooooor......rrrrrrrrrrrrrrrrrr...........rrrrrrrr................riiiiiir................riiIIiir................riiIIiir................riiiiiir................rrrrrrrr...............rqqqqqqqqr..............rqqqIIqqqr..............rqqqIIqqqr..............rrrrIIrrrr.......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566161,"updatedAt":1780548566161,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":11,"y":15,"c":"N"},{"x":12,"y":15,"c":"N"}],"lightColor":"N","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"............................................................1.......................1.......................1.......................1...............1111111111111111........1111111111111111............11111111................11111111................11111111................11111111................11111111................11111111..........11111111111111111111....11111111111111111111..........11111111................11111111................11111111................11111111................11111111................11111111...........111111111111111111......111111111111111111...........11111111................11111111................11111111................11111111................11111111................11111111...............1111111111..............1111111111..............1111111111..............1111111111.......................................................","door":{"x":12,"y":32}},"contentHash":"fnv1a:491a6042"},{"id":"pajn7gh0n6hg","name":"Tide Pearl Arch","category":"static","subtype":"nature","size":24,"width":24,"height":24,"blobId":"blob:6f5fee28","pixels":"............................................................................................................r....................rrr4rrr...............rrr44444rrr............rrro4'444orrr..........rrro4444444orrr.........rrooo44444ooorr........rroooo44444oooorr.......rroooo..4..oooorr.......rrooo.......ooorr.......rroo.........oorr......rrooo.........ooorr......rroo.........oorr.......rro...........orr.......rr#############rr.......rroo.........oorr.......rrrrr......rrrrr........rrrrr......rrrrr........rrrrro.....rrrrr...........rrrrr.rrrrr...............rrrrrrr........","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566166,"updatedAt":1780548566166,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":12,"y":8,"c":"'"}],"lightColor":"'","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"............................................................................................................1....................1111111...............11111111111............1111111111111..........111111111111111.........111111111111111........11111111111111111.......111111..1..111111.......11111.......11111.......1111.........1111......11111.........11111......1111.........1111.......111...........111.......11111111111111111.......1111.........1111.......11111......11111........11111......11111........111111.....11111...........11111.11111...............1111111........","door":null},"contentHash":"fnv1a:42e457b6"},{"id":"qc94dke0n6hk","name":"Lavender Market","category":"static","subtype":"building","size":32,"width":32,"height":20,"blobId":"blob:19801356","pixels":"...................................................................................................................................................................................................................................................................;;qq;;qq;;qq;;qq;;qq;;qq;;qq....;;qq;;qq;;qq;;qq;;qq;;qq;;qq....;;qq;;qq;;qq;;qq;;qq;;qq;;qq....rttttttttttttttttttttttttr......rttttttttttttttttttttttttr......rttttttttttttttttttttttttr......rttoouootttqqqqtttoouoottr......rttoooootttqqqqtttooooottr......rttoooootttqqqqtttooooottr......rttoooootttqqqqtttooooottr......rttttttttttqqqqttttttttttr......rrrrrrrrrrrrrrrrrrrrrrrrrr...","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566170,"updatedAt":1780548566170,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":12,"c":"]"},{"x":16,"y":12,"c":"]"},{"x":24,"y":12,"c":"]"}],"lightColor":"]","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...................................................................................................................................................................................................1111111111111111111111111111....1111111111111111111111111111....1111111111111111111111111111....11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111......11111111111111111111111111...................................................................","door":{"x":16,"y":17}},"contentHash":"fnv1a:15e89e5d"},{"id":"amolmqe0n6ho","name":"Skyseed Tower","category":"static","subtype":"building","size":36,"width":24,"height":36,"blobId":"blob:c702132d","pixels":"....................................................................................r....................rrrrrrr................rrrrSrrrr...............rrSSSSSrr...............rrSSISSrr..............rrSSSSSSSrr..............rrSSSSSrr...............rrSSSSSrr...............rrrrSrrrr................rrrrrrr................rqqqqqqr...............Trqqqqqqr.T.............Trqqqqqqr.T............T.rqqqqqqr..T...........T.rqSSSSqr..T..........T..rqSSSSqr...T.........T..rqqqqqqr...T.........T..rqqqqqqr...T........T...rqqqqqqr....T.......T..rqqSSSSqqr...T......T...rqqSSSSqqr....T.....T...rqqqqqqqqr....T.........rqqqqqqqqr..............rqqqqqqqqr..............rqqSSSSqqr..............rqqSSSSqqr..............rqqqqqqqqr..............rqqqqqqqqr.............rqqqqqqqqqqr............rqqqqqqqqqqr............rqqrrrrrrqqr............rqqrrrrrrqqr............rqqrrrrrrqqr......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566175,"updatedAt":1780548566175,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":12,"y":6,"c":"S"},{"x":11,"y":22,"c":"S"},{"x":12,"y":22,"c":"S"}],"lightColor":"S","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"....................................1....................1111111................111111111...............111111111...............111111111..............11111111111..............111111111...............111111111...............111111111................1111111................11111111...............111111111.1.............111111111.1............1.11111111..1...........1.11111111..1..........1..11111111...1.........1..11111111...1.........1..11111111...1........1...11111111....1.......1..1111111111...1......1...1111111111....1.....1...1111111111....1.........1111111111..............1111111111..............1111111111..............1111111111..............1111111111..............1111111111.............111111111111............111111111111............111111111111............111111111111............111111111111......................................................","door":{"x":12,"y":32}},"contentHash":"fnv1a:949e0194"},{"id":"uot7d880n6hu","name":"Orchard Terrace","category":"static","subtype":"nature","size":32,"width":32,"height":24,"blobId":"blob:927ad054","pixels":".........................................................................................................................................................................................................r......r.......r.............rrrprrrrrrprrr.rrrprrr.........rrppppprrppppprrrppppprr........rpppppprppppppprpppppppr.......rppvppprppvpppprppvppppppr.......rpppppErpppppEprpppppEpr........rrppppprrppppprrrppppprr......rpprrrprrrrrrprrrprrrprrrr......rrrrrwrrrrrrwrrrrrrrwrrrrr...........ww.....ww......ww..........rppppwwpppppwwppppppwwpppr......rrrrrwwrrrrrwwrrrrrrwwrrrr...........ww.....ww......ww..........rppppppppppppppppppppppppr......rrrrrrrrrrrrrrrrrrrrrrrrrr......................................rppppppppppppppppppppppppr......rrrrrrrrrrrrrrrrrrrrrrrrrr...","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566180,"updatedAt":1780548566180,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":9,"y":8,"c":"E","dir":"up"},{"x":23,"y":10,"c":"v","dir":"up"}],"particleConfig":{"enabled":true,"c":"E","dir":"up"},"depthPixels":".........................................................................................................1......1.......1.............11111111111111.1111111.........111111111111111111111111........111111111111111111111111.......11111111111111111111111111.......111111111111111111111111........111111111111111111111111......11111111111111111111111111......11111111111111111111111111...........11.....11......11..........11111111111111111111111111......11111111111111111111111111...........11.....11......11..........11111111111111111111111111......11111111111111111111111111......................................11111111111111111111111111......11111111111111111111111111...................................................................................................","door":null},"contentHash":"fnv1a:3b898f46"},{"id":"4o17k2h0n6hz","name":"Dreamcatcher Tree","category":"static","subtype":"nature","size":32,"width":28,"height":32,"blobId":"blob:6e3e7ec7","pixels":"..........................................................................................................................................................r......................rrrrrrrrrrr................rrrrrrprrrrrr.............rrrrpppprpppprrrr..........rrrppTrrr:rrrppprrr.........rrpTTrr..:..rrppprr........rrpTTTr...:...rpppprr.......rrTTTTr...:...rpppprr.......rrpTTr::::I::::rTpprr......rrpppTTr...:...rTTTpprr......rrppppr...:...rTTTTrr.......rrpppprr..:..rrTTTprr.......rrppppprrr:rrrTTTpprr........rrppppppprpppTppprr.........rrrppppppppppppprrr..........rrrrppppppppprrrr.............rrrrrrprrrrrr................rrrrrrrrrrr....................rjrr........................rjjr........................rjjr........................rjjr........................rjjr........................rjjr........................rjjr........................rjjr........................rjjr............","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566185,"updatedAt":1780548566185,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":12,"y":9,"c":":","dir":"up"}],"particleConfig":{"enabled":true,"c":":","dir":"up"},"depthPixels":"......................................................................1......................11111111111................1111111111111.............11111111111111111..........1111111111111111111.........1111111..1..1111111........1111111...1...1111111.......1111111...1...1111111.......111111111111111111111......11111111...1...11111111......1111111...1...1111111.......11111111..1..11111111.......111111111111111111111........1111111111111111111.........1111111111111111111..........11111111111111111.............1111111111111................11111111111....................1111........................1111........................1111........................1111........................1111........................1111........................1111........................1111........................1111................................................................................................","door":null},"contentHash":"fnv1a:0735cdb9"},{"id":"moej8bf0n6i2","name":"Nebula Fountain","category":"static","subtype":"nature","size":24,"width":24,"height":24,"blobId":"blob:32fd3772","pixels":"....................................................................................................................................................................................................................................@.....................@@@@@..................@@@I@@@................@@@@@@@@@............*...@@@@@@@...*..........*...@@@@@...*............*..r2@r...*..............*.r22r..*.................r22r.................rrrr22rrrrr............rr**r22r***rr..........rr***r22r****rr.........rrr*********rr..........r2rrrrr*rrrrrr..........rrrrrrrrrrrrrr.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566187,"updatedAt":1780548566187,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":12,"y":9,"c":"!"}],"lightColor":"!","hasParticles":true,"particlePixels":[{"x":9,"y":11,"c":"'","dir":"up"},{"x":15,"y":11,"c":":","dir":"up"}],"particleConfig":{"enabled":true,"c":"'","dir":"up"},"depthPixels":"....................................................................................................................................1.....................11111..................1111111................111111111............1...1111111...1..........1...11111...1............1..1111...1..............1.1111..1.................1111.................11111111111............1111111111111..........111111111111111.........11111111111111..........11111111111111..........11111111111111.....................................................................................................","door":null},"contentHash":"fnv1a:7ba33353"},{"id":"fygtpbt0n6i5","name":"Coral Lighthouse","category":"static","subtype":"building","size":36,"width":24,"height":36,"blobId":"blob:041f0a7c","pixels":".................................................................................rrrrrr..................rIIIIr..................rIIIIr...............rrrrrrrrrrrr............r2222222222r............r2222222222r............rrrrrrrrrrrr..............rvvvvvvr................rvvvvvvr................rvvvvvvr................rDDDDDDr................rDDDDDDr................rDDDDDDr................rvvvvvvr................rvvvvvvr................rv2222vr................rD2222Dr................rD2222Dr................rDDDDDDr................rvvvvvvr................rvvvvvvr................rvvvvvvr................rDDDDDDr................rDDDDDDr................rDDDDDDr................rvvvvvvr................rvvvvvvr................rvvvvvvr................rDDDDDDr................rD2222Dr..................2222................rrrrrrrrrrrr............rrrrrrrrrrrr......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566191,"updatedAt":1780548566191,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":11,"y":6,"c":"I"},{"x":12,"y":6,"c":"I"},{"x":13,"y":6,"c":"I"}],"lightColor":"I","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".........................................................111111..................111111..................111111...............111111111111............111111111111............111111111111............111111111111..............11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111................11111111..................1111................111111111111............111111111111..............................","door":{"x":12,"y":32}},"contentHash":"fnv1a:6976de86"},{"id":"5t7f73w0n6i8","name":"Origami Wind Shrine","category":"static","subtype":"nature","size":24,"width":20,"height":24,"blobId":"blob:3c8a8f38","pixels":"............................................................................................................................rk.........!r........rkk.....!!r..........r.kk.!!.r............r..!..r.............r.....r..............r...r................r.r..................I.................rrrr................r2krr.............rr!!2kkrr..........r!!r22r.kkr........r!..r22r...kr...........r22r................r22r................r22r.............rrrrrrrrrr..........rrrrrrrrrr.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566192,"updatedAt":1780548566192,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":9,"y":8,"c":"?","dir":"up"}],"particleConfig":{"enabled":true,"c":"?","dir":"up"},"depthPixels":"....................................................................................11.........11........111.....111..........1.11.11.1............1..1..1.............1.....1..............1...1................1.1..................1.................1111................11111.............111111111..........1111111.111........11..1111...11...........1111................1111................1111.............1111111111..........1111111111.............................................","door":null},"contentHash":"fnv1a:c0e51239"},{"id":"83ec0rj0n6i9","name":"Rainmaker Drum","category":"static","subtype":"nature","size":20,"width":20,"height":20,"blobId":"blob:2024783a","pixels":".............................................................................................................................*....*....*..........*....*....*.................................r...............rrrrrrrrr.........IrrrrrwrrrrrI......rrIIwwwwwwwIIrr.....rrwwIIwwwIIwwrr....rrxxxxxIIIxxxxwrr....rxxxxIIxIIxxxrr.....rxxIIxxxxxIIxrr......IIxxxxxxxxxII.........rrrrrrrrr...............r.........","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566194,"updatedAt":1780548566194,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".....................................................................................1....1....1..........1....1....1.................................1...............111111111.........1111111111111......111111111111111.....111111111111111....11111111111111111....111111111111111.....111111111111111......1111111111111.........111111111...............1.................................................","door":null},"contentHash":"fnv1a:98fc8d6a"},{"id":"yycojtr0n6ic","name":"Velvet Mushroom Grove","category":"static","subtype":"nature","size":24,"width":24,"height":22,"blobId":"blob:67c57bc6","pixels":".......................................................................................................................................r....................rrr`rrr............r..rr```````rr.......rrr`rrr```|`````r......rr````r```````|```r.....r``|```r```````r`r.....r``````|rr````rr`rr......r```````rrrrr`````r.....rr`````rr..rr`|```r......rrr`rrr...r`````|`r.......rr......rr`````r........ri......rr`````r........ri........rr`rr.........ri.........rr......................ri......................ri........pppppppppppppppppp...","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566197,"updatedAt":1780548566197,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":7,"y":12,"c":"|"},{"x":16,"y":10,"c":"|"}],"lightColor":"|","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...............................................................................................................1....................1111111............1..11111111111.......11111111111111111......1111111111111111111.....111111111111111111.....1111111111111111111......1111111111111111111.....111111111..11111111......1111111...111111111.......11......11111111........11......11111111........11........11111.........11.........11......................11......................11........111111111111111111...........................","door":null},"contentHash":"fnv1a:54799bf5"},{"id":"ryj22ty0n6ie","name":"Star Map Monument","category":"static","subtype":"nature","size":20,"width":20,"height":20,"blobId":"blob:e6f65864","pixels":"..............................................................................................................................rrrr)rrr............r22)222r............r22)222r............r2)111Ir............r2)1112r............r)))112r............r211))))............r21111)r............r211))2r............r22)222r............r222222r............rrrrrrrr...........rrrrrrrrrr..........rrrrrrrrrr.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566199,"updatedAt":1780548566199,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":6,"y":11,"c":")"}],"lightColor":")","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..........................................................................................................11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111............11111111...........1111111111..........1111111111.........................","door":null},"contentHash":"fnv1a:c55c0953"},{"id":"hamiyom0n6ig","name":"Lantern Reef Gate","category":"static","subtype":"ship","size":32,"width":32,"height":20,"blobId":"blob:c223e91d","pixels":".......................................................................................................................................................................................................................................................................................................rEEEEEEEEEEEEEEEEr..............rrruurrrrrrrruurrr..............rrruu........uurrr..............rrruu........uurrr.............2rrruu........uurrr#..........22222r............r#####.......2222222pppppppppppp#######.....222222222pppppppppp#########.....2222222rrrrrrrrrrrr#######.......22222..............#####..........2..................#......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566201,"updatedAt":1780548566201,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":10,"y":9,"c":"u"},{"x":21,"y":9,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".......................................................................................................................................................................................................................................................................111111111111111111..............111111111111111111..............11111........11111..............11111........11111.............111111........111111..........111111............111111.......11111111111111111111111111.....1111111111111111111111111111.....11111111111111111111111111.......11111..............11111..........1..................1......................................","door":null},"contentHash":"fnv1a:68ebe374"},{"id":"wi18jkn0n6ii","name":"Ember Salamander","category":"dynamic","subtype":"animal","size":14,"width":14,"height":10,"blobId":"blob:d78566a5","pixels":"..............................................rrr.........rrvArr.......rvAAAAvrr....rvAAEEAAAvr....rrvAAAArrr......rrAAr..rr....rr..rr......","faces":{"right":"..............................................rrr.........rrvArr.......rvAAAAvrr....rvAAEEAAAvr....rrvAAAArrr......rrAAr..rr....rr..rr......","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566202,"updatedAt":1780548566202,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":2,"y":5,"c":"E","dir":"up"}],"particleConfig":{"enabled":true,"c":"E","dir":"up"},"depthPixels":"..................111.........111111.......111111111....11111111111....1111111111......11111..11....11..11..................................","door":null},"contentHash":"fnv1a:f4828c08"},{"id":"ra2uekk0n6ij","name":"Moon Snail","category":"dynamic","subtype":"animal","size":12,"width":12,"height":10,"blobId":"blob:4c7f7268","pixels":"........................................rrrr.......r::IIr.....r::IIIIr...r::IrrIIr...r@@@rrrrr....r@@@@@@r.....rr..rr...","faces":{"right":"........................................rrrr.......r::IIr.....r::IIIIr...r::IrrIIr...r@@@rrrrr....r@@@@@@r.....rr..rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566203,"updatedAt":1780548566203,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":5,"y":4,"c":"?"}],"lightColor":"?","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................1111.......111111.....11111111...111111111...111111111....11111111.....11..11...........................","door":null},"contentHash":"fnv1a:af6fb952"},{"id":"jt8vazn0n6ij","name":"Ribbon Dragonfly","category":"dynamic","subtype":"animal","size":14,"width":14,"height":8,"blobId":"blob:64472c8c","pixels":"..............................rr....rr.....r::r..r!!r.....r::rr!!rEE......rrrrrEEr....r!!rr::r.....r!!r..r::r...","faces":{"right":"..............................rr....rr.....r::r..r!!r.....r::rr!!rEE......rrrrrEEr....r!!rr::r.....r!!r..r::r...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566203,"updatedAt":1780548566203,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":4,"y":3,"c":":","dir":"up"}],"particleConfig":{"enabled":true,"c":":","dir":"up"},"depthPixels":"................11....11.....1111..1111.....1111111111......11111111....11111111.....1111..1111.................","door":null},"contentHash":"fnv1a:47d65b2a"},{"id":"8zuio0o0n6ik","name":"Teacup Automaton","category":"dynamic","subtype":"human","size":16,"width":12,"height":16,"blobId":"blob:df5899eb","pixels":"................rrrr.......rttttr.....rttttttr....rtrrtrrr.....rttttr.....rr****rr...r**rrrr**r..r**t**t**r...r******r.....r****r.....rrr**rrr...rr..**..rr......rr.........r..r.......rr..rr...","faces":{"right":"................rrrr.......rttttr.....rttttttr....rtrrtrrr.....rttttr.....rr****rr...r**rrrr**r..r**t**t**r...r******r.....r****r.....rrr**rrr...rr..**..rr......rr.........r..r.......rr..rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566205,"updatedAt":1780548566205,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":7,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"....1111.......111111.....11111111....11111111.....111111.....11111111...1111111111..1111111111...11111111.....111111.....11111111...11..11..11......11.........1..1.......11..11...............","door":null},"contentHash":"fnv1a:90032f4a"},{"id":"sbdtcvl0n6il","name":"Sapphire Crab","category":"dynamic","subtype":"fish","size":12,"width":12,"height":8,"blobId":"blob:fbae6eb7","pixels":"..........................rr....rr...r++r..r++r...r++++++r...r++'++'++r.r++rrrr+++r..rr....rr...","faces":{"right":"..........................rr....rr...r++r..r++r...r++++++r...r++'++'++r.r++rrrr+++r..rr....rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566206,"updatedAt":1780548566206,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":2,"y":5,"c":"!","dir":"up"}],"particleConfig":{"enabled":true,"c":"!","dir":"up"},"depthPixels":"..............11....11...1111..1111...11111111...1111111111.11111111111..11....11...............","door":null},"contentHash":"fnv1a:49d22610"},{"id":"baobkva0n6im","name":"Cloud Alpaca","category":"dynamic","subtype":"animal","size":14,"width":14,"height":12,"blobId":"blob:a6a00ad5","pixels":"..............................................rrrrr.......rrkkkkkrr....rkkkkkkkkkr...rkkrkkkrkkr...rkkkkkkkkr.....rrkkkkrr.......rkkrkr........r..r.r.......rr..rrr.....","faces":{"right":"..............................................rrrrr.......rrkkkkkrr....rkkkkkkkkkr...rkkrkkkrkkr...rkkkkkkkkr.....rrkkkkrr.......rkkrkr........r..r.r.......rr..rrr.....","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566207,"updatedAt":1780548566207,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..................11111.......111111111....11111111111...11111111111...1111111111.....11111111.......111111........1..1.1.......11..111.................................","door":null},"contentHash":"fnv1a:b071257b"},{"id":"w7j87y70n6in","name":"Starling Bard","category":"dynamic","subtype":"human","size":16,"width":12,"height":16,"blobId":"blob:904d032d","pixels":"............................rrrr.......rEEEEr......rEEEEr.......rrrr.......rreerr.....reeeeeer....reeEeeer.....reeeer.......rjjr.......rrjjrr.....rr.jj.rr.......jj.........r..r.......rr..rr...","faces":{"right":"............................rrrr.......rEEEEr......rEEEEr.......rrrr.......rreerr.....reeeeeer....reeEeeer.....reeeer.......rjjr.......rrjjrr.....rr.jj.rr.......jj.........r..r.......rr..rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566208,"updatedAt":1780548566208,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":5,"y":5,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"....1111.......111111......111111.......1111.......111111.....11111111....11111111.....111111.......1111.......111111.....11.11.11.......11.........1..1.......11..11...........................","door":null},"contentHash":"fnv1a:da7ccead"},{"id":"ycxhb1a0n6io","name":"Glass Jellyfish","category":"dynamic","subtype":"fish","size":16,"width":12,"height":16,"blobId":"blob:cfa0e1a7","pixels":"................................................................rrrr.......r****r.....r******r...r**IIII**r..r********r...rr****rr.....r.**.r.....r..**..r.......**.........r..r.......r....r...","faces":{"right":"................................................................rrrr.......r****r.....r******r...r**IIII**r..r********r...rr****rr.....r.**.r.....r..**..r.......**.........r..r.......r....r...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566209,"updatedAt":1780548566209,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":5,"y":11,"c":"!","dir":"up"}],"particleConfig":{"enabled":true,"c":"!","dir":"up"},"depthPixels":"................1111.......111111.....11111111...1111111111..1111111111...11111111.....1.11.1.....1..11..1.......11.........1..1.......1....1...................................................","door":null},"contentHash":"fnv1a:b8e7ed4a"},{"id":"1aintdy0n6ip","name":"Copper Heron","category":"dynamic","subtype":"bird","size":16,"width":12,"height":16,"blobId":"blob:035186d7","pixels":".............................rr.........rIIr.........rrr..........rr..........rr........rrwwr......rwwwwwrr...rwwwwwwwr....rrwwwwr.......rrrr.........r...........r..........r.r........rr.rr...","faces":{"right":".............................rr.........rIIr.........rrr..........rr..........rr........rrwwr......rwwwwwrr...rwwwwwwwr....rrwwwwr.......rrrr.........r...........r..........r.r........rr.rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566209,"updatedAt":1780548566209,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".....11.........1111.........111..........11..........11........11111......11111111...111111111....1111111.......1111.........1...........1..........1.1........11.11...........................","door":null},"contentHash":"fnv1a:239c733c"},{"id":"htpwit60n6iq","name":"Paper Fox Kite","category":"dynamic","subtype":"animal","size":14,"width":14,"height":12,"blobId":"blob:6a1737ac","pixels":"...............................................rrr..........rkkEr........rkkEEEr......rkkEEEEr.....rkkEErrEr......rrEEEEEr........rEEErr.......rr..rr.......r....r......","faces":{"right":"...............................................rrr..........rkkEr........rkkEEEr......rkkEEEEr.....rkkEErrEr......rrEEEEEr........rEEErr.......rr..rr.......r....r......","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566210,"updatedAt":1780548566210,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...................111..........11111........1111111......11111111.....111111111......11111111........111111.......11..11.......1....1..................................","door":null},"contentHash":"fnv1a:7b7b8810"},{"id":"9tei0ni0n6ir","name":"Prism Seahorse","category":"dynamic","subtype":"fish","size":14,"width":10,"height":14,"blobId":"blob:6e8886d8","pixels":"........................rr.......r##r.....r#@##r....r####r.....r##r.......r#r.......r#r......r##r.....r##r......r##rr......rr##r.......rr...","faces":{"right":"........................rr.......r##r.....r#@##r....r####r.....r##r.......r#r.......r#r......r##r.....r##r......r##rr......rr##r.......rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566211,"updatedAt":1780548566211,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":5,"y":5,"c":"@"}],"lightColor":"@","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..............11.......1111.....111111....111111.....1111.......111.......111......1111.....1111......11111......11111.......11.............","door":null},"contentHash":"fnv1a:b18416f0"},{"id":"hpv8n7r0n6ir","name":"Orchard Sprite","category":"dynamic","subtype":"animal","size":12,"width":10,"height":12,"blobId":"blob:9242e4b0","pixels":".................................rrrr.....riiiir...riiviiir..riiiiiir...rroorr.....roor.....rroorr....r....r...rr....rr.","faces":{"right":".................................rrrr.....riiiir...riiviiir..riiiiiir...rroorr.....roor.....rroorr....r....r...rr....rr.","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566212,"updatedAt":1780548566212,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":5,"y":3,"c":"E","dir":"up"}],"particleConfig":{"enabled":true,"c":"E","dir":"up"},"depthPixels":".............1111.....111111...11111111..11111111...111111.....1111.....111111....1....1...11....11.....................","door":null},"contentHash":"fnv1a:507dbd81"},{"id":"sk53gby0n6is","name":"Map Turtle","category":"dynamic","subtype":"animal","size":12,"width":12,"height":10,"blobId":"blob:9f74869c","pixels":"........................................rrrr......rrpppprr...rppEppEpr...rpppppppr....rrpppprrr....rwwwwEr....rr....rr..","faces":{"right":"........................................rrrr......rrpppprr...rppEppEpr...rpppppppr....rrpppprrr....rwwwwEr....rr....rr..","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566212,"updatedAt":1780548566212,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................1111......11111111...111111111...111111111....111111111....1111111....11....11..........................","door":null},"contentHash":"fnv1a:ec49a791"},{"id":"vpvvg6s0n6it","name":"Tea Sparrow","category":"dynamic","subtype":"bird","size":8,"width":8,"height":8,"blobId":"blob:0afe1789","pixels":"..................rrr....rvvrr..rvvvvvr..rjjvr....rrr.....r.r...","faces":{"right":"..................rrr....rvvrr..rvvvvvr..rjjvr....rrr.....r.r...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566213,"updatedAt":1780548566213,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..........111....11111..1111111..11111....111.....1.1...........","door":null},"contentHash":"fnv1a:f0188671"},{"id":"omunqfu0n6it","name":"Bubble Diver","category":"dynamic","subtype":"human","size":16,"width":12,"height":16,"blobId":"blob:bba79985","pixels":"............................rrrr.......r''''r.....r'EEEE'r....r'EEEE'r.....r''''r.....rr++++rr...r++++++++r..r++E++E++r...r++++++r.....r++++r.....rr.++.rr.......++.........r..r.......rr..rr...","faces":{"right":"............................rrrr.......r''''r.....r'EEEE'r....r'EEEE'r.....r''''r.....rr++++rr...r++++++++r..r++E++E++r...r++++++r.....r++++r.....rr.++.rr.......++.........r..r.......rr..rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566214,"updatedAt":1780548566214,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":6,"y":3,"c":"'"}],"lightColor":"'","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"....1111.......111111.....11111111....11111111.....111111.....11111111...1111111111..1111111111...11111111.....111111.....11.11.11.......11.........1..1.......11..11...........................","door":null},"contentHash":"fnv1a:fff756cf"},{"id":"5pma1ea0n6iu","name":"Mist Eel","category":"dynamic","subtype":"fish","size":14,"width":14,"height":6,"blobId":"blob:94da51f5","pixels":"...............................rrr.........rr!!!rrrrr...r!!!!!!!!!!rr..rr!!rrrrrr...","faces":{"right":"...............................rrr.........rr!!!rrrrr...r!!!!!!!!!!rr..rr!!rrrrrr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566215,"updatedAt":1780548566215,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":2,"y":3,"c":"'","dir":"up"}],"particleConfig":{"enabled":true,"c":"'","dir":"up"},"depthPixels":".................111.........1111111111...1111111111111..1111111111.................","door":null},"contentHash":"fnv1a:d46bdfd6"},{"id":"z5e8rhd0n6iv","name":"Ruby Beetle","category":"dynamic","subtype":"animal","size":10,"width":10,"height":8,"blobId":"blob:cfd31a3a","pixels":"......................rrrr.....rAAAAr...rAEAAEAr...rAAAAr.....rrrr.....r....r...","faces":{"right":"......................rrrr.....rAAAAr...rAEAAEAr...rAAAAr.....rrrr.....r....r...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566215,"updatedAt":1780548566215,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"............1111.....111111...11111111...111111.....1111.....1....1.............","door":null},"contentHash":"fnv1a:4624614c"},{"id":"4au1hl00n6iw","name":"Snowy Tanuki","category":"dynamic","subtype":"animal","size":12,"width":12,"height":12,"blobId":"blob:147b9538","pixels":"........................................rrrr......rrkkkkrr...rkkpkkpkr...rkkkkkkkr....rrkkkkr......rpppr......rrkkkrr.....r.r.r.r....rr.r.rr....","faces":{"right":"........................................rrrr......rrkkkkrr...rkkpkkpkr...rkkkkkkkr....rrkkkkr......rpppr......rrkkkrr.....r.r.r.r....rr.r.rr....","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566216,"updatedAt":1780548566216,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................1111......11111111...111111111...111111111....1111111......11111......1111111.....1.1.1.1....11.1.11............................","door":null},"contentHash":"fnv1a:3eae965a"},{"id":"x1zmg8s0n6ix","name":"Lighthouse Keeper","category":"dynamic","subtype":"human","size":16,"width":12,"height":16,"blobId":"blob:6fd0a1e3","pixels":"............................rrrr.......rEEEEr.....rErrEEEr....rEEEEEEr.....rrrrrr.....rrwwwwrr...rwwwwwwwwr..rwwEwwEwwr...rwwwwwwr.....rwwwwr.....rr.11.rr.......11.........r..r.......rr..rr...","faces":{"right":"............................rrrr.......rEEEEr.....rErrEEEr....rEEEEEEr.....rrrrrr.....rrwwwwrr...rwwwwwwwwr..rwwEwwEwwr...rwwwwwwr.....rwwwwr.....rr.11.rr.......11.........r..r.......rr..rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566217,"updatedAt":1780548566217,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":7,"c":"I"}],"lightColor":"I","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"....1111.......111111.....11111111....11111111.....111111.....11111111...1111111111..1111111111...11111111.....111111.....11.11.11.......11.........1..1.......11..11...........................","door":null},"contentHash":"fnv1a:b166b726"},{"id":"swi2z4b0n6iz","name":"Midnight Ramen Cart","category":"static","subtype":"building","size":28,"width":28,"height":20,"blobId":"blob:52d1b121","pixels":"...........................................................................................................................................................................r~uu~~uu~~uu~~uu~~uu~r......r~uu~~uu~~uu~~uu~~uu~r......rrrrrrrrrrrrrrrrrrrrrr~......rxxxxxxxxxxxxxxxxxxr.u......rxxDDDDxxxx#####xxxrrr......rxxDDDDxxxx#####xxxrrr......rxxDDDDxxxx#####xxxrrr......rxxxxxxr44rxxxxxxxxrrr......rxxxxxxr44rxxxxxxxxrrr......rrrrrrrr44rrrrrrrrrrrr......rrrrrrrrrrrrrrrrrrrrrr......rrrrrrrrrrrrrrrrrrrr..........rr#rr.......rr#rr............rrr.........rrr......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566221,"updatedAt":1780548566221,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":10,"c":"u"},{"x":18,"y":10,"c":"u"},{"x":13,"y":15,"c":"u"}],"lightColor":"u","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...........................................................................................................................................................................1111111111111111111111......1111111111111111111111......11111111111111111111111......11111111111111111111.1......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......1111111111111111111111......11111111111111111111..........11111.......11111............111.........111......","door":{"x":13,"y":17}},"contentHash":"fnv1a:e21231b2"},{"id":"1ysaaxb0n6j3","name":"Quartz Clocktower","category":"static","subtype":"building","size":34,"width":22,"height":34,"blobId":"blob:974b7e88","pixels":"...................................................................................................k....................o.o..................rkkr.................rkkrkr................rrr2rr...............rrkk2kkr..............rrkk2kkr.............rrkkk22kkr............rkrkkkk22r...........rkkrkkkkkrkr..........rkkkrrkrrkkr............rVVVrVVr..............rVVVVVVr..............rVVVVVVr..............rViiiiVr..............rViiiiVr..............rViiiiVr..............rVVVVVVr..............rVVVVVVr..............rViiiiVr..............rViiiiVr..............rViiiiVr..............rVVVVVVr..............rVroorVr..............rVroorVr..............rVroorVr..............rVroorVr..............rVroorVr.............222roor222............2222222222......","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566225,"updatedAt":1780548566225,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":10,"y":8,"c":"'"},{"x":11,"y":8,"c":"'"}],"lightColor":"'","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".......................................................1....................1.1..................1111.................111111................111111...............11111111..............11111111.............1111111111............1111111111...........111111111111..........111111111111............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111..............11111111.............1111111111............1111111111..................................................","door":{"x":11,"y":31}},"contentHash":"fnv1a:f88fbc76"},{"id":"nvzdcd20n6j7","name":"Lotus Tea Pavilion","category":"static","subtype":"building","size":30,"width":30,"height":24,"blobId":"blob:1c5e74d0","pixels":"............................................................................................................................................................................................o.....o.....o...............oozooooozooooozoo...............ooozooooozooo.................rppopppppopppr................rrrrrrrrrrrrrr............rzzzzzzzzzzzzzzzzzzzzr........rzzzzzzzzzzzzzzzzzzzzr........rrrrrrrrrrrrrrrrrrrrrr.........rrrrrrrrrrrrrrrrrrrr..........rDDDiiiiDDDDiiiiDDDr..........rDDDiiiiDDDDiiiiDDDr..........rDDDiiiippppiiiiDDDr..........rDDDDDDDppppDDDDDDDr........**rDDDDDDDppppDDDDDDDr***....***rrrrrrrrpppprrrrrrrr****....*************************........*******************....................*..............","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566229,"updatedAt":1780548566229,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":9,"y":14,"c":"{"},{"x":20,"y":14,"c":"{"},{"x":15,"y":18,"c":"{"}],"lightColor":"{","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"............................................................................................................................................................................................1.....1.....1...............11111111111111111...............1111111111111.................11111111111111................11111111111111............1111111111111111111111........1111111111111111111111........1111111111111111111111.........11111111111111111111..........11111111111111111111..........11111111111111111111..........11111111111111111111..........11111111111111111111........1111111111111111111111111....111111111111111111111111111....1111111111111111111111111........1111111111111111111....................1..............","door":{"x":15,"y":21}},"contentHash":"fnv1a:9274aacb"},{"id":"nhuplyj0n6jb","name":"Meteorite Garden","category":"static","subtype":"nature","size":26,"width":26,"height":20,"blobId":"blob:ac647e93","pixels":".........................................................................................................................................................................r......................rrr2rrr..................r2[22!22r................r2222!!!22r...............r222!!!!!2r..............r22222!!!222r..............r!2222!22!r............!..r1222!2221r..!.........1...122212221...1........1...1.rr12rr1...1.........1...1...1r..1...1...........rrrrrrrrrrrrrr........oooooooooooooooooooooo....oooooooooooooooooooooo..","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566232,"updatedAt":1780548566232,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":13,"y":8,"c":"!"}],"lightColor":"!","hasParticles":true,"particlePixels":[{"x":13,"y":7,"c":"'","dir":"up"},{"x":18,"y":9,"c":"!","dir":"up"}],"particleConfig":{"enabled":true,"c":"'","dir":"up"},"depthPixels":"...............................................................................................................................................1......................1111111..................111111111................11111111111...............11111111111..............1111111111111..............11111111111............1..11111111111..1.........1...111111111...1........1...1.1111111...1.........1...1...11..1...1...........11111111111111........1111111111111111111111....1111111111111111111111............................","door":null},"contentHash":"fnv1a:8e460321"},{"id":"k7tysl60n6je","name":"Azure Glasshouse","category":"static","subtype":"building","size":28,"width":28,"height":26,"blobId":"blob:ed116ff1","pixels":"............................................................................................................................................................................................................................................r''r.......................r''''r...................1.r'1'''1r..1...............1r''1'''1'r.1...............1'''1'''1''r1..............r1'''1'''1'''1.............r'1'''1'''1'''1r...........r''1'''1'''1'''1'r..........r##1###1###1###1#r..........r##1###1###1###1#r..........r##1###1###1###1#r..........r##1###1###1#ooo#r..........r#ooo##1###1#ooo#r..........r#ooo##ruur1#ooo#r..........r#ooo##ruur1#ooo#r..........r##1###ruur1###1#r..........r##1###ruur1###1#r..........rrr1rrrruur1rrr1rr.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566236,"updatedAt":1780548566236,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":15,"c":"!"},{"x":14,"y":12,"c":"!"},{"x":20,"y":15,"c":"!"}],"lightColor":"!","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"........................................................................................................................................................1111.......................111111...................1.11111111..1...............11111111111.1...............1111111111111..............11111111111111.............1111111111111111...........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111.........................................................................................","door":{"x":14,"y":23}},"contentHash":"fnv1a:fd39cee2"},{"id":"4mmqdjs0n6jh","name":"Foxfire Torii","category":"static","subtype":"nature","size":24,"width":24,"height":24,"blobId":"blob:eb78bd9d","pixels":"............................................................................................................................................................................rAAAAAAAAAAAAAAr........rAAAAAAAAAAAAAAr........rrrrrrrrrrrrrrrr......rjjjjjjjjjjjjjjjjjjr....rrrrrrrrrrrrrrrrrrrr........rAr......rAr............rAr......rAr............EAr......rAE...........EEEr......rEEE..........EuEr......rEuE.........EEEEE......EEEEE.........EEEr......rEEE..........EEEr......rEEE...........EAr......rAE............rAr......rAr...........jjjjj....jjjjj..........jjjjj....jjjjj.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566238,"updatedAt":1780548566238,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":6,"y":14,"c":"E"},{"x":17,"y":14,"c":"E"},{"x":12,"y":8,"c":"E"}],"lightColor":"E","hasParticles":true,"particlePixels":[{"x":6,"y":13,"c":"E","dir":"up"},{"x":17,"y":13,"c":"u","dir":"up"}],"particleConfig":{"enabled":true,"c":"E","dir":"up"},"depthPixels":"............................................................................................................................1111111111111111........1111111111111111........1111111111111111......11111111111111111111....11111111111111111111........111......111............111......111............111......111...........1111......1111..........1111......1111.........11111......11111.........1111......1111..........1111......1111...........111......111............111......111...........11111....11111..........11111....11111.....................................................","door":null},"contentHash":"fnv1a:a654664f"},{"id":"wuqr3r80n6jk","name":"Pearl Cloud Altar","category":"static","subtype":"nature","size":24,"width":24,"height":22,"blobId":"blob:ab82452a","pixels":"....................................................................................................................................................................................r.....................rrDrr..................rDDDD;r.................rD'DDDr................rDDDDDDDr................rDDDDDr'''.............'rDDDDDr'''''........''333rrDrr333'''.......''333333r333333'''.....''333333333333333'.......3333r''''''r33333........333rrrrrrrr3333.........r333333333333r..........r333333333333r..........rrrrrrrrrrrrrr.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566241,"updatedAt":1780548566241,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":12,"y":9,"c":"k"},{"x":8,"y":15,"c":"k"},{"x":16,"y":15,"c":"k"}],"lightColor":"k","hasParticles":true,"particlePixels":[{"x":12,"y":8,"c":"k","dir":"up"}],"particleConfig":{"enabled":true,"c":"k","dir":"up"},"depthPixels":"....................................................................................................................................1.....................11111..................1111111.................1111111................111111111................1111111111.............1111111111111........1111111111111111.......111111111111111111.....111111111111111111.......11111111111111111........111111111111111.........11111111111111..........11111111111111..........11111111111111.....................................................","door":null},"contentHash":"fnv1a:5d6a41b6"},{"id":"a0otca20n6jn","name":"Clockwork Conservatory","category":"static","subtype":"building","size":28,"width":28,"height":28,"blobId":"blob:deee1a0e","pixels":"..............................................................................................................................................................................................................................................r.......................rrrr2rrrr..................rrrrr2rrrrr...............rrrr!!!2!!!rrrr.............rr2!!!!2!!!!2rr............rr!!2!!!r!!!2!!rr...........rr!!!2rrvrr2!!!rr...........rr!!!!rvvvr!!!!rr..........r!!!!!rvvivvr!!!!rr.........r!22222rvvvr22222r..........r!!!!!!rrvrr!!!!!r..........r!!!!!!2!r!2!!!!!r..........r!!!!!2!!2!!2!!!!r..........r!!!!2!!!2!!!2!!!r..........rTTT2rrrr2rrrr2TTr..........rTTTTrrrr2rrrrTTTr..........rTTTTTTrvvrTTTTTTr..........rTTTTTTrvvrTTTTTTr..........rTTTTTTrvvrTTTTTTr..........rrrrrrrrvvrrrrrrrr.....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566245,"updatedAt":1780548566245,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":16,"c":"i"},{"x":14,"y":11,"c":"i"},{"x":20,"y":16,"c":"i"}],"lightColor":"i","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..........................................................................................................................................................1.......................111111111..................11111111111...............111111111111111.............111111111111111............11111111111111111...........11111111111111111...........11111111111111111..........1111111111111111111.........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111..........111111111111111111.........................................................................................","door":{"x":14,"y":25}},"contentHash":"fnv1a:a968fbf2"},{"id":"f7y5i5g0n6jq","name":"Inkcap Mouse","category":"dynamic","subtype":"animal","size":12,"width":12,"height":10,"blobId":"blob:020521fc","pixels":"............................@.........@@@@@......@rrrrr@r.....rrrrrrrr....r2@2rrr{r.2222222rrr....222222r.......r2.r....","faces":{"right":"............................@.........@@@@@......@rrrrr@r.....rrrrrrrr....r2@2rrr{r.2222222rrr....222222r.......r2.r....","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566246,"updatedAt":1780548566246,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":true,"particlePixels":[{"x":4,"y":2,"c":"@","dir":"up"}],"particleConfig":{"enabled":true,"c":"@","dir":"up"},"depthPixels":"................1.........11111......11111111.....11111111....111111111.1111111111....1111111.......11.1................","door":null},"contentHash":"fnv1a:dc8f8729"},{"id":"6vzm44u0n6jq","name":"Comet Finch","category":"dynamic","subtype":"bird","size":12,"width":12,"height":10,"blobId":"blob:0d922c68","pixels":"................................r...I....r.rrr...I.rr*rrrDE...I****rrr....Err*rrr...EE..rr.r........r..r........r.......","faces":{"right":"................................r...I....r.rrr...I.rr*rrrDE...I****rrr....Err*rrr...EE..rr.r........r..r........r.......","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566247,"updatedAt":1780548566247,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":8,"y":4,"c":"I"}],"lightColor":"I","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................................1...1....1.111...1.11111111...11111111....1111111...11..11.1........1..1........1.......","door":null},"contentHash":"fnv1a:2fdc4acd"},{"id":"kdxvdqs0n6jr","name":"Lotus Kappa","category":"dynamic","subtype":"animal","size":14,"width":12,"height":14,"blobId":"blob:b84a8f50","pixels":"..............................|........|||||||.....rpp|ppr.....rpppppr....rppppprpr....rpppppu.....rpppppr......rYYYr......SrYYYrS......rYYYr.......rYYYr.......r...r...","faces":{"right":"..............................|........|||||||.....rpp|ppr.....rpppppr....rppppprpr....rpppppu.....rpppppr......rYYYr......SrYYYrS......rYYYr.......rYYYr.......r...r...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566247,"updatedAt":1780548566247,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":6,"y":3,"c":"S"}],"lightColor":"S","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"..................1........1111111.....1111111.....1111111....111111111....1111111.....1111111......11111......1111111......11111.......11111.......1...1...............","door":null},"contentHash":"fnv1a:2a4c8e40"},{"id":"fh5fkb90n6js","name":"Lantern Manta","category":"dynamic","subtype":"fish","size":14,"width":14,"height":10,"blobId":"blob:e03f2d6f","pixels":"..................................r.......&..rrrWrrr.....&rWWWWW!Wr&&&.r&WWWWWW&&r....&WWWWWW&ru...&.rrrWrrr&&..&....!r!....&&....!..!......","faces":{"right":"..................................r.......&..rrrWrrr.....&rWWWWW!Wr&&&.r&WWWWWW&&r....&WWWWWW&ru...&.rrrWrrr&&..&....!r!....&&....!..!......","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566248,"updatedAt":1780548566248,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":7,"y":4,"c":"!"},{"x":10,"y":6,"c":"!"}],"lightColor":"!","hasParticles":true,"particlePixels":[{"x":2,"y":5,"c":"!","dir":"up"}],"particleConfig":{"enabled":true,"c":"!","dir":"up"},"depthPixels":"..................................1.......1..1111111.....1111111111111.11111111111....1111111111...1.111111111..1....111....11....1..1......","door":null},"contentHash":"fnv1a:7908a30f"},{"id":"w8qlpfz0n6jt","name":"Pocket Astronaut","category":"dynamic","subtype":"human","size":16,"width":12,"height":16,"blobId":"blob:377269dc","pixels":"..................r.........rrlrr......rlllllr.....rl****r....rll***ulr....rlllllr.....rlllllr......rrlrr......rllllr......2lhl*r2....2rllllr.22.2.rllllr......rllllr.......rr.rr.......rr.rr...","faces":{"right":"..................r.........rrlrr......rlllllr.....rl****r....rll***ulr....rlllllr.....rlllllr......rrlrr......rllllr......2lhl*r2....2rllllr.22.2.rllllr......rllllr.......rr.rr.......rr.rr...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566249,"updatedAt":1780548566249,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":6,"y":4,"c":"'"}],"lightColor":"'","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"......1.........11111......1111111.....1111111....111111111....1111111.....1111111......11111......111111......1111111....1111111.11.1.111111......111111.......11.11.......11.11...............","door":null},"contentHash":"fnv1a:a489dccd"},{"id":"1x04kig0n6ju","name":"Mossy Capybara","category":"dynamic","subtype":"animal","size":14,"width":14,"height":10,"blobId":"blob:7302add1","pixels":".............................................OOOOOO.r......OOOOOOrwrr...rwwwwrwwwrwr.jwwwwwwrrwrD.j.rwwwwwwwr......rrrwrrr..........r.......","faces":{"right":".............................................OOOOOO.r......OOOOOOrwrr...rwwwwrwwwrwr.jwwwwwwrrwrD.j.rwwwwwwwr......rrrwrrr..........r.......","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566250,"updatedAt":1780548566250,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":".............................................111111.1......1111111111...111111111111.111111111111.1.111111111......1111111..........1.......","door":null},"contentHash":"fnv1a:4f037c3c"},{"id":"0e82z400n6jv","name":"Opal Octopus","category":"dynamic","subtype":"fish","size":12,"width":12,"height":12,"blobId":"blob:bc79bf69","pixels":"..................r.........rr|rr......r|||||r.....r|!|||r....r|||||r|r....r|||||y.....r|||||r.....2r2|222.....2.2r2.2....2.2.2.22....2.2.2.2.2.","faces":{"right":"..................r.........rr|rr......r|||||r.....r|!|||r....r|||||r|r....r|||||y.....r|||||r.....2r2|222.....2.2r2.2....2.2.2.22....2.2.2.2.2.","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566252,"updatedAt":1780548566252,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":6,"y":4,"c":"y"}],"lightColor":"y","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"......1.........11111......1111111.....1111111....111111111....1111111.....1111111.....1111111.....1.111.1....1.1.1.11....1.1.1.1.1.............","door":null},"contentHash":"fnv1a:e4eb2153"},{"id":"ftua1wf0n6jw","name":"Tiny Oracle","category":"dynamic","subtype":"human","size":16,"width":10,"height":16,"blobId":"blob:1bf6f487","pixels":".........................r.......rrirr.....riiir....riiirir....riiir.....r:::r.....r[[[r.:::.r[[[r.:..:r[I[r:....r[[[r.....r[[[r.....r[[[r.....r[[[r......r.r...","faces":{"right":".........................r.......rrirr.....riiir....riiirir....riiir.....r:::r.....r[[[r.:::.r[[[r.:..:r[I[r:....r[[[r.....r[[[r.....r[[[r.....r[[[r......r.r...","left":"mirror"},"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566253,"updatedAt":1780548566253,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":5,"y":5,"c":":"}],"lightColor":":","hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"...............1.......11111.....11111....1111111....11111.....11111.....11111.111.11111.1..1111111....11111.....11111.....11111.....11111......1.1.............","door":null},"contentHash":"fnv1a:048e00cd"},{"id":"ny269lx0n6jz","name":"Amber Apiary","category":"static","subtype":"building","size":24,"width":24,"height":24,"blobId":"blob:40911b03","pixels":"..............................................................................................................................I.......I.......I.....ur....I.ur....I.ur..........ur......ur...................................rIIIIIIIIIIIIr..........rIIIIIIIIIIIIr..........rrrrrrrrrrrrrr...........rvvvvvvvvvvr............ruuuuuuuuuur............rvvvvvvvvvvr............rvvvvvvvvvvr............ruujjjjjjuur............rvvjrrrrjvvr............rvvjrrrrjvvr............ruujrrrrjuur............rvvjjjjjjvvr............rvvvvvvvvvvr.........pppppppppppppppppp......pppppppppppppppppp...","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566256,"updatedAt":1780548566256,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":true,"lightPixels":[{"x":12,"y":10,"c":"E"},{"x":7,"y":15,"c":"E"},{"x":17,"y":15,"c":"E"}],"lightColor":"E","hasParticles":true,"particlePixels":[{"x":12,"y":6,"c":"u","dir":"up"}],"particleConfig":{"enabled":true,"c":"u","dir":"up"},"depthPixels":"..............................................................................1.......1.......1.....11....1.11....1.11..........11......11...................................11111111111111..........11111111111111..........11111111111111...........111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111............111111111111.........111111111111111111......111111111111111111...................................................","door":null},"contentHash":"fnv1a:e335acf6"},{"id":"9o027ha0n6k2","name":"Glass Moon Dock","category":"static","subtype":"ship","size":32,"width":32,"height":16,"blobId":"blob:2f761d6a","pixels":"................................................................................................................................................................................'...............................r.*.....................I...rrrr*****...I...............***r'''**********.............rrrrr'''''*****'rrrrr............vv..r''''''*''r.vv..............vv..vrrrr'rrrr..vv..............vv..vv..vr..vv..vv...........rwwvvwwvvwwvvwwvvwwvvwwr........rwwvvwwvvwwvvwwvvwwvvwwr........rrrrrrrrrrrrrrrrrrrrrrrr....","faces":null,"parentAssetId":null,"originalAssetId":null,"createdAt":1780548566259,"updatedAt":1780548566259,"author":"Pixel Island","ownerAccountId":"island-team","version":1,"meta":{"hasLight":false,"lightPixels":[],"lightColor":null,"hasParticles":false,"particlePixels":[],"particleConfig":{"enabled":false,"c":"k","dir":"up"},"depthPixels":"................................................................................................................1...............................1.1.....................1...111111111...1...............11111111111111111.............111111111111111111111............11..11111111111.11..............11..1111111111..11..............11..11..11..11..11...........111111111111111111111111........111111111111111111111111........111111111111111111111111....................................................................","door":null},"contentHash":"fnv1a:8bcfaf02"},{"id":"sr2m7zu0n6k7","name":"Bluebell Chapel","category":"static","subtype":"building","size":30,"width":22,"height":30,"blobId":"blob:53fb3b62","pixels":"...................................................................................................r.....................I...................rrrrr...................r.....................r.....................r....................rr...................r< dirname(__DIR__) . DIRECTORY_SEPARATOR . '_data' . DIRECTORY_SEPARATOR . 'pixel_island.sqlite', + 'db_path' => getenv('PIXEL_ISLAND_DB_PATH') ?: dirname(__DIR__) . DIRECTORY_SEPARATOR . '_data' . DIRECTORY_SEPARATOR . 'pixel_island.sqlite', 'world_width' => 144, 'world_height' => 112, 'max_assets_per_account' => 220, diff --git a/api/filedb.php b/api/filedb.php index 3e5ca9e..24c258a 100644 --- a/api/filedb.php +++ b/api/filedb.php @@ -8,7 +8,10 @@ function fb_respond(array $payload, int $status = 200): void { echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } -function fb_fail(string $message, int $status = 400, array $extra = []): void { fb_respond(['ok' => false, 'error' => $message] + $extra, $status); } +function fb_fail(string $message, int $status = 400, array $extra = []): void { + if (!empty($GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT'])) throw new RuntimeException($message, $status); + fb_respond(['ok' => false, 'error' => $message] + $extra, $status); +} function fb_now(): int { return (int) floor(microtime(true) * 1000); } function fb_clean_text($value, string $fallback = '', int $max = 80): string { $text = trim((string)($value ?? '')); @@ -33,6 +36,37 @@ function fb_request_json(int $maxBytes): array { function fb_default_db(): array { return ['schema'=>1,'accounts'=>[],'assets'=>[],'objects'=>[],'votes'=>[],'reports'=>[],'events'=>[],'commands'=>[],'nextEventId'=>1]; } + +function fb_seed_path(array $config): string { + return dirname($config['db_path']) . DIRECTORY_SEPARATOR . 'default_gallery_seed.json'; +} +function fb_seed_defaults(array &$db, array $config): void { + $path = fb_seed_path($config); + if (!is_file($path)) return; + $seed = json_decode((string)file_get_contents($path), true); + if (!is_array($seed)) return; + $now = fb_now(); + if (!isset($db['accounts']['island-team'])) $db['accounts']['island-team'] = ['id'=>'island-team','name'=>'Pixel Island','password_hash'=>password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT),'created_at'=>$now,'updated_at'=>$now,'disabled_at'=>null]; + foreach (is_array($seed['assets'] ?? null) ? $seed['assets'] : [] as $asset) { + if (!is_array($asset)) continue; + $id = fb_clean_id($asset['id'] ?? ''); + if ($id === '' || isset($db['assets'][$id])) continue; + $asset['ownerAccountId'] = 'island-team'; + $asset['author'] = $asset['author'] ?? 'Pixel Island'; + $db['assets'][$id]=['id'=>$id,'owner_account_id'=>'island-team','author_name'=>fb_clean_text($asset['author'] ?? 'Pixel Island','Pixel Island',32),'json'=>$asset,'version'=>max(1,(int)($asset['version'] ?? 1)),'created_at'=>(int)($asset['createdAt'] ?? $now),'updated_at'=>(int)($asset['updatedAt'] ?? $now),'deleted_at'=>null,'deleted_by'=>null,'content_hash'=>$asset['contentHash'] ?? null]; + } + $insertObject = function(array $object, string $kind) use (&$db, $now): void { + $id = fb_clean_id($object['id'] ?? ''); + $assetId = fb_clean_id($object['assetId'] ?? ''); + if ($id === '' || $assetId === '' || isset($db['objects'][$id]) || !isset($db['assets'][$assetId]) || !empty($db['assets'][$assetId]['deleted_at'])) return; + $object['ownerAccountId']='island-team'; + $object['status']='active'; + $object['publishedAt']=(int)($object['publishedAt'] ?? $now); + $db['objects'][$id]=['id'=>$id,'kind'=>$kind,'asset_id'=>$assetId,'owner_account_id'=>'island-team','json'=>$object,'version'=>max(1,(int)($object['version'] ?? 1)),'published_at'=>(int)$object['publishedAt'],'updated_at'=>$now,'deleted_at'=>null,'deleted_by'=>null,'moderation_status'=>'active']; + }; + foreach (is_array($seed['placed'] ?? null) ? $seed['placed'] : [] as $object) if (is_array($object)) $insertObject($object, 'static'); + foreach (is_array($seed['dynamicSummons'] ?? null) ? $seed['dynamicSummons'] : [] as $object) if (is_array($object)) $insertObject($object, 'dynamic'); +} function fb_db_path(array $config): string { return dirname($config['db_path']) . DIRECTORY_SEPARATOR . 'pixel_island_filedb.json'; } @@ -121,10 +155,17 @@ function fb_quota(array $db, array $actor, array $config): bool { foreach ($db['objects'] as $o) if (($o['owner_account_id'] ?? '')===$actor['id'] && empty($o['deleted_at']) && (int)($o['published_at'] ?? 0) >= $since) $used++; return $used < $limit; } +function fb_command_result(array &$db, string $cmdId, string $actorId, int $createdAt, array $result): void { + $db['commands'][$cmdId]=['id'=>$cmdId,'actor_account_id'=>$actorId,'created_at'=>$createdAt,'applied_at'=>fb_now(),'result'=>$result]; +} function fb_process(array &$db, array $command, array $actor, array $config): array { $cmdId=fb_clean_id($command['id'] ?? ''); $type=(string)($command['type'] ?? ''); if ($cmdId==='' || $type==='') return ['ok'=>false,'id'=>$cmdId,'error'=>'invalid_command']; - if (isset($db['commands'][$cmdId])) return ['ok'=>true,'id'=>$cmdId,'duplicate'=>true]; + if (isset($db['commands'][$cmdId])) { + $result = is_array($db['commands'][$cmdId]['result'] ?? null) ? $db['commands'][$cmdId]['result'] : ['ok'=>true,'id'=>$cmdId]; + $result['duplicate'] = true; + return $result; + } $now=fb_now(); if ($type==='asset.create') { $asset=fb_normalize_asset(is_array($command['asset'] ?? null)?$command['asset']:[], $actor); @@ -136,11 +177,30 @@ function fb_process(array &$db, array $command, array $actor, array $config): ar $db['assets'][$assetId]['deleted_at']=$now; $db['assets'][$assetId]['deleted_by']=$actor['id']; $db['assets'][$assetId]['updated_at']=$now; foreach ($db['objects'] as &$o) if (($o['asset_id'] ?? '')===$assetId && empty($o['deleted_at'])) { $o['deleted_at']=$now; $o['deleted_by']=$actor['id']; $o['updated_at']=$now; } unset($o); fb_event($db,'asset.delete',['type'=>'asset.delete','assetId'=>$assetId,'actorAccountId'=>$actor['id']]); + } elseif ($type==='publish.asset_object') { + $asset=fb_normalize_asset(is_array($command['asset'] ?? null)?$command['asset']:[], $actor); + $kind=(($command['kind'] ?? $asset['category'] ?? '')==='dynamic')?'dynamic':'static'; + $object=fb_normalize_object(is_array($command['object'] ?? null)?$command['object']:[], $kind, $actor, $config); + if (($object['assetId'] ?? '') !== ($asset['id'] ?? '')) fb_fail('publish_asset_mismatch',400); + if (isset($db['assets'][$asset['id']]) && empty($db['assets'][$asset['id']]['deleted_at']) && ($db['assets'][$asset['id']]['owner_account_id'] ?? '') !== $actor['id']) fb_fail('asset_id_already_owned',403); + if (!fb_quota($db,$actor,$config)) fb_fail('publish_limit_reached',429); + $active=0; foreach($db['objects'] as $o) if (empty($o['deleted_at']) && ($o['moderation_status'] ?? 'active')==='active') $active++; + if ($active >= (int)$config['max_world_objects']) fb_fail('world_object_limit_reached',409); + $db['assets'][$asset['id']]=['id'=>$asset['id'],'owner_account_id'=>$actor['id'],'author_name'=>$asset['author'],'json'=>$asset,'version'=>$asset['version'],'created_at'=>$asset['createdAt'],'updated_at'=>$asset['updatedAt'],'deleted_at'=>null,'deleted_by'=>null,'content_hash'=>$asset['contentHash'] ?? null]; + $object['publishedAt']=$now; + if ($kind==='dynamic') $object['createdAt']=$now; else $object['placedAt']=$now; + $db['objects'][$object['id']]=['id'=>$object['id'],'kind'=>$kind,'asset_id'=>$object['assetId'],'owner_account_id'=>$actor['id'],'json'=>$object,'version'=>$object['version'],'published_at'=>$now,'updated_at'=>$now,'deleted_at'=>null,'deleted_by'=>null,'moderation_status'=>'active']; + fb_event($db,'asset.upsert',['type'=>'asset.upsert','assetId'=>$asset['id'],'actorAccountId'=>$actor['id']]); + fb_event($db,'object.upsert',['type'=>'object.upsert','kind'=>$kind,'objectId'=>$object['id'],'actorAccountId'=>$actor['id']]); + $result=['ok'=>true,'id'=>$cmdId,'type'=>$type,'assetId'=>$asset['id'],'objectId'=>$object['id'],'kind'=>$kind,'publishedAt'=>$now,'object'=>$object]; + fb_command_result($db,$cmdId,$actor['id'],(int)($command['createdAt'] ?? $now),$result); + return $result; } elseif ($type==='object.publish' || $type==='object.move') { $kind=(($command['kind'] ?? '')==='dynamic')?'dynamic':'static'; $object=fb_normalize_object(is_array($command['object'] ?? null)?$command['object']:[], $kind, $actor, $config); fb_asset_owner($db,$object['assetId'],$actor['id']); if ($type==='object.publish' && !fb_quota($db,$actor,$config)) fb_fail('publish_limit_reached',429); $active=0; foreach($db['objects'] as $o) if (empty($o['deleted_at']) && ($o['moderation_status'] ?? 'active')==='active') $active++; if ($type==='object.publish' && $active >= (int)$config['max_world_objects']) fb_fail('world_object_limit_reached',409); + if ($type==='object.publish') $object['publishedAt']=$now; $db['objects'][$object['id']]=['id'=>$object['id'],'kind'=>$kind,'asset_id'=>$object['assetId'],'owner_account_id'=>$actor['id'],'json'=>$object,'version'=>$object['version'],'published_at'=>(int)$object['publishedAt'],'updated_at'=>$now,'deleted_at'=>null,'deleted_by'=>null,'moderation_status'=>'active']; fb_event($db,'object.upsert',['type'=>'object.upsert','kind'=>$kind,'objectId'=>$object['id'],'actorAccountId'=>$actor['id']]); } elseif ($type==='object.delete') { @@ -151,8 +211,9 @@ function fb_process(array &$db, array $command, array $actor, array $config): ar } elseif ($type==='report.object') { $objectId=fb_clean_id($command['objectId'] ?? ''); $assetId=fb_clean_id($command['assetId'] ?? ''); $kind=(($command['objectKind'] ?? $command['kind'] ?? '')==='dynamic')?'dynamic':'static'; $reason=fb_clean_text($command['reason'] ?? 'other','other',48); if($objectId==='' || $assetId==='') fb_fail('report_target_required'); $reportId=fb_clean_id($command['reportId'] ?? $command['id'] ?? ('report_'.bin2hex(random_bytes(6)))); $db['reports'][$reportId]=['id'=>$reportId,'object_id'=>$objectId,'asset_id'=>$assetId,'object_kind'=>$kind,'reporter_account_id'=>$actor['id'],'reason'=>$reason,'created_at'=>$now,'status'=>'open']; fb_event($db,'report.object',['type'=>'report.object','objectId'=>$objectId,'assetId'=>$assetId,'actorAccountId'=>$actor['id'],'reason'=>$reason]); } else return ['ok'=>false,'id'=>$cmdId,'error'=>'unsupported_command']; - $db['commands'][$cmdId]=['id'=>$cmdId,'actor_account_id'=>$actor['id'],'created_at'=>(int)($command['createdAt'] ?? $now),'applied_at'=>$now]; - return ['ok'=>true,'id'=>$cmdId]; + $result=['ok'=>true,'id'=>$cmdId,'type'=>$type]; + fb_command_result($db,$cmdId,$actor['id'],(int)($command['createdAt'] ?? $now),$result); + return $result; } function fb_votes(array $db, string $targetType): array { $out=[]; foreach($db['votes'] as $v){ if(($v['target_type'] ?? '')!==$targetType || (int)($v['value'] ?? 0)===0) continue; $id=$v['target_id']; $val=(int)$v['value']; if(!isset($out[$id])) $out[$id]=['up'=>0,'down'=>0,'voters'=>[]]; if($val>0)$out[$id]['up']++; if($val<0)$out[$id]['down']++; $out[$id]['voters'][$v['voter_account_id']]=$val; } return $out; } function fb_snapshot(array $db): array { @@ -169,8 +230,9 @@ function fb_snapshot(array $db): array { $action = $_GET['action'] ?? ''; if ($action === 'health' || $action === '') fb_respond(['ok'=>true,'service'=>'pixel-island-api','serverNow'=>fb_now(),'sqlite'=>false,'fileDb'=>true,'note'=>'pdo_sqlite is unavailable; using locked JSON fallback']); fb_with_db($config, function(array &$db) use ($action, $config) { + fb_seed_defaults($db, $config); if ($action === 'snapshot') return fb_snapshot($db); if ($action === 'account') { $data=fb_request_json((int)$config['max_json_bytes']); $account=fb_auth($db, is_array($data['account'] ?? null)?$data['account']:$data, true); return ['ok'=>true,'account'=>['id'=>$account['id'],'name'=>$account['name'],'createdAt'=>(int)$account['created_at']]]; } - if ($action === 'commands') { $data=fb_request_json((int)$config['max_json_bytes']); $actor=fb_auth($db, is_array($data['account'] ?? null)?$data['account']:[], true); $commands=is_array($data['commands'] ?? null)?$data['commands']:[]; if(count($commands)>100) fb_fail('too_many_commands',413); $applied=[]; $rejected=[]; foreach($commands as $command){ if(!is_array($command)) continue; try{ $result=fb_process($db,$command,$actor,$config); if($result['ok'] ?? false) $applied[]=$result['id']; else $rejected[]=$result; } catch(Throwable $e){ $rejected[]=['ok'=>false,'id'=>fb_clean_id($command['id'] ?? ''),'error'=>$e->getMessage()]; } } return ['ok'=>true,'appliedCommandIds'=>$applied,'rejectedCommands'=>$rejected,'snapshot'=>fb_snapshot($db)]; } + if ($action === 'commands') { $data=fb_request_json((int)$config['max_json_bytes']); $actor=fb_auth($db, is_array($data['account'] ?? null)?$data['account']:[], true); $commands=is_array($data['commands'] ?? null)?$data['commands']:[]; if(count($commands)>100) fb_fail('too_many_commands',413); $applied=[]; $rejected=[]; foreach($commands as $command){ if(!is_array($command)) continue; try{ $GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT']=true; $result=fb_process($db,$command,$actor,$config); $GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT']=false; if($result['ok'] ?? false) $applied[]=$result['id']; else $rejected[]=$result; } catch(Throwable $e){ $GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT']=false; $id=fb_clean_id($command['id'] ?? ''); $result=['ok'=>false,'id'=>$id,'type'=>(string)($command['type'] ?? ''),'error'=>$e->getMessage()]; if($id!=='') fb_command_result($db,$id,$actor['id'],(int)($command['createdAt'] ?? fb_now()),$result); $rejected[]=$result; } } return ['ok'=>true,'appliedCommandIds'=>$applied,'rejectedCommands'=>$rejected,'snapshot'=>fb_snapshot($db)]; } return ['ok'=>false,'error'=>'unknown_action']; }); diff --git a/api/index.php b/api/index.php index 2c32c9c..4cfa52d 100644 --- a/api/index.php +++ b/api/index.php @@ -19,6 +19,9 @@ function respond(array $payload, int $status = 200): void { } function fail(string $message, int $status = 400, array $extra = []): void { + if (!empty($GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT'])) { + throw new RuntimeException($message, $status); + } respond(['ok' => false, 'error' => $message] + $extra, $status); } @@ -134,9 +137,13 @@ CREATE TABLE IF NOT EXISTS commands ( id TEXT PRIMARY KEY, actor_account_id TEXT NOT NULL, created_at INTEGER NOT NULL, - applied_at INTEGER NOT NULL + applied_at INTEGER NOT NULL, + result_json TEXT ); SQL); + $columns = []; + foreach ($db->query('PRAGMA table_info(commands)') as $row) $columns[$row['name']] = true; + if (empty($columns['result_json'])) $db->exec('ALTER TABLE commands ADD COLUMN result_json TEXT'); } function authenticate(PDO $db, array $account, bool $createIfMissing = true): array { @@ -178,6 +185,69 @@ function decode_json_row(?string $json): array { return is_array($decoded) ? $decoded : []; } + +function default_seed_path(): string { + return dirname(__DIR__) . DIRECTORY_SEPARATOR . '_data' . DIRECTORY_SEPARATOR . 'default_gallery_seed.json'; +} + +function read_default_seed(): array { + $path = default_seed_path(); + if (!is_file($path)) return ['assets' => [], 'placed' => [], 'dynamicSummons' => []]; + $raw = file_get_contents($path); + if (!is_string($raw) || $raw === '') return ['assets' => [], 'placed' => [], 'dynamicSummons' => []]; + $data = json_decode($raw, true); + return is_array($data) ? $data : ['assets' => [], 'placed' => [], 'dynamicSummons' => []]; +} + +function seed_default_gallery(PDO $db): void { + static $done = false; + if ($done) return; + $done = true; + $seed = read_default_seed(); + $assets = is_array($seed['assets'] ?? null) ? $seed['assets'] : []; + $placed = is_array($seed['placed'] ?? null) ? $seed['placed'] : []; + $dynamic = is_array($seed['dynamicSummons'] ?? null) ? $seed['dynamicSummons'] : []; + if (!$assets && !$placed && !$dynamic) return; + $now = now_ms(); + $db->prepare('INSERT OR IGNORE INTO accounts(id, name, password_hash, created_at, updated_at) VALUES(?,?,?,?,?)') + ->execute(['island-team', 'Pixel Island', password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT), $now, $now]); + foreach ($assets as $asset) { + if (!is_array($asset)) continue; + $id = clean_id($asset['id'] ?? ''); + if ($id === '') continue; + $exists = $db->prepare('SELECT id FROM assets WHERE id = ?'); + $exists->execute([$id]); + if ($exists->fetch()) continue; + $asset['ownerAccountId'] = 'island-team'; + $asset['author'] = $asset['author'] ?? 'Pixel Island'; + $asset['contentHash'] = $asset['contentHash'] ?? null; + $json = json_encode($asset, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json === false) continue; + $db->prepare('INSERT INTO assets(id, owner_account_id, author_name, json, version, created_at, updated_at, deleted_at, deleted_by, content_hash) VALUES(?,?,?,?,?,?,?,?,?,?)') + ->execute([$id, 'island-team', clean_text($asset['author'] ?? 'Pixel Island', 'Pixel Island', 32), $json, max(1, (int)($asset['version'] ?? 1)), (int)($asset['createdAt'] ?? $now), (int)($asset['updatedAt'] ?? $now), null, null, $asset['contentHash'] ?? null]); + } + $insertObject = function(array $object, string $kind) use ($db, $now): void { + $id = clean_id($object['id'] ?? ''); + $assetId = clean_id($object['assetId'] ?? ''); + if ($id === '' || $assetId === '') return; + $exists = $db->prepare('SELECT id FROM objects WHERE id = ?'); + $exists->execute([$id]); + if ($exists->fetch()) return; + $assetExists = $db->prepare('SELECT id FROM assets WHERE id = ? AND deleted_at IS NULL'); + $assetExists->execute([$assetId]); + if (!$assetExists->fetch()) return; + $object['ownerAccountId'] = 'island-team'; + $object['status'] = 'active'; + $object['publishedAt'] = (int)($object['publishedAt'] ?? $now); + $json = json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json === false) return; + $db->prepare('INSERT INTO objects(id, kind, asset_id, owner_account_id, json, version, published_at, updated_at, deleted_at, deleted_by, moderation_status) VALUES(?,?,?,?,?,?,?,?,?,?,?)') + ->execute([$id, $kind, $assetId, 'island-team', $json, max(1, (int)($object['version'] ?? 1)), (int)$object['publishedAt'], $now, null, null, 'active']); + }; + foreach ($placed as $object) if (is_array($object)) $insertObject($object, 'static'); + foreach ($dynamic as $object) if (is_array($object)) $insertObject($object, 'dynamic'); +} + function normalize_asset(array $asset, array $actor, array $config): array { $id = clean_id($asset['id'] ?? ''); if ($id === '') fail('asset_id_required'); @@ -237,6 +307,12 @@ function ensure_asset_owner(PDO $db, string $assetId, string $actorId): array { return $asset; } +function store_command_result(PDO $db, string $cmdId, string $actorId, int $createdAt, array $result): void { + $json = json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $db->prepare('INSERT INTO commands(id, actor_account_id, created_at, applied_at, result_json) VALUES(?,?,?,?,?)') + ->execute([$cmdId, $actorId, $createdAt, now_ms(), $json === false ? null : $json]); +} + function publish_quota_available(PDO $db, array $actor, array $config): bool { $now = now_ms(); $created = (int) ($actor['created_at'] ?? $now); @@ -252,9 +328,15 @@ function process_command(PDO $db, array $command, array $actor, array $config): $type = (string) ($command['type'] ?? ''); if ($cmdId === '' || $type === '') return ['ok' => false, 'id' => $cmdId, 'error' => 'invalid_command']; - $stmt = $db->prepare('SELECT id FROM commands WHERE id = ?'); + $stmt = $db->prepare('SELECT result_json FROM commands WHERE id = ?'); $stmt->execute([$cmdId]); - if ($stmt->fetch()) return ['ok' => true, 'id' => $cmdId, 'duplicate' => true]; + $stored = $stmt->fetch(); + if ($stored) { + $result = decode_json_row($stored['result_json'] ?? ''); + if (!$result) $result = ['ok' => true, 'id' => $cmdId]; + $result['duplicate'] = true; + return $result; + } $now = now_ms(); if ($type === 'asset.create') { @@ -277,6 +359,39 @@ function process_command(PDO $db, array $command, array $actor, array $config): $db->prepare('UPDATE objects SET deleted_at = ?, deleted_by = ?, updated_at = ? WHERE asset_id = ? AND deleted_at IS NULL') ->execute([$now, $actor['id'], $now, $assetId]); record_event($db, 'asset.delete', ['type' => 'asset.delete', 'assetId' => $assetId, 'actorAccountId' => $actor['id']]); + } elseif ($type === 'publish.asset_object') { + $asset = normalize_asset(is_array($command['asset'] ?? null) ? $command['asset'] : [], $actor, $config); + $kind = (($command['kind'] ?? $asset['category'] ?? '') === 'dynamic') ? 'dynamic' : 'static'; + $object = normalize_object(is_array($command['object'] ?? null) ? $command['object'] : [], $kind, $actor, $config); + if ($object['assetId'] !== $asset['id']) fail('publish_asset_mismatch', 400); + $existingAsset = $db->prepare('SELECT owner_account_id FROM assets WHERE id = ? AND deleted_at IS NULL'); + $existingAsset->execute([$asset['id']]); + $assetRow = $existingAsset->fetch(); + if ($assetRow && $assetRow['owner_account_id'] !== $actor['id']) fail('asset_id_already_owned', 403); + if (!publish_quota_available($db, $actor, $config)) fail('publish_limit_reached', 429); + $activeCount = (int) $db->query("SELECT COUNT(*) AS c FROM objects WHERE deleted_at IS NULL AND moderation_status = 'active'")->fetch()['c']; + if ($activeCount >= (int) $config['max_world_objects']) fail('world_object_limit_reached', 409); + + $assetJson = json_encode($asset, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($assetJson === false) fail('asset_too_large', 413); + $db->prepare('INSERT INTO assets(id, owner_account_id, author_name, json, version, created_at, updated_at, deleted_at, deleted_by, content_hash) + VALUES(?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(id) DO UPDATE SET author_name=excluded.author_name, json=excluded.json, version=excluded.version, updated_at=excluded.updated_at, deleted_at=NULL, deleted_by=NULL, content_hash=excluded.content_hash') + ->execute([$asset['id'], $actor['id'], $asset['author'], $assetJson, $asset['version'], $asset['createdAt'], $asset['updatedAt'], null, null, $asset['contentHash'] ?? null]); + + $object['publishedAt'] = $now; + if ($kind === 'dynamic') $object['createdAt'] = $now; + else $object['placedAt'] = $now; + $objectJson = json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($objectJson === false) fail('object_too_large', 413); + $db->prepare('INSERT INTO objects(id, kind, asset_id, owner_account_id, json, version, published_at, updated_at, deleted_at, deleted_by, moderation_status) + VALUES(?,?,?,?,?,?,?,?,?,?,?)') + ->execute([$object['id'], $kind, $object['assetId'], $actor['id'], $objectJson, $object['version'], $now, $now, null, null, 'active']); + record_event($db, 'asset.upsert', ['type' => 'asset.upsert', 'assetId' => $asset['id'], 'actorAccountId' => $actor['id']]); + record_event($db, 'object.upsert', ['type' => 'object.upsert', 'kind' => $kind, 'objectId' => $object['id'], 'actorAccountId' => $actor['id']]); + $result = ['ok' => true, 'id' => $cmdId, 'type' => $type, 'assetId' => $asset['id'], 'objectId' => $object['id'], 'kind' => $kind, 'publishedAt' => $now, 'object' => $object]; + store_command_result($db, $cmdId, $actor['id'], (int) ($command['createdAt'] ?? $now), $result); + return $result; } elseif ($type === 'object.publish' || $type === 'object.move') { $kind = (($command['kind'] ?? '') === 'dynamic') ? 'dynamic' : 'static'; $object = normalize_object(is_array($command['object'] ?? null) ? $command['object'] : [], $kind, $actor, $config); @@ -284,6 +399,7 @@ function process_command(PDO $db, array $command, array $actor, array $config): if ($type === 'object.publish' && !publish_quota_available($db, $actor, $config)) fail('publish_limit_reached', 429); $activeCount = (int) $db->query("SELECT COUNT(*) AS c FROM objects WHERE deleted_at IS NULL AND moderation_status = 'active'")->fetch()['c']; if ($type === 'object.publish' && $activeCount >= (int) $config['max_world_objects']) fail('world_object_limit_reached', 409); + if ($type === 'object.publish') $object['publishedAt'] = $now; $json = json_encode($object, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $publishedAt = (int) ($object['publishedAt'] ?? $now); $db->prepare('INSERT INTO objects(id, kind, asset_id, owner_account_id, json, version, published_at, updated_at, deleted_at, deleted_by, moderation_status) @@ -329,9 +445,9 @@ function process_command(PDO $db, array $command, array $actor, array $config): return ['ok' => false, 'id' => $cmdId, 'error' => 'unsupported_command']; } - $db->prepare('INSERT INTO commands(id, actor_account_id, created_at, applied_at) VALUES(?,?,?,?)') - ->execute([$cmdId, $actor['id'], (int) ($command['createdAt'] ?? $now), $now]); - return ['ok' => true, 'id' => $cmdId]; + $result = ['ok' => true, 'id' => $cmdId, 'type' => $type]; + store_command_result($db, $cmdId, $actor['id'], (int) ($command['createdAt'] ?? $now), $result); + return $result; } function vote_snapshot(PDO $db, string $targetType): array { @@ -350,6 +466,7 @@ function vote_snapshot(PDO $db, string $targetType): array { } function build_snapshot(PDO $db): array { + seed_default_gallery($db); $assets = []; foreach ($db->query('SELECT json FROM assets WHERE deleted_at IS NULL ORDER BY updated_at DESC LIMIT 2000') as $row) { $asset = decode_json_row($row['json']); @@ -433,7 +550,9 @@ try { if (!is_array($command)) continue; try { $database->beginTransaction(); + $GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT'] = true; $result = process_command($database, $command, $actor, $config); + $GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT'] = false; if ($result['ok'] ?? false) { $database->commit(); $applied[] = $result['id']; @@ -442,8 +561,15 @@ try { $rejected[] = $result; } } catch (Throwable $e) { + $GLOBALS['PIXEL_ISLAND_COMMAND_CONTEXT'] = false; if ($database->inTransaction()) $database->rollBack(); - $rejected[] = ['ok' => false, 'id' => clean_id($command['id'] ?? ''), 'error' => $e->getMessage()]; + $id = clean_id($command['id'] ?? ''); + $rejection = ['ok' => false, 'id' => $id, 'type' => (string)($command['type'] ?? ''), 'error' => $e->getMessage()]; + if ($id !== '') { + try { store_command_result($database, $id, $actor['id'], (int)($command['createdAt'] ?? now_ms()), $rejection); } + catch (Throwable $ignored) {} + } + $rejected[] = $rejection; } } respond(['ok' => true, 'appliedCommandIds' => $applied, 'rejectedCommands' => $rejected, 'snapshot' => build_snapshot($database)]); diff --git a/app.js b/app.js index c3c0b90..e60c340 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ -(() => { +(() => { 'use strict'; - console.info('Pixel Island Summoner shared build loaded'); + console.info('Pixel Island loaded'); const STORAGE_KEY = 'pixel-island-summoner:phase6b'; const LEGACY_STORAGE_KEYS = ['pixel-island-summoner:phase6a', 'pixel-island-summoner:phase6b:schema27']; @@ -47,93 +47,6 @@ const EDITOR_HISTORY_LIMIT = 80; const MAX_EDITOR_DIMENSION = 64; const PHASE5_GUARDRAILS = { maxAssets: 220, maxWorldObjects: 1000, maxReports: 200, defaultDisplayLimit: 250, newArrivalSlots: 150, revivalSlots: 100, publishLimitFirstDay: 5, publishLimitTrusted: 10, upvoteDelaySlots: 20, downvoteAdvanceSlots: 25, upvoteRankCap: 50, maxParticles: 200, particleMinZoom: 0.72 }; - const DEFAULT_GALLERY_DISPLAY_ASSET_NAMES = new Set([ - 'Pine Cluster', - 'Lantern Walker', - 'Coral Skiff', - 'Wildflower Patch', - 'Bridge Mechanic', - 'Town Gardener', - 'Lantern Courier', - 'Plaza Musician', - 'Desert Train', - 'Tideglass Lighthouse', - 'Rosetta Stela', - 'Rain Bell Tower', - 'Puddle Toad', - 'Aurora Bathhouse', - 'Copper Windmill', - 'Moon Gate Bridge', - 'Starlit Bookshop', - 'Crystal Bakery', - 'Kelp Observatory', - 'Ember Shrine', - 'Honeycomb Greenhouse', - 'Celadon Pagoda', - 'Tide Pearl Arch', - 'Lavender Market', - 'Skyseed Tower', - 'Orchard Terrace', - 'Dreamcatcher Tree', - 'Nebula Fountain', - 'Coral Lighthouse', - 'Origami Wind Shrine', - 'Rainmaker Drum', - 'Velvet Mushroom Grove', - 'Star Map Monument', - 'Lantern Reef Gate', - 'Ember Salamander', - 'Moon Snail', - 'Ribbon Dragonfly', - 'Teacup Automaton', - 'Sapphire Crab', - 'Cloud Alpaca', - 'Starling Bard', - 'Glass Jellyfish', - 'Copper Heron', - 'Paper Fox Kite', - 'Prism Seahorse', - 'Orchard Sprite', - 'Map Turtle', - 'Tea Sparrow', - 'Bubble Diver', - 'Mist Eel', - 'Ruby Beetle', - 'Snowy Tanuki', - 'Lighthouse Keeper', - 'Midnight Ramen Cart', - 'Quartz Clocktower', - 'Lotus Tea Pavilion', - 'Meteorite Garden', - 'Azure Glasshouse', - 'Foxfire Torii', - 'Pearl Cloud Altar', - 'Clockwork Conservatory', - 'Inkcap Mouse', - 'Comet Finch', - 'Lotus Kappa', - 'Lantern Manta', - 'Pocket Astronaut', - 'Mossy Capybara', - 'Opal Octopus', - 'Tiny Oracle', - 'Amber Apiary', - 'Glass Moon Dock', - 'Bluebell Chapel', - 'Stardust Orchard', - 'Prism Ferris Wheel', - 'Moss Lantern Gate', - 'Shell Music Stage', - 'Terracotta Bathhouse', - 'Copper Hedgehog', - 'Lily Crane', - 'Pearl Angler', - 'Neon Guppy', - 'Cloud Jelly Pup', - 'Ember Firefly', - 'Teal Seahare', - 'Honeybee Courier' - ]); const editorActions = () => MODULES.EditorActions || null; @@ -224,7 +137,9 @@ dirty = true; continue; } - if (Number(entry.expiresAt || 0) > 0 && now > Number(entry.expiresAt)) { + // Keep optimistic shared-world placement visuals until a server event accepts or rejects them. + // A short TTL made newly drawn works vanish while the command was still pending. + if (Number(entry.expiresAt || 0) > 0 && now > Number(entry.expiresAt) && entry.expirePending === true) { delete visuals[id]; dirty = true; continue; @@ -257,7 +172,7 @@ } function currentRole() { - return els.assetCategory?.value || 'nature'; + return els.assetCategory?.value || 'human'; } function roleToCategory(role) { @@ -283,7 +198,7 @@ openCollection: $('openCollection'), openMenu: $('openMenu'), closeEditor: $('closeEditor'), - placementPreviewBar: $('placementPreviewBar'), confirmPreviewPlace: $('confirmPreviewPlace'), backToCanvas: $('backToCanvas'), createAccount: $('createAccount'), accountNote: $('accountNote'), accountId: $('accountId'), accountPass: $('accountPass'), + placeHereButton: $('placeHereButton'), createAccount: $('createAccount'), accountNote: $('accountNote'), accountId: $('accountId'), accountPass: $('accountPass'), drawer: $('studioDrawer'), tabs: [...document.querySelectorAll('.tab')], panels: [...document.querySelectorAll('.tabPanel')], @@ -416,15 +331,16 @@ let shapeGesture = null; let shapePreview = null; let placementPreview = null; - let sharedSyncInFlight = false; let sharedSyncTimer = 0; - let sharedSyncEnabled = true; + let sharedSyncInFlight = false; + let sharedApiAvailable = null; + let lastSharedSyncAt = 0; + let suppressCanvasContextMenuOnce = false; async function bootstrap() { resizeCanvas(); resetView(false); await restoreStateFromIndexedDb(); - await initializeSharedWorld(); rebuildWorldIndex(); hydrateRuntime(); coastalFoamTextures = buildCoastalFoamTextures(); @@ -432,6 +348,7 @@ renderPalette(); hydrateAuthorUI(); updateAccountUI(); + if (els.assetCategory && !els.assetCategory.value) els.assetCategory.value = 'human'; refreshCategoryUI(); setupEditor(8, blankPixels(8), null); clearEditorHistory(); @@ -439,6 +356,7 @@ updateSelectedLabel(); hydrateVisualSettingsUI(); updateRotationStats(); + await syncSharedWorld({ silent: true, reason: 'boot' }); startSharedSyncLoop(); scheduleFrame(); } @@ -486,8 +404,7 @@ }); els.createAccount?.addEventListener('click', () => createLocalAccount(false)); - els.confirmPreviewPlace?.addEventListener('click', confirmPreviewPlacement); - els.backToCanvas?.addEventListener('click', cancelPlacementPreview); + els.placeHereButton?.addEventListener('click', confirmPreviewPlacement); els.settingLights?.addEventListener('change', () => { visualSettings().enableLights = !!els.settingLights.checked; @@ -532,6 +449,14 @@ els.canvas.addEventListener('wheel', onWorldWheel, { passive: false }); els.canvas.addEventListener('contextmenu', (event) => { event.preventDefault(); + if (suppressCanvasContextMenuOnce) { + suppressCanvasContextMenuOnce = false; + return; + } + if (mode === 'place' && placementPreview?.asset && placementPreview.x != null && placementPreview.y != null) { + confirmPreviewPlacement(); + return; + } cancelPlacementPreview(false); clearWorldSelection(true); setMode('inspect'); @@ -594,8 +519,14 @@ els.toggleAdvanced?.addEventListener('click', () => { setAdvancedDraw(!advancedDraw); }); - els.clearPaint.addEventListener('click', () => { - if (!confirm('Clear the entire canvas? This removes all pixels, lights, depth, particles, and selection.')) return; + els.clearPaint.addEventListener('click', async () => { + const confirmed = await confirmWebsiteDialog({ + title: 'Clear canvas', + message: 'Clear the entire canvas? This removes all pixels, lights, depth, particles, and selection.', + okText: 'Clear', + danger: true + }); + if (!confirmed) return; commitEditorMutation(() => { editorPixels = blankPixels(editorSize); editorLeftPixels = mirrorEditorPixelsHorizontal(editorPixels, editorSize, editorWidth, editorHeight); @@ -677,7 +608,7 @@ function setDrawerOpen(open) { els.drawer.classList.toggle('open', open); - els.openEditor.hidden = open; + els.openEditor.hidden = open || mode === 'place' || Boolean(placementPreview); if (open) ensureEditorHelpers(); if (!open) { selectedObject = null; @@ -781,6 +712,28 @@ [els.modeErase, els.drawerErase].filter(Boolean).forEach((el) => el.classList.toggle('active', isErase)); els.canvas.classList.toggle('placeCursor', isPlace); els.canvas.classList.toggle('eraseCursor', isErase); + syncPlacementUi(); + } + + function syncPlacementUi() { + const hasPreview = Boolean(placementPreview); + if (els.openEditor) els.openEditor.hidden = els.drawer?.classList.contains('open') || mode === 'place' || hasPreview; + updatePlaceHereButton(); + } + + function updatePlaceHereButton() { + if (!els.placeHereButton) return; + const ready = Boolean(mode === 'place' && placementPreview?.asset && placementPreview.x != null && placementPreview.y != null); + els.placeHereButton.hidden = !ready; + if (!ready) return; + const x = Number(placementPreview.x); + const y = Number(placementPreview.y); + const pos = tileToWorld(x + .5, y + .5); + pos.y -= getLiftAtCoord(x, y); + const sx = pos.x * view.zoom + view.x; + const sy = pos.y * view.zoom + view.y; + els.placeHereButton.style.left = `${Math.round(clamp(sx, 96, cw - 96))}px`; + els.placeHereButton.style.top = `${Math.round(clamp(sy - 64, 80, ch - 118))}px`; } function refreshCategoryUI() { @@ -907,9 +860,14 @@ const downPos = getCanvasPoint(event); cursorScreen = { x: downPos.x, y: downPos.y, active: true }; if (event.button === 2) { - cancelPlacementPreview(false); - clearWorldSelection(true); - setMode('inspect'); + if (mode === 'place' && placementPreview?.asset && placementPreview.x != null && placementPreview.y != null) { + suppressCanvasContextMenuOnce = true; + confirmPreviewPlacement(); + } else { + cancelPlacementPreview(false); + clearWorldSelection(true); + setMode('inspect'); + } pointer.down = false; pointer.id = null; return; @@ -1212,12 +1170,20 @@ function getPublishQuotaStatus(now = Date.now()) { state.publishLog = Array.isArray(state.publishLog) ? state.publishLog : []; + state.pendingPublishLog = Array.isArray(state.pendingPublishLog) ? state.pendingPublishLog : []; const author = currentVoterKey(); const windowMs = 60 * 60 * 1000; state.publishLog = state.publishLog.filter((entry) => now - Number(entry.at || 0) < windowMs * 24); - const used = state.publishLog.filter((entry) => entry.author === author && now - Number(entry.at || 0) < windowMs).length; + state.pendingPublishLog = state.pendingPublishLog.filter((entry) => entry.status === 'pending' && now - Number(entry.at || 0) < windowMs * 24); + const serverConfirmed = [...(state.placed || []), ...(state.dynamicSummons || [])].filter((object) => { + const owner = normalizeOwnerAccountId(object.ownerAccountId); + return owner === author && now - Number(object.publishedAt || object.placedAt || object.createdAt || 0) < windowMs; + }).length; + const localConfirmed = state.publishLog.filter((entry) => entry.author === author && now - Number(entry.at || 0) < windowMs).length; + const pending = state.pendingPublishLog.filter((entry) => entry.author === author && now - Number(entry.at || 0) < windowMs).length; + const used = (isSharedWorld() ? serverConfirmed : localConfirmed) + pending; const limit = currentPublishLimit(now); - return { used, limit, remaining: Math.max(0, limit - used), accountRequired: !state.account?.createdAt }; + return { used, pending, limit, remaining: Math.max(0, limit - used), accountRequired: !state.account?.createdAt }; } function canPublishObject(action = 'publish') { @@ -1236,17 +1202,25 @@ return true; } - function recordObjectPublish(kind, object, action = 'publish') { + function recordObjectPublish(kind, object, action = 'publish', commandId = '') { if (!object) return; const now = Date.now(); object.publishedAt = now; object.status = 'active'; state.publishLog = Array.isArray(state.publishLog) ? state.publishLog : []; - state.publishLog.push({ at: now, author: currentVoterKey(), kind, objectId: object.id, assetId: object.assetId, action }); + state.publishLog.push({ at: now, author: currentVoterKey(), kind, objectId: object.id, assetId: object.assetId, action, commandId }); state.publishLog = state.publishLog.slice(-300); updatePublishQuotaUI(); } + function recordPendingSharedPublish(kind, object, commandId = '') { + if (!object || !commandId) return; + state.pendingPublishLog = Array.isArray(state.pendingPublishLog) ? state.pendingPublishLog : []; + state.pendingPublishLog.push({ at: Date.now(), author: currentVoterKey(), kind, objectId: object.id, assetId: object.assetId, commandId, status: 'pending' }); + state.pendingPublishLog = state.pendingPublishLog.slice(-300); + updatePublishQuotaUI(); + } + function isSharedWorld() { return state.worldMode === 'shared'; } @@ -1261,6 +1235,7 @@ function ensureWorldProtectionState(target = state) { target.worldMode = target.worldMode === 'shared' ? 'shared' : 'local'; + target.pendingPublishLog = Array.isArray(target.pendingPublishLog) ? target.pendingPublishLog.slice(-300) : []; target.serverSync = { lastServerEventId: target.serverSync?.lastServerEventId || null, authority: { ...DEFAULT_SERVER_AUTHORITY, ...(target.serverSync?.authority || {}) }, @@ -1331,6 +1306,7 @@ if (kind === 'dynamic') hydrateRuntime(); saveState(); updateSelectionBubble(performance.now()); + render(); return object; } @@ -1363,7 +1339,7 @@ function displayAssetAuthor(asset) { if (!asset) return state.authorName || 'Local Artist'; - return isSharedWorld() ? (asset.author || 'Local Artist') : (state.authorName || 'Local Artist'); + return asset.author || state.authorName || 'Local Artist'; } function makeSharedCommand(type, payload = {}) { @@ -1379,188 +1355,231 @@ function queueSharedCommand(command, options = {}) { if (!command) return; ensureWorldProtectionState(); - Phase2Sync?.cacheOutboxCommand?.(command).catch((error) => console.warn('Phase 2 outbox cache failed.', error)); - if (command.object?.id && command.object?.assetId) { + const cachePromise = Phase2Sync?.cacheOutboxCommand?.(command); + const commandObject = command.object?.id && command.object?.assetId ? command.object : null; + if (commandObject) { const visualKind = command.kind === 'dynamic' ? 'dynamic' : 'static'; const existing = visualKind === 'dynamic' - ? state.dynamicSummons.find((item) => item.id === command.object.id) - : state.placed.find((item) => item.id === command.object.id); - const toX = visualKind === 'dynamic' ? Number(command.object.homeX) : Number(command.object.x); - const toY = visualKind === 'dynamic' ? Number(command.object.homeY) : Number(command.object.y); - state.serverSync.pendingObjectVisuals[command.object.id] = { + ? state.dynamicSummons.find((item) => item.id === commandObject.id) + : state.placed.find((item) => item.id === commandObject.id); + const toX = visualKind === 'dynamic' ? Number(commandObject.homeX) : Number(commandObject.x); + const toY = visualKind === 'dynamic' ? Number(commandObject.homeY) : Number(commandObject.y); + state.serverSync.pendingObjectVisuals[commandObject.id] = { + status: 'pending', kind: visualKind, - assetId: command.object.assetId, - object: { ...command.object }, + assetId: commandObject.assetId, + object: { ...commandObject }, fromX: Number(existing ? (visualKind === 'dynamic' ? existing.homeX : existing.x) : toX), fromY: Number(existing ? (visualKind === 'dynamic' ? existing.homeY : existing.y) : toY), toX, toY, createdAt: command.createdAt || Date.now(), - expiresAt: Date.now() + 15000, + expiresAt: 0, + expirePending: false, commandId: command.id }; } saveState(); - triggerSharedSyncSoon(); if (options.toast !== false) toast('Change queued for server validation.'); + if (cachePromise?.then) { + cachePromise + .then(() => syncSharedWorld({ silent: true, reason: 'queue' })) + .catch((error) => console.warn('Phase 2 outbox cache failed.', error)); + } else setTimeout(() => syncSharedWorld({ silent: true, reason: 'queue' }), 0); } - function makeSharedAssetCommandForPublish(asset, createdAt = Date.now()) { - if (!asset || !asset.id) return null; - const command = makeSharedCommand('asset.create', { asset: { ...asset } }); - command.createdAt = createdAt; - return command; - } - function queueSharedAssetBeforeObject(asset, objectCommand) { - const createdAt = Math.max(0, Number(objectCommand?.createdAt || Date.now()) - 1); - const assetCommand = makeSharedAssetCommandForPublish(asset, createdAt); - if (assetCommand) queueSharedCommand(assetCommand, { toast: false }); - } function sharedApiUrl(action) { return `${SHARED_API_ENDPOINT}?action=${encodeURIComponent(action)}`; } - async function sharedApiGet(action) { + async function requestSharedApi(action, options = {}) { const response = await fetch(sharedApiUrl(action), { - method: 'GET', - headers: { 'Accept': 'application/json' }, - credentials: 'same-origin', - cache: 'no-store' - }); - const data = await response.json().catch(() => ({})); - if (!response.ok || data?.ok === false) throw new Error(data?.error || `HTTP ${response.status}`); - return data; - } - - async function sharedApiPost(action, payload) { - const response = await fetch(sharedApiUrl(action), { - method: 'POST', - headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, credentials: 'same-origin', cache: 'no-store', - body: JSON.stringify(payload || {}) + ...options, + headers: { + ...(options.body ? { 'Content-Type': 'application/json' } : {}), + ...(options.headers || {}) + } }); - const data = await response.json().catch(() => ({})); - if (!response.ok || data?.ok === false) throw new Error(data?.error || `HTTP ${response.status}`); + const text = await response.text(); + let data = null; + try { data = text ? JSON.parse(text) : null; } + catch (error) { throw new Error(`invalid_json_response:${response.status}`); } + if (!response.ok || !data?.ok) { + const message = data?.error || `http_${response.status}`; + const error = new Error(message); + error.payload = data; + error.status = response.status; + throw error; + } return data; } - async function initializeSharedWorld() { + function prunePendingVisualsByCommandIds(commandIds) { + const ids = new Set(Array.from(commandIds || []).filter(Boolean)); + if (!ids.size) return; ensureWorldProtectionState(); - state.worldMode = 'shared'; - try { - await syncSharedWorld({ initial: true, quiet: true }); - } catch (error) { - sharedSyncEnabled = false; - console.warn('Shared API is not available; using local-only fallback.', error); - state.worldMode = 'local'; - toast('Shared API unavailable. Running in local-only mode.'); + for (const [objectId, entry] of Object.entries(state.serverSync.pendingObjectVisuals || {})) { + if (ids.has(entry?.commandId)) delete state.serverSync.pendingObjectVisuals[objectId]; } } - function startSharedSyncLoop() { - if (sharedSyncTimer || !sharedSyncEnabled) return; - sharedSyncTimer = window.setInterval(() => { - syncSharedWorld({ quiet: true }).catch((error) => console.warn('Shared sync failed.', error)); - }, SHARED_SYNC_INTERVAL_MS); - } - - function triggerSharedSyncSoon() { - if (!sharedSyncEnabled || !isSharedWorld()) return; - window.setTimeout(() => { - syncSharedWorld({ quiet: true }).catch((error) => console.warn('Shared sync failed.', error)); - }, 80); - } - - async function registerSharedAccount() { - if (!sharedSyncEnabled || !state.account?.createdAt) return null; - return sharedApiPost('account', { account: state.account }); - } - - async function flushSharedOutbox() { - if (!Phase2Sync?.readOutboxCommands || !Phase2Sync?.deleteOutboxCommands || !state.account?.createdAt) return null; - const commands = await Phase2Sync.readOutboxCommands(100); - if (!commands.length) return null; - const data = await sharedApiPost('commands', { account: state.account, commands }); - const applied = Array.isArray(data.appliedCommandIds) ? data.appliedCommandIds : []; - if (applied.length) await Phase2Sync.deleteOutboxCommands(applied); - const rejected = Array.isArray(data.rejectedCommands) ? data.rejectedCommands : []; - if (rejected.length) { - console.warn('Some shared commands were rejected.', rejected); - toast(`Server rejected ${rejected.length} queued change(s).`); + function markPendingVisualsFailed(rejections) { + const items = Array.isArray(rejections) ? rejections : []; + if (!items.length) return; + ensureWorldProtectionState(); + const byCommandId = new Map(items.map((item) => [item?.id, item]).filter(([id]) => id)); + for (const entry of Object.values(state.serverSync.pendingObjectVisuals || {})) { + const rejection = byCommandId.get(entry?.commandId); + if (!rejection) continue; + entry.status = 'failed'; + entry.error = rejection.error || 'server_rejected'; + entry.failedAt = Date.now(); + entry.expirePending = false; + entry.expiresAt = 0; + if (entry.object) entry.object.publishFailed = true; } - return data.snapshot || null; + } + + function prunePendingVisualsResolvedBySnapshot() { + ensureWorldProtectionState(); + const serverObjectIds = new Set([...(state.placed || []), ...(state.dynamicSummons || [])].map((item) => item.id).filter(Boolean)); + for (const objectId of Object.keys(state.serverSync.pendingObjectVisuals || {})) { + if (serverObjectIds.has(objectId)) delete state.serverSync.pendingObjectVisuals[objectId]; + } + } + + function removePublishLogForCommandIds(commandIds) { + const ids = new Set(Array.from(commandIds || []).filter(Boolean)); + if (!ids.size) return; + if (Array.isArray(state.publishLog)) state.publishLog = state.publishLog.filter((entry) => !ids.has(entry.commandId)); + if (Array.isArray(state.pendingPublishLog)) state.pendingPublishLog = state.pendingPublishLog.filter((entry) => !ids.has(entry.commandId)); + updatePublishQuotaUI(); + } + + function describeSharedRejection(error) { + const code = String(error || 'server_rejected'); + if (code === 'publish_limit_reached') return 'Publish limit reached. This placement was not published.'; + if (code === 'world_object_limit_reached') return 'Island object limit reached. This placement was not published.'; + if (code === 'asset_owner_required') return 'Only the asset owner can publish this work. Use Remix to make your own copy.'; + if (code === 'object_owner_required') return 'Only the owner can move or delete that island object.'; + if (code === 'invalid_account_password') return 'Account password did not match the server record.'; + return `Server rejected a queued change: ${code}`; + } + + function handleSharedCommandResponse(data, options = {}) { + const applied = Array.isArray(data?.appliedCommandIds) ? data.appliedCommandIds.filter(Boolean) : []; + const rejected = Array.isArray(data?.rejectedCommands) ? data.rejectedCommands.filter(Boolean) : []; + const rejectedIds = rejected.map((item) => item?.id).filter(Boolean); + const finishedIds = [...applied, ...rejectedIds]; + if (finishedIds.length) { + Phase2Sync?.deleteOutboxCommands?.(finishedIds).catch((error) => console.warn('Phase 2 outbox delete failed.', error)); + } + prunePendingVisualsByCommandIds(applied); + markPendingVisualsFailed(rejected); + removePublishLogForCommandIds(finishedIds); + if (rejected.length && options.silent !== true) toast(`${describeSharedRejection(rejected[0]?.error)} Try publishing again or choose a new tile.`); + else if (rejected.length) console.warn(describeSharedRejection(rejected[0]?.error), rejected); + } + + function serverAssetMerge(snapshotAssets) { + const serverAssets = (Array.isArray(snapshotAssets) ? snapshotAssets : []).map(normalizeAsset); + const serverIds = new Set(serverAssets.map((asset) => asset.id)); + const actor = currentAccountId(); + const localOnly = (state.assets || []).filter((asset) => { + if (!asset?.id || serverIds.has(asset.id)) return false; + const owner = normalizeOwnerAccountId(asset.ownerAccountId); + if (owner === 'island-team') return false; + return owner === actor || (!owner && (asset.author || '') === (state.authorName || 'Local Artist')); + }); + return [...localOnly, ...serverAssets]; + } + + function applySharedSnapshot(snapshot) { + if (!snapshot?.ok) return false; + const previousSelectedAssetId = selectedAssetId; + const pendingVisuals = state.serverSync?.pendingObjectVisuals || {}; + const merged = mergeDefaultGallery({ + ...state, + schema: SAVE_SCHEMA, + worldMode: 'shared', + assets: serverAssetMerge(snapshot.assets), + placed: Array.isArray(snapshot.placed) ? snapshot.placed : [], + dynamicSummons: Array.isArray(snapshot.dynamicSummons) ? snapshot.dynamicSummons : [], + objectVotes: snapshot.objectVotes || {}, + assetVotes: snapshot.assetVotes || {}, + moderationReports: normalizeModerationReports(snapshot.moderationReports || []), + tombstones: snapshot.tombstones || { assets: {}, objects: {} }, + serverSync: { + ...sanitizeManifestServerSync(state.serverSync || {}), + lastServerEventId: snapshot.lastEventId || state.serverSync?.lastServerEventId || null, + authority: { ...DEFAULT_SERVER_AUTHORITY, ...(snapshot.authority || {}) }, + clock: { + ...(state.serverSync?.clock || {}), + worldTimeMs: Number(snapshot.serverNow) || Date.now(), + syncedAt: Date.now(), + dayMs: Number(state.serverSync?.clock?.dayMs) || DAY_MS + }, + pendingObjectVisuals: pendingVisuals + } + }); + state = normalizeState(merged); + prunePendingVisualsResolvedBySnapshot(); + if (previousSelectedAssetId && state.assets.some((asset) => asset.id === previousSelectedAssetId)) selectedAssetId = previousSelectedAssetId; + else selectedAssetId = state.assets[0]?.id ?? null; + rebuildWorldIndex(); + hydrateRuntime(); + spriteCache.clear(); + saveState(); + renderLibrary({ preserveScroll: true }); + updateSelectedLabel(); + updateRotationStats(); + render(); + return true; } async function syncSharedWorld(options = {}) { - if (!sharedSyncEnabled || sharedSyncInFlight) return false; + if (sharedSyncInFlight || document.hidden) return false; + if (!globalThis.fetch) return false; sharedSyncInFlight = true; try { - ensureWorldProtectionState(); - state.worldMode = 'shared'; - if (state.account?.createdAt) await registerSharedAccount(); - let snapshot = await flushSharedOutbox(); - if (!snapshot) snapshot = await sharedApiGet('snapshot'); - applySharedSnapshot(snapshot, options); + let data = null; + const commands = Phase2Sync?.readOutboxCommands ? await Phase2Sync.readOutboxCommands(300) : []; + if (commands.length && state.account?.id && state.account?.password) { + data = await requestSharedApi('commands', { + method: 'POST', + body: JSON.stringify({ account: state.account, commands }) + }); + handleSharedCommandResponse(data, options); + if (data.snapshot) applySharedSnapshot(data.snapshot); + } else { + data = await requestSharedApi('snapshot'); + applySharedSnapshot(data); + } + sharedApiAvailable = true; + lastSharedSyncAt = Date.now(); return true; + } catch (error) { + if (sharedApiAvailable !== false) console.warn('Shared API unavailable; using local island state.', error); + sharedApiAvailable = false; + if (options.reason === 'boot' && state.worldMode !== 'shared') { + state.worldMode = 'local'; + ensureWorldProtectionState(); + } + return false; } finally { sharedSyncInFlight = false; } } - function applySharedSnapshot(snapshot, options = {}) { - if (!snapshot || snapshot.ok === false) return false; - ensureWorldProtectionState(); - state.worldMode = 'shared'; - state.serverSync.lastServerEventId = snapshot.lastEventId || state.serverSync.lastServerEventId || null; - state.serverSync.authority = { ...DEFAULT_SERVER_AUTHORITY, ...(snapshot.authority || {}) }; - state.serverSync.clock = { - ...(state.serverSync.clock || {}), - worldTimeMs: Number(snapshot.serverNow) || Date.now(), - syncedAt: Date.now(), - dayMs: DAY_MS - }; - - const serverAssetIds = new Set((snapshot.assets || []).map((asset) => String(asset?.id || '')).filter(Boolean)); - const tombstonedAssets = snapshot.tombstones?.assets || {}; - const localOnlyAssets = (state.assets || []).filter((asset) => { - const id = String(asset?.id || ''); - if (!id || serverAssetIds.has(id) || tombstonedAssets[id]) return false; - return true; - }); - const serverAssets = (snapshot.assets || []).map(normalizeAsset); - state.assets = dedupeNormalizedAssets([...serverAssets, ...localOnlyAssets]); - state.placed = normalizePlacements(snapshot.placed || []); - state.dynamicSummons = normalizeDynamicSummons(snapshot.dynamicSummons || []); - state.assetVotes = snapshot.assetVotes || {}; - state.objectVotes = snapshot.objectVotes || {}; - state.moderationReports = normalizeModerationReports(snapshot.moderationReports || []); - state.tombstones = snapshot.tombstones || { assets: {}, objects: {} }; - rebuildWorldIndex(); - hydrateRuntime(); - cachePhase2Snapshot(); - localStorage.setItem(STORAGE_KEY, JSON.stringify(buildLocalManifest(state))); - if (selectedAssetId && !findAsset(selectedAssetId)) selectedAssetId = state.assets[0]?.id ?? null; - renderLibrary({ preserveScroll: activeTabName === 'library' }); - updateSelectionBubble(performance.now()); - updateRotationStats(); - render(); - if (!options.quiet && !options.initial) toast('Shared island synced.'); - return true; + function startSharedSyncLoop() { + if (sharedSyncTimer) clearInterval(sharedSyncTimer); + sharedSyncTimer = setInterval(() => syncSharedWorld({ silent: true, reason: 'timer' }), SHARED_SYNC_INTERVAL_MS); } - function queueSharedVote(targetType, targetId, value) { - if (!isSharedWorld() || !targetId) return; - ensureLocalAccount('vote'); - const payload = targetType === 'asset' - ? { assetId: targetId, value } - : { objectId: targetId, value }; - queueSharedCommand(makeSharedCommand(`vote.${targetType}`, payload), { toast: false }); - } - - function shouldHideUnderPlacementPreview(assetId, x, y) { return false; } @@ -1603,7 +1622,8 @@ const itemY = lerp(startY, targetY, eased) + .5; if (!Number.isFinite(itemX) || !Number.isFinite(itemY)) continue; if (!isApproxVisible(asset, itemX, itemY, rect)) continue; - items.push({ kind: pending.kind, asset, x: itemX, y: itemY, source: { ...source, pending: true }, pending: true }); + const failed = pending.status === 'failed'; + items.push({ kind: pending.kind, asset, x: itemX, y: itemY, source: { ...source, pending: !failed, failed }, pending: !failed, failed }); } if (placementPreview?.asset && placementPreview.x != null && placementPreview.y != null) { const asset = placementPreview.asset; @@ -1739,7 +1759,7 @@ } function selectWorldObject(kind, objectId, assetId, selectedAt = performance.now(), options = {}) { - selectedObject = { kind, id: objectId, assetId, selectedAt }; + selectedObject = { kind, id: objectId, assetId, selectedAt, bounce: options.bounce !== false }; cameraFollowSelected = options.follow !== false; if (els.bubbleMenuActions) els.bubbleMenuActions.hidden = true; selectedAssetId = assetId; @@ -1772,16 +1792,45 @@ } function placeSelected(x, y) { + const asset = placementPreview?.asset || findAsset(selectedAssetId); + if (!asset) { + toast('Select an asset first.'); + setDrawerOpen(true); + setTab('library'); + return; + } + const adjusted = avoidRemixSourceOverlap(asset, x, y); + x = adjusted.x; + y = adjusted.y; + const tile = world.get(x, y); + if (!canPlace(asset, tile)) return; + const savedAssetId = placementPreview?.savedAssetId || selectedAssetId || asset.id; + placementPreview = { asset, savedAssetId, x, y }; + clearWorldSelection(false); + syncPlacementUi(); + if (visualSettings().enableParticles) spawnEffects.push({ x: x + .5, y: y + .5, started: performance.now(), preview: true }); + render(); + toast('Position selected. Right-click or PLACE HERE to publish.'); + } + + function publishSelectedAt(x, y) { if (placementPreview?.asset) { const adjusted = avoidRemixSourceOverlap(placementPreview.asset, x, y); x = adjusted.x; y = adjusted.y; const tile = world.get(x, y); if (!canPlace(placementPreview.asset, tile)) return; + if (placementPreview.savedAssetId) { + selectedAssetId = placementPreview.savedAssetId; + placementPreview = null; + syncPlacementUi(); + placeSelected(x, y); + 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.'); + toast(adjusted.moved ? 'Preview nudged away from the remix source. Right-click or DRAW to exit.' : 'Preview set. Right-click or DRAW to exit.'); return; } const asset = findAsset(selectedAssetId); @@ -1797,9 +1846,8 @@ if (!canPlace(asset, tile)) return; if (adjusted.moved) toast('Placed slightly away from the remix source to avoid overlap.'); - const existingObject = asset.category === 'static' - ? state.placed.find((p) => p.assetId === asset.id) - : state.dynamicSummons.find((p) => p.assetId === asset.id); + // Each placement creates a distinct island object, so the same work can be summoned multiple times. + const existingObject = null; const kind = asset.category === 'dynamic' ? 'dynamic' : 'static'; const wasLocallyHidden = existingObject ? Boolean(state.hiddenObjects?.[existingObject.id]) : false; const wasRotationHidden = existingObject ? !isWorldObjectVisibleByRotation(kind, existingObject.id) : false; @@ -1846,15 +1894,19 @@ status: 'active', version: Number(existingObject?.version || 0) + 1 }; - const objectCommand = makeSharedCommand(existingObject ? 'object.move' : 'object.publish', { kind, object: nextObject }); - queueSharedAssetBeforeObject(asset, objectCommand); - queueSharedCommand(objectCommand); + const command = makeSharedCommand(existingObject ? 'object.move' : 'object.publish', { kind, object: nextObject }); + const publishCommand = makeSharedCommand('publish.asset_object', { kind, asset: normalizeAsset({ ...asset, ownerAccountId: actor }), object: nextObject }); + queueSharedCommand(existingObject ? command : publishCommand); + if (!existingObject) recordPendingSharedPublish(kind, nextObject, publishCommand.id); + syncSharedWorld({ silent: true, reason: 'publish' }); + setMode('inspect'); + syncPlacementUi(); return; } let object = null; if (asset.category === 'static') { - const existing = state.placed.find((p) => p.assetId === asset.id); + const existing = null; if (existing) { existing.x = x; existing.y = y; existing.placedAt = Date.now(); existing.version = (existing.version || 1) + 1; if (needsPublishSlot) { @@ -1862,18 +1914,18 @@ recordObjectPublish('static', existing, wasLocallyHidden || wasRotationHidden ? 'republish' : 'publish'); } object = existing; - selectWorldObject('static', existing.id, asset.id, performance.now()); + selectWorldObject('static', existing.id, asset.id, performance.now(), { bounce: false }); toast(needsPublishSlot ? `${asset.name} republished.` : `${asset.name} moved.`); } else { const placed = { id: uid(), assetId: asset.id, ownerAccountId: normalizeOwnerAccountId(asset.ownerAccountId, currentAccountId()), x, y, placedAt: Date.now(), publishedAt: Date.now(), status: 'active', version: 1 }; state.placed.push(placed); recordObjectPublish('static', placed, 'publish'); object = placed; - selectWorldObject('static', placed.id, asset.id, performance.now()); + selectWorldObject('static', placed.id, asset.id, performance.now(), { bounce: false }); toast(`${asset.name} placed.`); } } else { - const existing = state.dynamicSummons.find((p) => p.assetId === asset.id); + const existing = null; if (existing) { existing.homeX = x; existing.homeY = y; existing.createdAt = Date.now(); existing.version = (existing.version || 1) + 1; if (needsPublishSlot) { @@ -1890,14 +1942,15 @@ toast(`${asset.name} summoned.`); } hydrateRuntime(); - const found = state.dynamicSummons.find((p) => p.assetId === asset.id); - if (found) selectWorldObject('dynamic', found.id, asset.id, performance.now()); + if (object) selectWorldObject('dynamic', object.id, asset.id, performance.now(), { bounce: false }); } rebuildWorldIndex(); recordSyncEvent(Phase2Sync?.createObjectUpsertEvent?.(kind, object)); if (visualSettings().enableParticles) spawnEffects.push({ x: x + .5, y: y + .5, started: performance.now() }); saveState(); updateRotationStats(); + setMode('inspect'); + syncPlacementUi(); } function canPlace(asset, tile) { @@ -2736,7 +2789,7 @@ } function particleDirectionLabel(dir) { - return ({ up: '↑', down: '↓', left: '←', right: '→' }[dir] || '↑'); + return ({ up: '↑', down: '↓', left: '←', right: '→' }[dir] || 'Unknown'); } function particleRangeLabel() { @@ -3420,18 +3473,32 @@ return asset; } + function queueSharedAssetSave(asset, options = {}) { + if (!asset || !isSharedWorld()) return false; + ensureLocalAccount('save'); + const actor = currentAccountId(); + if (!actor) return false; + const ownedAsset = normalizeAsset({ ...asset, ownerAccountId: actor, author: state.account?.name || state.authorName || asset.author }); + queueSharedCommand(makeSharedCommand('asset.create', { asset: ownedAsset }), { toast: options.toast === true }); + syncSharedWorld({ silent: true, reason: 'asset-save' }); + return true; + } + function saveAssetFromEditor() { + if (isSharedWorld()) ensureLocalAccount('save'); const record = buildEditorAssetRecord(); if (!record) return null; const { asset, existing } = record; const duplicate = existing ? null : findEquivalentCollectionAsset(asset); if (duplicate) { selectedAssetId = duplicate.id; + queueSharedAssetSave(duplicate, { toast: false }); toast(`${duplicate.name} is already in Collection.`); return duplicate; } persistAssetRecord(record); - recordSyncEvent(Phase2Sync?.createAssetUpsertEvent?.(asset)); + if (isSharedWorld()) queueSharedAssetSave(asset, { toast: false }); + else recordSyncEvent(Phase2Sync?.createAssetUpsertEvent?.(asset)); editParentId = null; editOriginalId = null; editingAssetId = null; @@ -3450,13 +3517,12 @@ const asset = saveAssetFromEditor(); if (!asset) return; selectedAssetId = asset.id; - placementPreview = { asset, x: null, y: null, savedAssetId: asset.id }; + placementPreview = null; clearWorldSelection(false); setMode('place'); setDrawerOpen(false); - if (els.placementPreviewBar) els.placementPreviewBar.hidden = false; - if (els.openEditor) els.openEditor.hidden = true; - toast('Saved to Collection. Click a tile, then choose Place here or Back to canvas.'); + syncPlacementUi(); + toast('Saved to Collection. Left-click a tile to choose a position, then right-click or PLACE HERE to publish.'); } @@ -3493,17 +3559,16 @@ placementPreview = { asset, x: null, y: null }; setDrawerOpen(false); setMode('place'); - if (els.placementPreviewBar) els.placementPreviewBar.hidden = false; - if (els.openEditor) els.openEditor.hidden = true; - toast('Click a valid tile to preview. Then choose Place here or Back to canvas.'); + syncPlacementUi(); + toast('Left-click a valid tile to preview. Right-click or PLACE HERE to confirm a saved work.'); } function cancelPlacementPreview(reopenDrawer = true) { const hadPreview = !!placementPreview; placementPreview = null; - if (els.placementPreviewBar) els.placementPreviewBar.hidden = true; - if (!reopenDrawer && els.openEditor) els.openEditor.hidden = false; + syncPlacementUi(); if (reopenDrawer) setDrawerOpen(true); + else syncPlacementUi(); if (hadPreview) render(); } @@ -3520,9 +3585,8 @@ selectedAssetId = assetId; const { x, y } = placementPreview; placementPreview = null; - if (els.placementPreviewBar) els.placementPreviewBar.hidden = true; - if (els.openEditor) els.openEditor.hidden = false; - placeSelected(x, y); + syncPlacementUi(); + publishSelectedAt(x, y); setMode('inspect'); updateSelectionBubble(performance.now()); } @@ -3531,7 +3595,7 @@ editParentId = null; editOriginalId = null; els.assetName.value = ''; - els.assetCategory.value = ''; + els.assetCategory.value = 'human'; staticKind = 'nature'; dynamicKind = 'human'; editingSide = 'right'; @@ -3587,7 +3651,6 @@ state.authorName = state.account.name; } saveState(); - registerSharedAccount().catch((error) => console.warn('Shared account registration failed.', error)); updateAccountUI(); renderLibrary(); if (!silent) toast('Local account generated. Change the password after creation.'); @@ -3630,7 +3693,8 @@ function updatePublishQuotaUI() { const quota = getPublishQuotaStatus(); - const text = quota.accountRequired ? 'Publish: account needed' : `Publish: ${quota.remaining}/${quota.limit} left`; + const pendingText = quota.pending ? ` / ${quota.pending} pending` : ''; + const text = quota.accountRequired ? 'Publish: account needed' : `Publish: ${quota.remaining}/${quota.limit} left${pendingText}`; [els.drawQuotaBadge, els.finishQuotaBadge].filter(Boolean).forEach((badge) => { badge.textContent = text; badge.classList.toggle('quotaEmpty', !quota.accountRequired && quota.remaining <= 0); @@ -3686,6 +3750,7 @@ 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'; + if (libraryViewTab === 'mine' && mine.length === 0 && others.length > 0) libraryViewTab = 'others'; uiState.library.viewTab = libraryViewTab; renderCollectionTabs(groups); @@ -3703,7 +3768,7 @@ 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; + const placedCount = visibleAssets.filter((asset) => getAssetPlacementStats(asset.id).total > 0).length; summary.textContent = `${visibleAssets.length} works / ${placedCount} on island`; toolbar.append(summary); @@ -3830,6 +3895,14 @@ return snapshot; } + function getAssetPlacementStats(assetId) { + const confirmed = state.placed.filter((p) => p.assetId === assetId).length + state.dynamicSummons.filter((p) => p.assetId === assetId).length; + const visuals = Object.values(state.serverSync?.pendingObjectVisuals || {}).filter((entry) => entry?.assetId === assetId || entry?.object?.assetId === assetId); + const failed = visuals.filter((entry) => entry?.status === 'failed').length; + const pending = Math.max(0, visuals.length - failed); + return { confirmed, pending, failed, total: confirmed + pending + failed }; + } + function makeAssetCard(asset) { const card = document.createElement('article'); const expanded = asset.id === selectedAssetId; @@ -3846,15 +3919,17 @@ 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 placementStats = getAssetPlacementStats(asset.id); + const placementCount = placementStats.total; + const isPlaced = placementCount > 0; 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'; + placeTag.className = `assetStatus ${isPlaced ? 'placed' : 'stored'}${placementStats.pending && !placementStats.confirmed ? ' validating' : ''}${placementStats.failed && !placementStats.confirmed ? ' failed' : ''}`; + placeTag.textContent = isPlaced ? (placementStats.confirmed ? (placementCount > 1 ? `On island ${placementCount}` : 'On island') : (placementStats.failed ? 'Publish failed' : 'Publishing')) : 'Stored'; statusRow.append(roleTag, placeTag); meta.append(statusRow); @@ -3892,7 +3967,7 @@ 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) => { + const move = isMine ? makeButton('Place', (event) => { event?.stopPropagation?.(); const scrollState = captureLibraryScrollState(); selectedAssetId = asset.id; @@ -3900,7 +3975,8 @@ renderLibrary({ preserveScroll: true, scrollState }); setMode('place'); setDrawerOpen(false); - toast('Click a valid tile to place or move it.'); + syncPlacementUi(); + toast('Left-click a tile to choose a position, then right-click or PLACE HERE to publish another copy.'); }) : null; const del = makeButton('Delete', (event) => { event?.stopPropagation?.(); deleteAsset(asset); }); del.classList.add('danger'); @@ -4297,6 +4373,7 @@ renderLibrary(); updateSelectedLabel(); updateSelectionBubble(performance.now()); + render(); toast(`${label} deleted.`); } @@ -4527,6 +4604,7 @@ dynamicLogicRemainder = 0; render(lastRuntimeUpdate); lastRenderAt = lastRuntimeUpdate; + syncSharedWorld({ silent: true, reason: 'visible' }); scheduleFrame(); } @@ -4558,7 +4636,10 @@ const phase = getPhase(); const progress = phase.progress || 0; const degrees = progress * 360; - if (els.analogClockHand) els.analogClockHand.style.transform = `translate(-50%, -100%) rotate(${degrees.toFixed(2)}deg)`; + if (els.analogClockHand) { + els.analogClockHand.style.setProperty('--clock-rotate', `${degrees.toFixed(2)}deg`); + els.analogClockHand.style.transform = `translate(-50%, -100%) rotate(${degrees.toFixed(2)}deg)`; + } if (els.analogClock) { els.analogClock.style.setProperty('--clock-rotate', `${degrees.toFixed(2)}deg`); els.analogClock.setAttribute('aria-label', `${phase.label} island clock`); @@ -4824,6 +4905,7 @@ } if (nightLightsActive && phase.darkness > 0) drawLightSources(lightSources, phase.darkness, time); if (nightLightsActive) drawNightCursorGlow(time, phase); + updatePlaceHereButton(); } function drawTerrainProjectedShadows(phase) { @@ -5007,7 +5089,7 @@ if (asset.subtype === 'fish') { alpha = .42; bob = 4 + Math.sin(time / 380 + seed) * 1.7; } } - if (includeSelectBounce && selectedObject?.id === item.source?.id) { + if (includeSelectBounce && selectedObject?.bounce !== false && selectedObject?.id === item.source?.id) { const selectionElapsed = time - (selectedObject.selectedAt || 0); if (selectionElapsed < 920) { const jumpT = clamp(selectionElapsed / 520, 0, 1); @@ -5045,7 +5127,7 @@ if (drawShadow && !(asset.category === 'dynamic' && asset.subtype === 'fish')) drawSpriteShadow(info, phase); ctx.save(); - ctx.globalAlpha = alpha; + ctx.globalAlpha = item.failed || item.source?.failed ? alpha * 0.42 : alpha; const pivotX = Math.round(drawX + sprite.width / 2); const pivotY = Math.round(drawY + sprite.height * 0.8); if (angle || Math.abs(stretchX - 1) > 0.001 || Math.abs(stretchY - 1) > 0.001) { @@ -5063,6 +5145,15 @@ } ctx.restore(); + if (item.failed || item.source?.failed) { + ctx.save(); + ctx.strokeStyle = 'rgba(219, 68, 68, .9)'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 4]); + ctx.strokeRect(Math.round(drawX) - 3, Math.round(drawY) - 3, sprite.width + 6, sprite.height + 6); + ctx.restore(); + } + // External sprite lighting is now handled inside shadeAssetPixelColor() via getLocalSpriteLights(). // Keeping the old overlay pass here would double-apply non-cursor lights and waste a per-pixel scan. } @@ -5435,7 +5526,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { votes.voters[voter] = delta; } state.assetVotes[assetId] = votes; - if (isSharedWorld()) queueSharedVote('asset', assetId, votes.voters[voter] || 0); saveState(); renderLibrary({ preserveScroll: Boolean(scrollState), scrollState }); } @@ -5634,7 +5724,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { votes.voters[voter] = delta; } state.objectVotes[key] = votes; - if (isSharedWorld()) queueSharedVote('object', key, votes.voters[voter] || 0); saveState(); updateSelectionBubble(performance.now()); renderLibrary(); @@ -5756,16 +5845,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { createdAt: Date.now() }; state.moderationReports.push(report); - if (isSharedWorld()) { - ensureLocalAccount('report'); - queueSharedCommand(makeSharedCommand('report.object', { - reportId: report.id, - objectId: report.objectId, - assetId: report.assetId, - objectKind: report.objectKind, - reason: report.reason - }), { toast: false }); - } state.moderationReports = state.moderationReports.slice(-PHASE5_GUARDRAILS.maxReports); state.hiddenObjects ||= {}; state.hiddenObjects[target.id] = true; @@ -6295,11 +6374,15 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { assetVotes: manifest.assetVotes || {}, hiddenAssets: manifest.hiddenAssets || {}, hiddenObjects: manifest.hiddenObjects || {}, + assets: Array.isArray(manifest.assets) ? manifest.assets : seeded.assets, + placed: Array.isArray(manifest.placed) ? manifest.placed : seeded.placed, + dynamicSummons: Array.isArray(manifest.dynamicSummons) ? manifest.dynamicSummons : seeded.dynamicSummons, moderationReports: normalizeModerationReports(manifest.moderationReports || []), guardrails: { ...PHASE5_GUARDRAILS, ...(manifest.guardrails || {}) }, settings: { ...defaultVisualSettings(), ...(manifest.settings || {}) }, account: normalizeAccount(manifest.account), publishLog: Array.isArray(manifest.publishLog) ? manifest.publishLog.slice(-300) : [], + pendingPublishLog: Array.isArray(manifest.pendingPublishLog) ? manifest.pendingPublishLog.slice(-300) : [], worldMode: manifest.worldMode === 'shared' ? 'shared' : 'local', serverSync: sanitizeManifestServerSync(manifest.serverSync), tombstones: manifest.tombstones || { assets: {}, objects: {} }, @@ -6314,7 +6397,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { authority: { ...DEFAULT_SERVER_AUTHORITY, ...(serverSync.authority || {}) }, clock: serverSync.clock && typeof serverSync.clock === 'object' ? { ...serverSync.clock } : { worldTimeMs: Date.now(), syncedAt: Date.now() }, dynamicTargets: serverSync.dynamicTargets && typeof serverSync.dynamicTargets === 'object' ? { ...serverSync.dynamicTargets } : {}, - pendingObjectVisuals: {} + pendingObjectVisuals: serverSync.pendingObjectVisuals && typeof serverSync.pendingObjectVisuals === 'object' ? { ...serverSync.pendingObjectVisuals } : {} }; } @@ -6332,14 +6415,18 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { const cachedAssets = await Phase2Sync.readCachedAssets(assetIds); if (!cachedAssets.length) return false; const previousSelectedAssetId = selectedAssetId; - state = normalizeState({ + const assetById = new Map(); + for (const asset of [...(state.assets || []), ...cachedAssets]) { + if (asset?.id) assetById.set(asset.id, asset); + } + state = normalizeState(mergeDefaultGallery({ ...state, schema: SAVE_SCHEMA, - assets: cachedAssets, - placed: Array.isArray(snapshot.placed) ? snapshot.placed : [], - dynamicSummons: Array.isArray(snapshot.dynamicSummons) ? snapshot.dynamicSummons : [], + assets: [...assetById.values()], + placed: Array.isArray(snapshot.placed) ? snapshot.placed : state.placed, + dynamicSummons: Array.isArray(snapshot.dynamicSummons) ? snapshot.dynamicSummons : state.dynamicSummons, hiddenObjects: snapshot.hiddenObjects || state.hiddenObjects || {} - }); + })); if (previousSelectedAssetId && state.assets.some((asset) => asset.id === previousSelectedAssetId)) selectedAssetId = previousSelectedAssetId; else selectedAssetId = state.assets[0]?.id ?? null; return true; @@ -6361,11 +6448,15 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { assetVotes: target.assetVotes || {}, hiddenAssets: target.hiddenAssets || {}, hiddenObjects: target.hiddenObjects || {}, + assets: Array.isArray(target.assets) ? target.assets : [], + placed: Array.isArray(target.placed) ? target.placed : [], + dynamicSummons: Array.isArray(target.dynamicSummons) ? target.dynamicSummons : [], moderationReports: Array.isArray(target.moderationReports) ? target.moderationReports : [], guardrails: target.guardrails || null, settings: target.settings || null, account: target.account || null, publishLog: Array.isArray(target.publishLog) ? target.publishLog.slice(-300) : [], + pendingPublishLog: Array.isArray(target.pendingPublishLog) ? target.pendingPublishLog.slice(-300) : [], worldMode: target.worldMode === 'shared' ? 'shared' : 'local', serverSync, tombstones: target.tombstones || { assets: {}, objects: {} }, @@ -6468,6 +6559,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { settings: { ...defaultVisualSettings(), ...(input.settings || {}) }, account: normalizeAccount(input.account), publishLog: Array.isArray(input.publishLog) ? input.publishLog.slice(-300) : [], + pendingPublishLog: Array.isArray(input.pendingPublishLog) ? input.pendingPublishLog.slice(-300) : [], worldMode: input.worldMode === 'shared' ? 'shared' : 'local', serverSync: input.serverSync || { lastServerEventId: null }, tombstones: input.tombstones || { assets: {}, objects: {} }, @@ -6491,16 +6583,28 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { 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: '' })); + const protectedOwners = new Set(['island-team']); + const ownerByAssetId = new Map((target.assets || []).map((asset) => [asset.id, normalizeOwnerAccountId(asset.ownerAccountId)])); + target.assets = (target.assets || []).map((asset) => { + const owner = normalizeOwnerAccountId(asset.ownerAccountId); + if (protectedOwners.has(owner)) return { ...asset, author: 'Pixel Island', ownerAccountId: owner || 'island-team' }; + return { ...asset, author, ownerAccountId: owner || '' }; + }); + target.placed = (target.placed || []).map((item) => { + const owner = normalizeOwnerAccountId(item.ownerAccountId, ownerByAssetId.get(item.assetId) || ''); + return protectedOwners.has(owner) ? { ...item, ownerAccountId: owner } : { ...item, ownerAccountId: owner || '' }; + }); + target.dynamicSummons = (target.dynamicSummons || []).map((item) => { + const owner = normalizeOwnerAccountId(item.ownerAccountId, ownerByAssetId.get(item.assetId) || ''); + return protectedOwners.has(owner) ? { ...item, ownerAccountId: owner } : { ...item, ownerAccountId: owner || '' }; + }); return target; } function cleanWorkText(value, fallback = '') { const text = String(value ?? '') .replace(/[\uFFFD\u0080-\u009F]/g, '') - .replace(/[。-゚]+/g, '') + .replace(/[\r\n]+/g, '\n') .replace(/\s{2,}/g, ' ') .trim(); return text || fallback; @@ -6550,7 +6654,6 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { .filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y)) : []; 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; } @@ -6681,1032 +6784,20 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { return { homeX: pos.x, homeY: pos.y }; } - function buildDefaultGalleryPack(now = Date.now()) { - const assets = [ - makeAsset('Crescent Tea House', 'static', 'building', drawCrescentTeaHouse(), { - depthPixels: drawCrescentTeaHouseDepth(), - lightPixels: [{ x: 6, y: 5 }, { x: 9, y: 5 }, { x: 7, y: 10 }, { x: 8, y: 10 }], - lightColor: '#ffe55d', - door: { x: 7, y: 14 } - }), - makeAsset('Prism Sakura', 'static', 'nature', drawPrismSakura(), { - depthPixels: drawPrismSakuraDepth(), - particlePixels: [{ x: 5, y: 4, c: nearestPaletteCode('#ffb0c7'), dir: 'up' }, { x: 10, y: 5, c: nearestPaletteCode('#ead0ff'), dir: 'up' }] - }), - makeAsset('Clockwork Whale', 'static', 'ship', drawClockworkWhale(), { depthPixels: drawClockworkWhaleDepth() }), - makeAsset('Lantern Cat', 'dynamic', 'animal', drawLanternCatRight(), { - lightPixels: [{ x: 11, y: 8 }], - lightColor: '#ffe55d', - depthPixels: drawSmallRaisedDepth() - }), - makeAsset('Cloud Koi', 'dynamic', 'fish', drawCloudKoiRight(), { - particlePixels: [{ x: 5, y: 8, c: nearestPaletteCode('#aef8ff'), dir: 'up' }] - }), - makeAsset('Paper Crane', 'dynamic', 'bird', drawPaperCraneRight(), { - particlePixels: [{ x: 8, y: 6, c: nearestPaletteCode('#eef4ff'), dir: 'up' }] - }), - makeAsset('Moon Lantern', 'static', 'building', drawMoonLantern(), { - depthPixels: drawMoonLanternDepth(), - lightPixels: [{ x: 6, y: 7 }, { x: 7, y: 7 }, { x: 8, y: 7 }, { x: 7, y: 8 }, { x: 8, y: 8 }], - lightColor: '#ffe99a', - door: { x: 7, y: 14 } - }), - makeAsset('Glass Fern', 'static', 'nature', drawGlassFern(), { - depthPixels: filledDepthFromPixels(drawGlassFern()) - }), - makeAsset('Linen Cottage', 'static', 'building', drawCottage(), { - depthPixels: filledDepthFromPixels(drawCottage()), - lightPixels: [{ x: 10, y: 9 }], - lightColor: '#ffe38a', - door: { x: 7, y: 13 } - }), - makeAsset('Pine Cluster', 'static', 'nature', drawPineCluster(), { - depthPixels: filledDepthFromPixels(drawPineCluster()) - }), - makeAsset('Shell Rock', 'static', 'nature', drawShellRock(), { - depthPixels: filledDepthFromPixels(drawShellRock()) - }), - makeAsset('Coral Skiff', 'static', 'ship', drawCoralSkiff(), { depthPixels: drawCoralSkiffDepth() }), - makeAsset('Sprout Fox', 'dynamic', 'animal', drawSproutFoxRight(), { - depthPixels: drawSmallRaisedDepth() - }), - makeAsset('Azure Minnow', 'dynamic', 'fish', drawAzureMinnowRight(), { - particlePixels: [{ x: 5, y: 7, c: nearestPaletteCode('#bff7ff'), dir: 'up' }] - }), - makeAsset('Violet Moth', 'dynamic', 'animal', drawVioletMothRight(), { - particlePixels: [{ x: 7, y: 3, c: nearestPaletteCode('#ead1ff'), dir: 'up' }] - }), - makeAsset('Lantern Walker', 'dynamic', 'human', drawLanternWalkerRight(), { - lightPixels: [{ x: 9, y: 7 }, { x: 11, y: 8 }], - lightColor: '#ffe99a', - depthPixels: drawSmallRaisedDepth() - }), - makeAssetSized('Wildflower Patch', 'static', 'nature', drawWildflowerPatch(), 8, 8, { - depthPixels: filledDepthFromPixelsSized(drawWildflowerPatch(), 8, 8) - }), - makeAssetSized('River Stones', 'static', 'nature', drawRiverStones(), 8, 8, { - depthPixels: filledDepthFromPixelsSized(drawRiverStones(), 8, 8) - }), - makeAsset('Maple Canopy', 'static', 'nature', drawMapleCanopy(), { - depthPixels: drawMapleCanopyDepth() - }), - makeAsset('Misty Falls', 'static', 'nature', drawMistyFalls(), { - depthPixels: drawMistyFallsDepth(), - particlePixels: [{ x: 6, y: 8, c: nearestPaletteCode('#dffbff'), dir: 'up' }, { x: 10, y: 8, c: nearestPaletteCode('#dffbff'), dir: 'up' }] - }), - makeAssetSized('Lotus Pond', 'static', 'nature', drawLotusPond(), 16, 12, { - depthPixels: drawLotusPondDepth(), - particlePixels: [{ x: 6, y: 6, c: nearestPaletteCode('#ffd9e8'), dir: 'up' }, { x: 11, y: 5, c: nearestPaletteCode('#ffffff'), dir: 'up' }] - }), - makeAsset('Mossy Arch', 'static', 'nature', drawMossyArch(), { - depthPixels: drawMossyArchDepth() - }), - makeAssetSized('Sunflower Grove', 'static', 'nature', drawSunflowerGrove(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawSunflowerGrove(), 12, 16) - }), - makeAssetSized('Reed Bed', 'static', 'nature', drawReedBed(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawReedBed(), 12, 12) - }), - makeAssetSized('Firefly Swirl', 'dynamic', 'animal', drawFireflySwirlRight(), 8, 8, { - lightPixels: [{ x: 3, y: 4 }, { x: 5, y: 3 }], - lightColor: '#fff08a', - depthPixels: filledDepthFromPixelsSized(drawFireflySwirlRight(), 8, 8) - }), - makeAssetSized('Meadow Hare', 'dynamic', 'animal', drawMeadowHareRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawMeadowHareRight(), 12, 12) - }), - makeAssetSized('Brook Turtle', 'dynamic', 'animal', drawBrookTurtleRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawBrookTurtleRight(), 12, 10), - particlePixels: [{ x: 1, y: 7, c: nearestPaletteCode('#a8e5ff'), dir: 'up' }] - }), - makeAssetSized('Leaf Sparrow', 'dynamic', 'bird', drawLeafSparrowRight(), 8, 8, { - depthPixels: filledDepthFromPixelsSized(drawLeafSparrowRight(), 8, 8) - }), - makeAssetSized('Red Panda', 'dynamic', 'animal', drawRedPandaRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawRedPandaRight(), 12, 12) - }), - makeAssetSized('River Otter', 'dynamic', 'animal', drawRiverOtterRight(), 14, 8, { - depthPixels: filledDepthFromPixelsSized(drawRiverOtterRight(), 14, 8) - }), - makeAssetSized('Amber Deer', 'dynamic', 'animal', drawAmberDeerRight(), 14, 14, { - depthPixels: filledDepthFromPixelsSized(drawAmberDeerRight(), 14, 14) - }), - makeAssetSized('Forest Owl', 'dynamic', 'bird', drawForestOwlRight(), 10, 12, { - depthPixels: filledDepthFromPixelsSized(drawForestOwlRight(), 10, 12) - }), - makeAssetSized('Kingfisher', 'dynamic', 'bird', drawKingfisherRight(), 10, 8, { - depthPixels: filledDepthFromPixelsSized(drawKingfisherRight(), 10, 8) - }), - makeAssetSized('Pond Duck', 'dynamic', 'bird', drawPondDuckRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawPondDuckRight(), 12, 10) - }), - makeAssetSized('Heron', 'dynamic', 'bird', drawHeronRight(), 10, 16, { - depthPixels: filledDepthFromPixelsSized(drawHeronRight(), 10, 16) - }), - makeAssetSized('Sunset Koi', 'dynamic', 'fish', drawSunsetKoiRight(), 12, 8, { - depthPixels: filledDepthFromPixelsSized(drawSunsetKoiRight(), 12, 8), - particlePixels: [{ x: 2, y: 4, c: nearestPaletteCode('#c8f4ff'), dir: 'up' }] - }), - makeAssetSized('Silver Trout', 'dynamic', 'fish', drawSilverTroutRight(), 12, 6, { - depthPixels: filledDepthFromPixelsSized(drawSilverTroutRight(), 12, 6), - particlePixels: [{ x: 1, y: 3, c: nearestPaletteCode('#dffbff'), dir: 'up' }] - }), - makeAssetSized('Butterfly Fish', 'dynamic', 'fish', drawButterflyFishRight(), 10, 8, { - depthPixels: filledDepthFromPixelsSized(drawButterflyFishRight(), 10, 8), - particlePixels: [{ x: 1, y: 4, c: nearestPaletteCode('#bff7ff'), dir: 'up' }] - }), - makeAssetSized('Town Gardener', 'dynamic', 'human', drawTownGardenerRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawTownGardenerRight(), 12, 16) - }), - makeAssetSized('Lantern Courier', 'dynamic', 'human', drawLanternCourierRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawLanternCourierRight(), 12, 16), - lightPixels: [{ x: 8, y: 9 }], - lightColor: '#ffe99a' - }), - makeAssetSized('Plaza Musician', 'dynamic', 'human', drawPlazaMusicianRight(), 14, 16, { - depthPixels: filledDepthFromPixelsSized(drawPlazaMusicianRight(), 14, 16) - }), - makeAssetSized('Bridge Mechanic', 'dynamic', 'human', drawBridgeMechanicRight(), 14, 16, { - depthPixels: filledDepthFromPixelsSized(drawBridgeMechanicRight(), 14, 16) - }), - makeAssetSized('Harbor Clocktower', 'static', 'building', drawHarborClocktower(), 32, 40, { - depthPixels: filledDepthFromPixelsSized(drawHarborClocktower(), 32, 40), - lightPixels: [{ x: 15, y: 12 }, { x: 16, y: 12 }, { x: 13, y: 22 }, { x: 18, y: 22 }, { x: 14, y: 28 }, { x: 17, y: 28 }], - lightColor: '#ffe99a', - door: { x: 15, y: 38 } - }), - makeAssetSized('Glass Greenhouse', 'static', 'building', drawGlassGreenhouse(), 28, 20, { - depthPixels: filledDepthFromPixelsSized(drawGlassGreenhouse(), 28, 20), - lightPixels: [{ x: 10, y: 10 }, { x: 13, y: 10 }, { x: 16, y: 10 }], - lightColor: '#fff4c2', - door: { x: 13, y: 18 } - }), - makeAssetSized('Steam Workshop', 'static', 'building', drawSteamWorkshop(), 40, 24, { - depthPixels: filledDepthFromPixelsSized(drawSteamWorkshop(), 40, 24), - lightPixels: [{ x: 13, y: 14 }, { x: 18, y: 14 }, { x: 27, y: 14 }, { x: 32, y: 14 }], - lightColor: '#ffd979', - door: { x: 20, y: 22 } - }), - makeAssetSized('Canal Bridge', 'static', 'building', drawCanalBridge(), 48, 16, { - depthPixels: filledDepthFromPixelsSized(drawCanalBridge(), 48, 16) - }), - makeAssetSized('Grand Fountain', 'static', 'building', drawGrandFountain(), 24, 24, { - depthPixels: filledDepthFromPixelsSized(drawGrandFountain(), 24, 24), - lightPixels: [{ x: 11, y: 7 }, { x: 12, y: 7 }, { x: 9, y: 12 }, { x: 14, y: 12 }], - lightColor: '#dffbff', - particlePixels: [{ x: 11, y: 5, c: nearestPaletteCode('#eafcff'), dir: 'up' }, { x: 12, y: 5, c: nearestPaletteCode('#eafcff'), dir: 'up' }] - }), - makeAssetSized('Rocket Monument', 'static', 'building', drawRocketMonument(), 20, 32, { - depthPixels: filledDepthFromPixelsSized(drawRocketMonument(), 20, 32), - lightPixels: [{ x: 9, y: 8 }, { x: 10, y: 8 }, { x: 8, y: 24 }, { x: 11, y: 24 }, { x: 9, y: 27 }, { x: 10, y: 27 }], - lightColor: '#ffdca6', - particlePixels: [{ x: 8, y: 27, c: nearestPaletteCode('#ffd979'), dir: 'up' }, { x: 11, y: 27, c: nearestPaletteCode('#ff8a5c'), dir: 'up' }], - door: { x: 9, y: 30 } - }), - makeAssetSized('Arcade Booth', 'static', 'building', drawArcadeBooth(), 24, 20, { - depthPixels: filledDepthFromPixelsSized(drawArcadeBooth(), 24, 20), - lightPixels: [{ x: 7, y: 10 }, { x: 16, y: 10 }, { x: 11, y: 6 }], - lightColor: '#98d8ff', - door: { x: 11, y: 18 } - }), - makeAssetSized('Chess Knight Statue', 'static', 'building', drawChessKnightStatue(), 16, 24, { - depthPixels: filledDepthFromPixelsSized(drawChessKnightStatue(), 16, 24), - lightPixels: [{ x: 7, y: 8 }, { x: 8, y: 8 }], - lightColor: '#dffbff' - }), - makeAssetSized('Desert Train', 'static', 'building', drawDesertTrain(), 44, 16, { - depthPixels: filledDepthFromPixelsSized(drawDesertTrain(), 44, 16), - lightPixels: [{ x: 7, y: 7 }, { x: 13, y: 8 }, { x: 19, y: 8 }, { x: 28, y: 8 }, { x: 35, y: 8 }], - lightColor: '#ffd979', - particlePixels: [{ x: 5, y: 3, c: nearestPaletteCode('#d9e4e8'), dir: 'up' }, { x: 7, y: 2, c: nearestPaletteCode('#d9e4e8'), dir: 'up' }] - }), - makeAssetSized('Tea Robot', 'dynamic', 'human', drawTeaRobotRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawTeaRobotRight(), 12, 16), - lightPixels: [{ x: 8, y: 8 }], - lightColor: '#9be6ff' - }), - makeAssetSized('Balloon Vendor', 'dynamic', 'human', drawBalloonVendorRight(), 14, 18, { - depthPixels: filledDepthFromPixelsSized(drawBalloonVendorRight(), 14, 18) - }), - makeAssetSized('Jelly Comet', 'dynamic', 'animal', drawJellyCometRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawJellyCometRight(), 12, 12), - lightPixels: [{ x: 7, y: 4 }], - lightColor: '#b8d6ff', - particlePixels: [{ x: 2, y: 6, c: nearestPaletteCode('#d8f0ff'), dir: 'up' }] - }), - makeAssetSized('Courier Bike', 'dynamic', 'human', drawCourierBikeRight(), 16, 12, { - depthPixels: filledDepthFromPixelsSized(drawCourierBikeRight(), 16, 12) - }), - makeAssetSized('Great Cedar', 'static', 'nature', drawGreatCedar(), 24, 32, { - depthPixels: drawGreatCedarDepth() - }), - makeAssetSized('Moonfall Cascade', 'static', 'nature', drawMoonfallCascade(), 24, 32, { - depthPixels: drawMoonfallCascadeDepth(), - particlePixels: [{ x: 9, y: 20, c: nearestPaletteCode('#dffbff'), dir: 'up' }, { x: 14, y: 20, c: nearestPaletteCode('#dffbff'), dir: 'up' }] - }), - makeAssetSized('Sunblossom Gate', 'static', 'nature', drawSunblossomGate(), 24, 24, { - depthPixels: drawSunblossomGateDepth() - }), - makeAssetSized('Echo Cavern', 'static', 'nature', drawEchoCavern(), 32, 20, { - depthPixels: drawEchoCavernDepth() - }), - makeAssetSized('Worldroot Shrine', 'static', 'nature', drawWorldrootShrine(), 28, 28, { - depthPixels: drawWorldrootShrineDepth(), - lightPixels: [{ x: 13, y: 12 }, { x: 14, y: 12 }, { x: 12, y: 13 }, { x: 15, y: 13 }], - lightColor: '#ffe99a', - door: { x: 14, y: 24 } - }), - makeAssetSized('Moon Jelly Aquarium', 'static', 'building', drawMoonJellyAquarium(), 24, 28, { - depthPixels: drawMoonJellyAquariumDepth(), - lightPixels: [{ x: 11, y: 0 }, { x: 12, y: 0 }, { x: 10, y: 10 }, { x: 13, y: 10 }, { x: 11, y: 20 }, { x: 12, y: 20 }], - lightColor: '#ffe7a6', - door: { x: 11, y: 24 } - }), - makeAssetSized('Aurora Observatory', 'static', 'building', drawAuroraObservatory(), 28, 28, { - depthPixels: filledDepthFromPixelsSized(drawAuroraObservatory(), 28, 28), - lightPixels: [{ x: 13, y: 2 }, { x: 14, y: 2 }, { x: 9, y: 13 }, { x: 18, y: 13 }, { x: 13, y: 23 }], - lightColor: '#d9eeff', - door: { x: 13, y: 24 } - }), - makeAssetSized('Hearthwind Mill', 'static', 'building', drawHearthwindMill(), 24, 28, { - depthPixels: filledDepthFromPixelsSized(drawHearthwindMill(), 24, 28), - lightPixels: [{ x: 8, y: 13 }, { x: 15, y: 13 }, { x: 11, y: 18 }], - lightColor: '#ffe6a5', - door: { x: 11, y: 22 } - }), - makeAssetSized('Tideglass Lighthouse', 'static', 'building', drawTideglassLighthouse(), 20, 36, { - depthPixels: filledDepthFromPixelsSized(drawTideglassLighthouse(), 20, 36), - lightPixels: [{ x: 8, y: 2 }, { x: 9, y: 2 }, { x: 8, y: 16 }, { x: 8, y: 24 }], - lightColor: '#f8f0bb', - door: { x: 8, y: 33 } - }), - makeAssetSized('Mossrail Depot', 'static', 'building', drawMossrailDepot(), 32, 20, { - depthPixels: filledDepthFromPixelsSized(drawMossrailDepot(), 32, 20), - lightPixels: [{ x: 6, y: 8 }, { x: 13, y: 8 }, { x: 20, y: 8 }, { x: 26, y: 8 }], - lightColor: '#ffe1a0', - door: { x: 15, y: 16 } - }), - makeAssetSized('Lantern Library', 'static', 'building', drawLanternLibrary(), 28, 24, { - depthPixels: filledDepthFromPixelsSized(drawLanternLibrary(), 28, 24), - lightPixels: [{ x: 8, y: 7 }, { x: 19, y: 7 }, { x: 13, y: 16 }, { x: 14, y: 16 }], - lightColor: '#ffe8b0', - door: { x: 13, y: 18 } - }), - makeAssetSized('Velvet Theater', 'static', 'building', drawVelvetTheater(), 28, 20, { - depthPixels: filledDepthFromPixelsSized(drawVelvetTheater(), 28, 20), - lightPixels: [{ x: 5, y: 1 }, { x: 9, y: 1 }, { x: 13, y: 1 }, { x: 17, y: 1 }, { x: 21, y: 1 }, { x: 13, y: 14 }], - lightColor: '#ffd19a', - door: { x: 13, y: 16 } - }), - makeAssetSized('Bloom Bridge Gate', 'static', 'nature', drawBloomBridgeGate(), 24, 24, { - depthPixels: filledDepthFromPixelsSized(drawBloomBridgeGate(), 24, 24), - particlePixels: [{ x: 7, y: 2, c: nearestPaletteCode('#ffd4ea'), dir: 'up' }, { x: 16, y: 2, c: nearestPaletteCode('#ffd4ea'), dir: 'up' }] - }), - makeAssetSized('Starreef Submarine', 'static', 'ship', drawStarreefSubmarine(), 32, 16, { - depthPixels: filledDepthFromPixelsSized(drawStarreefSubmarine(), 32, 16), - lightPixels: [{ x: 12, y: 3 }, { x: 15, y: 7 }, { x: 20, y: 7 }], - lightColor: '#c5f1ff' - }), - makeAssetSized('Astrolabe Courier', 'dynamic', 'human', drawAstrolabeCourierRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawAstrolabeCourierRight(), 12, 16), - lightPixels: [{ x: 6, y: 5 }], - lightColor: '#d9eeff' - }), - makeAssetSized('Puddle Toad', 'dynamic', 'animal', drawPuddleToadRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawPuddleToadRight(), 12, 10) - }), - makeAssetSized('Ribbon Swan', 'dynamic', 'bird', drawRibbonSwanRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawRibbonSwanRight(), 12, 12) - }), - makeAssetSized('Lantern Beetle', 'dynamic', 'animal', drawLanternBeetleRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawLanternBeetleRight(), 12, 12), - lightPixels: [{ x: 5, y: 3 }], - lightColor: '#fff1a8' - }), - makeAssetSized('Smile Portrait', 'static', 'building', drawSmilePortrait(), 24, 32, { - depthPixels: filledDepthFromPixelsSized(drawSmilePortrait(), 24, 32) - }), - makeAssetSized('Great Wave Panel', 'static', 'nature', drawGreatWavePanel(), 32, 24, { - depthPixels: filledDepthFromPixelsSized(drawGreatWavePanel(), 32, 24) - }), - makeAssetSized('Sunflower Still Life', 'static', 'nature', drawSunflowerStillLife(), 24, 32, { - depthPixels: filledDepthFromPixelsSized(drawSunflowerStillLife(), 24, 32) - }), - makeAssetSized('Rosetta Stela', 'static', 'building', drawRosettaStela(), 24, 32, { - depthPixels: filledDepthFromPixelsSized(drawRosettaStela(), 24, 32) - }), - makeAssetSized('Pharaoh Mask', 'static', 'building', drawPharaohMask(), 24, 32, { - depthPixels: filledDepthFromPixelsSized(drawPharaohMask(), 24, 32) - }), - makeAssetSized('Stone Circle', 'static', 'nature', drawStoneCircle(), 32, 24, { - depthPixels: filledDepthFromPixelsSized(drawStoneCircle(), 32, 24) - }), - makeAssetSized('Terracotta Sentinel', 'static', 'building', drawTerracottaSentinel(), 20, 32, { - depthPixels: filledDepthFromPixelsSized(drawTerracottaSentinel(), 20, 32) - }), - makeAssetSized('Rain Bell Tower', 'static', 'building', drawRainBellTower(), 20, 36, { - depthPixels: filledDepthFromPixelsSized(drawRainBellTower(), 20, 36), - lightPixels: [{ x: 9, y: 11 }, { x: 9, y: 19 }], - lightColor: '#fff0b8', - door: { x: 9, y: 33 } - }), - makeAssetSized('Crimson Pagoda', 'static', 'building', drawCrimsonPagoda(), 24, 32, { - depthPixels: filledDepthFromPixelsSized(drawCrimsonPagoda(), 24, 32), - lightPixels: [{ x: 8, y: 12 }, { x: 15, y: 12 }, { x: 11, y: 22 }], - lightColor: '#ffd38a', - door: { x: 11, y: 28 } - }), - makeAssetSized('Meteor Forge', 'static', 'building', drawMeteorForge(), 32, 24, { - depthPixels: filledDepthFromPixelsSized(drawMeteorForge(), 32, 24), - lightPixels: [{ x: 8, y: 10 }, { x: 23, y: 10 }, { x: 15, y: 16 }], - lightColor: '#ffb36b', - door: { x: 15, y: 19 } - }), - makeAssetSized('Ink Garden Screen', 'static', 'nature', drawInkGardenScreen(), 32, 24, { - depthPixels: filledDepthFromPixelsSized(drawInkGardenScreen(), 32, 24) - }), - makeAssetSized('Meadow Totem', 'static', 'nature', drawMeadowTotem(), 20, 28, { - depthPixels: filledDepthFromPixelsSized(drawMeadowTotem(), 20, 28) - }), - makeAssetSized('Twilight Caravan', 'static', 'ship', drawTwilightCaravan(), 32, 16, { - depthPixels: filledDepthFromPixelsSized(drawTwilightCaravan(), 32, 16), - lightPixels: [{ x: 9, y: 7 }, { x: 15, y: 7 }, { x: 22, y: 7 }], - lightColor: '#ffdba0' - }), - makeAssetSized('Pepper Fox', 'dynamic', 'animal', drawPepperFoxRight(), 16, 16, { - depthPixels: filledDepthFromPixelsSized(drawPepperFoxRight(), 16, 16) - }), - makeAssetSized('Glass Manta', 'dynamic', 'fish', drawGlassMantaRight(), 16, 12, { - depthPixels: filledDepthFromPixelsSized(drawGlassMantaRight(), 16, 12), - lightPixels: [{ x: 7, y: 5 }], - lightColor: '#9ef4ff' - }), - makeAssetSized('Festival Drummer', 'dynamic', 'human', drawFestivalDrummerRight(), 14, 16, { - depthPixels: filledDepthFromPixelsSized(drawFestivalDrummerRight(), 14, 16) - }), - makeAssetSized('Bloom Sprite', 'dynamic', 'animal', drawBloomSpriteRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawBloomSpriteRight(), 12, 12), - lightPixels: [{ x: 5, y: 3 }], - lightColor: '#ffe7a8' - }), - makeAssetSized('Aurora Bathhouse', 'static', 'building', drawAuroraBathhouse(), 28, 28, { - depthPixels: filledDepthFromPixelsSized(drawAuroraBathhouse(), 28, 28), - lightPixels: [{ x: 10, y: 17 }, { x: 17, y: 17 }, { x: 14, y: 22 }], - lightColor: '#baf6ff', - door: { x: 14, y: 24 } - }), - makeAssetSized('Copper Windmill', 'static', 'building', drawCopperWindmill(), 24, 32, { - depthPixels: filledDepthFromPixelsSized(drawCopperWindmill(), 24, 32), - lightPixels: [{ x: 11, y: 21 }], - lightColor: '#ffd48a', - door: { x: 11, y: 28 } - }), - makeAssetSized('Moon Gate Bridge', 'static', 'nature', drawMoonGateBridge(), 32, 20, { - depthPixels: filledDepthFromPixelsSized(drawMoonGateBridge(), 32, 20), - lightPixels: [{ x: 9, y: 9 }, { x: 22, y: 9 }], - lightColor: '#e5f6ff' - }), - makeAssetSized('Starlit Bookshop', 'static', 'building', drawStarlitBookshop(), 24, 28, { - depthPixels: filledDepthFromPixelsSized(drawStarlitBookshop(), 24, 28), - lightPixels: [{ x: 8, y: 14 }, { x: 15, y: 14 }, { x: 12, y: 7 }], - lightColor: '#ffe99a', - door: { x: 12, y: 24 } - }), - makeAssetSized('Crystal Bakery', 'static', 'building', drawCrystalBakery(), 24, 24, { - depthPixels: filledDepthFromPixelsSized(drawCrystalBakery(), 24, 24), - lightPixels: [{ x: 7, y: 14 }, { x: 16, y: 14 }], - lightColor: '#ffd8ef', - door: { x: 12, y: 21 } - }), - makeAssetSized('Kelp Observatory', 'static', 'building', drawKelpObservatory(), 28, 32, { - depthPixels: filledDepthFromPixelsSized(drawKelpObservatory(), 28, 32), - lightPixels: [{ x: 13, y: 9 }, { x: 14, y: 9 }, { x: 9, y: 19 }, { x: 18, y: 19 }], - lightColor: '#9ff4ff', - door: { x: 14, y: 28 } - }), - makeAssetSized('Ember Shrine', 'static', 'building', drawEmberShrine(), 20, 28, { - depthPixels: filledDepthFromPixelsSized(drawEmberShrine(), 20, 28), - lightPixels: [{ x: 10, y: 13 }, { x: 6, y: 20 }, { x: 14, y: 20 }], - lightColor: '#ffb36b', - door: { x: 10, y: 24 } - }), - makeAssetSized('Honeycomb Greenhouse', 'static', 'building', drawHoneycombGreenhouse(), 28, 24, { - depthPixels: filledDepthFromPixelsSized(drawHoneycombGreenhouse(), 28, 24), - lightPixels: [{ x: 8, y: 13 }, { x: 14, y: 10 }, { x: 20, y: 13 }], - lightColor: '#fff08a', - door: { x: 14, y: 21 } - }), - makeAssetSized('Celadon Pagoda', 'static', 'building', drawCeladonPagoda(), 24, 36, { - depthPixels: filledDepthFromPixelsSized(drawCeladonPagoda(), 24, 36), - lightPixels: [{ x: 11, y: 15 }, { x: 12, y: 15 }, { x: 11, y: 25 }, { x: 12, y: 25 }], - lightColor: '#e7ffb0', - door: { x: 12, y: 32 } - }), - makeAssetSized('Tide Pearl Arch', 'static', 'nature', drawTidePearlArch(), 24, 24, { - depthPixels: filledDepthFromPixelsSized(drawTidePearlArch(), 24, 24), - lightPixels: [{ x: 12, y: 8 }], - lightColor: '#e9fdff' - }), - makeAssetSized('Lavender Market', 'static', 'building', drawLavenderMarket(), 32, 20, { - depthPixels: filledDepthFromPixelsSized(drawLavenderMarket(), 32, 20), - lightPixels: [{ x: 8, y: 12 }, { x: 16, y: 12 }, { x: 24, y: 12 }], - lightColor: '#ffe0ff', - door: { x: 16, y: 17 } - }), - makeAssetSized('Skyseed Tower', 'static', 'building', drawSkyseedTower(), 24, 36, { - depthPixels: filledDepthFromPixelsSized(drawSkyseedTower(), 24, 36), - lightPixels: [{ x: 12, y: 6 }, { x: 11, y: 22 }, { x: 12, y: 22 }], - lightColor: '#cfffd7', - door: { x: 12, y: 32 } - }), - makeAssetSized('Orchard Terrace', 'static', 'nature', drawOrchardTerrace(), 32, 24, { - depthPixels: filledDepthFromPixelsSized(drawOrchardTerrace(), 32, 24), - particlePixels: [{ x: 9, y: 8, c: nearestPaletteCode('#ffd66e'), dir: 'up' }, { x: 23, y: 10, c: nearestPaletteCode('#ff7f6e'), dir: 'up' }] - }), - makeAssetSized('Dreamcatcher Tree', 'static', 'nature', drawDreamcatcherTree(), 28, 32, { - depthPixels: filledDepthFromPixelsSized(drawDreamcatcherTree(), 28, 32), - particlePixels: [{ x: 12, y: 9, c: nearestPaletteCode('#ead1ff'), dir: 'up' }, { x: 17, y: 11, c: nearestPaletteCode('#fff2b0'), dir: 'up' }] - }), - makeAssetSized('Nebula Fountain', 'static', 'nature', drawNebulaFountain(), 24, 24, { - depthPixels: filledDepthFromPixelsSized(drawNebulaFountain(), 24, 24), - lightPixels: [{ x: 11, y: 9 }, { x: 12, y: 9 }], - lightColor: '#b8e7ff', - particlePixels: [{ x: 9, y: 11, c: nearestPaletteCode('#dffbff'), dir: 'up' }, { x: 15, y: 11, c: nearestPaletteCode('#ead1ff'), dir: 'up' }] - }), - makeAssetSized('Coral Lighthouse', 'static', 'building', drawCoralLighthouse(), 24, 36, { - depthPixels: filledDepthFromPixelsSized(drawCoralLighthouse(), 24, 36), - lightPixels: [{ x: 11, y: 6 }, { x: 12, y: 6 }, { x: 13, y: 6 }], - lightColor: '#fff1a8', - door: { x: 12, y: 32 } - }), - makeAssetSized('Origami Wind Shrine', 'static', 'nature', drawOrigamiWindShrine(), 20, 24, { - depthPixels: filledDepthFromPixelsSized(drawOrigamiWindShrine(), 20, 24), - particlePixels: [{ x: 9, y: 8, c: nearestPaletteCode('#eef4ff'), dir: 'up' }, { x: 12, y: 6, c: nearestPaletteCode('#cfeaff'), dir: 'up' }] - }), - makeAssetSized('Rainmaker Drum', 'static', 'nature', drawRainmakerDrum(), 20, 20, { - depthPixels: filledDepthFromPixelsSized(drawRainmakerDrum(), 20, 20), - particlePixels: [{ x: 7, y: 5, c: nearestPaletteCode('#bfefff'), dir: 'down' }, { x: 12, y: 5, c: nearestPaletteCode('#dffbff'), dir: 'down' }] - }), - makeAssetSized('Velvet Mushroom Grove', 'static', 'nature', drawVelvetMushroomGrove(), 24, 22, { - depthPixels: filledDepthFromPixelsSized(drawVelvetMushroomGrove(), 24, 22), - lightPixels: [{ x: 7, y: 12 }, { x: 16, y: 10 }], - lightColor: '#ffb8f2' - }), - makeAssetSized('Star Map Monument', 'static', 'nature', drawStarMapMonument(), 20, 20, { - depthPixels: filledDepthFromPixelsSized(drawStarMapMonument(), 20, 20), - lightPixels: [{ x: 10, y: 5 }, { x: 6, y: 11 }, { x: 14, y: 12 }], - lightColor: '#dce8ff' - }), - makeAssetSized('Lantern Reef Gate', 'static', 'ship', drawLanternReefGate(), 32, 20, { - depthPixels: filledDepthFromPixelsSized(drawLanternReefGate(), 32, 20), - lightPixels: [{ x: 10, y: 9 }, { x: 21, y: 9 }], - lightColor: '#ffd98a' - }), - makeAssetSized('Ember Salamander', 'dynamic', 'animal', drawEmberSalamanderRight(), 14, 10, { - depthPixels: filledDepthFromPixelsSized(drawEmberSalamanderRight(), 14, 10), - particlePixels: [{ x: 2, y: 5, c: nearestPaletteCode('#ffb36b'), dir: 'up' }] - }), - makeAssetSized('Moon Snail', 'dynamic', 'animal', drawMoonSnailRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawMoonSnailRight(), 12, 10), - lightPixels: [{ x: 5, y: 4 }], - lightColor: '#eef4ff' - }), - makeAssetSized('Ribbon Dragonfly', 'dynamic', 'animal', drawRibbonDragonflyRight(), 14, 8, { - depthPixels: filledDepthFromPixelsSized(drawRibbonDragonflyRight(), 14, 8), - particlePixels: [{ x: 4, y: 3, c: nearestPaletteCode('#ead1ff'), dir: 'up' }] - }), - makeAssetSized('Teacup Automaton', 'dynamic', 'human', drawTeacupAutomatonRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawTeacupAutomatonRight(), 12, 16), - lightPixels: [{ x: 8, y: 7 }], - lightColor: '#ffe99a' - }), - makeAssetSized('Sapphire Crab', 'dynamic', 'fish', drawSapphireCrabRight(), 12, 8, { - depthPixels: filledDepthFromPixelsSized(drawSapphireCrabRight(), 12, 8), - particlePixels: [{ x: 2, y: 5, c: nearestPaletteCode('#c8f4ff'), dir: 'up' }] - }), - makeAssetSized('Cloud Alpaca', 'dynamic', 'animal', drawCloudAlpacaRight(), 14, 12, { - depthPixels: filledDepthFromPixelsSized(drawCloudAlpacaRight(), 14, 12) - }), - makeAssetSized('Starling Bard', 'dynamic', 'human', drawStarlingBardRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawStarlingBardRight(), 12, 16), - lightPixels: [{ x: 5, y: 5 }], - lightColor: '#ffe99a' - }), - makeAssetSized('Glass Jellyfish', 'dynamic', 'fish', drawGlassJellyfishRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawGlassJellyfishRight(), 12, 16), - lightPixels: [{ x: 6, y: 4 }], - lightColor: '#aef8ff', - particlePixels: [{ x: 5, y: 11, c: nearestPaletteCode('#c8f4ff'), dir: 'up' }] - }), - makeAssetSized('Copper Heron', 'dynamic', 'bird', drawCopperHeronRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawCopperHeronRight(), 12, 16) - }), - makeAssetSized('Paper Fox Kite', 'dynamic', 'animal', drawPaperFoxKiteRight(), 14, 12, { - depthPixels: filledDepthFromPixelsSized(drawPaperFoxKiteRight(), 14, 12), - particlePixels: [{ x: 4, y: 3, c: nearestPaletteCode('#eef4ff'), dir: 'up' }] - }), - makeAssetSized('Prism Seahorse', 'dynamic', 'fish', drawPrismSeahorseRight(), 10, 14, { - depthPixels: filledDepthFromPixelsSized(drawPrismSeahorseRight(), 10, 14), - lightPixels: [{ x: 5, y: 5 }], - lightColor: '#d7c6ff' - }), - makeAssetSized('Orchard Sprite', 'dynamic', 'animal', drawOrchardSpriteRight(), 10, 12, { - depthPixels: filledDepthFromPixelsSized(drawOrchardSpriteRight(), 10, 12), - particlePixels: [{ x: 5, y: 3, c: nearestPaletteCode('#ffd66e'), dir: 'up' }] - }), - makeAssetSized('Map Turtle', 'dynamic', 'animal', drawMapTurtleRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawMapTurtleRight(), 12, 10) - }), - makeAssetSized('Tea Sparrow', 'dynamic', 'bird', drawTeaSparrowRight(), 8, 8, { - depthPixels: filledDepthFromPixelsSized(drawTeaSparrowRight(), 8, 8) - }), - makeAssetSized('Bubble Diver', 'dynamic', 'human', drawBubbleDiverRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawBubbleDiverRight(), 12, 16), - lightPixels: [{ x: 6, y: 3 }], - lightColor: '#dffbff' - }), - makeAssetSized('Mist Eel', 'dynamic', 'fish', drawMistEelRight(), 14, 6, { - depthPixels: filledDepthFromPixelsSized(drawMistEelRight(), 14, 6), - particlePixels: [{ x: 2, y: 3, c: nearestPaletteCode('#dffbff'), dir: 'up' }] - }), - makeAssetSized('Ruby Beetle', 'dynamic', 'animal', drawRubyBeetleRight(), 10, 8, { - depthPixels: filledDepthFromPixelsSized(drawRubyBeetleRight(), 10, 8) - }), - makeAssetSized('Snowy Tanuki', 'dynamic', 'animal', drawSnowyTanukiRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawSnowyTanukiRight(), 12, 12) - }), - makeAssetSized('Lighthouse Keeper', 'dynamic', 'human', drawLighthouseKeeperRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawLighthouseKeeperRight(), 12, 16), - lightPixels: [{ x: 8, y: 7 }], - lightColor: '#fff1a8' - }), - makeAssetSized('Midnight Ramen Cart', 'static', 'building', drawMidnightRamenCart(), 28, 20, { - depthPixels: filledDepthFromPixelsSized(drawMidnightRamenCart(), 28, 20), - lightPixels: [{ x: 8, y: 10 }, { x: 18, y: 10 }, { x: 13, y: 15 }], - lightColor: '#ffd58a', - door: { x: 13, y: 17 } - }), - makeAssetSized('Quartz Clocktower', 'static', 'building', drawQuartzClocktower(), 22, 34, { - depthPixels: filledDepthFromPixelsSized(drawQuartzClocktower(), 22, 34), - lightPixels: [{ x: 10, y: 8 }, { x: 11, y: 8 }, { x: 10, y: 22 }, { x: 11, y: 22 }], - lightColor: '#e6fbff', - door: { x: 11, y: 31 } - }), - makeAssetSized('Lotus Tea Pavilion', 'static', 'building', drawLotusTeaPavilion(), 30, 24, { - depthPixels: filledDepthFromPixelsSized(drawLotusTeaPavilion(), 30, 24), - lightPixels: [{ x: 9, y: 14 }, { x: 20, y: 14 }, { x: 15, y: 18 }], - lightColor: '#ffe0f1', - door: { x: 15, y: 21 } - }), - makeAssetSized('Meteorite Garden', 'static', 'nature', drawMeteoriteGarden(), 26, 20, { - depthPixels: filledDepthFromPixelsSized(drawMeteoriteGarden(), 26, 20), - lightPixels: [{ x: 13, y: 8 }, { x: 7, y: 13 }, { x: 19, y: 14 }], - lightColor: '#b7e9ff', - particlePixels: [{ x: 13, y: 7, c: nearestPaletteCode('#dffbff'), dir: 'up' }, { x: 18, y: 9, c: nearestPaletteCode('#b7e9ff'), dir: 'up' }] - }), - makeAssetSized('Azure Glasshouse', 'static', 'building', drawAzureGlasshouse(), 28, 26, { - depthPixels: filledDepthFromPixelsSized(drawAzureGlasshouse(), 28, 26), - lightPixels: [{ x: 8, y: 15 }, { x: 14, y: 12 }, { x: 20, y: 15 }], - lightColor: '#b9f5ff', - door: { x: 14, y: 23 } - }), - makeAssetSized('Foxfire Torii', 'static', 'nature', drawFoxfireTorii(), 24, 24, { - depthPixels: filledDepthFromPixelsSized(drawFoxfireTorii(), 24, 24), - lightPixels: [{ x: 6, y: 14 }, { x: 17, y: 14 }, { x: 12, y: 8 }], - lightColor: '#ffb36b', - particlePixels: [{ x: 6, y: 13, c: nearestPaletteCode('#ff9e66'), dir: 'up' }, { x: 17, y: 13, c: nearestPaletteCode('#ffd58a'), dir: 'up' }] - }), - makeAssetSized('Pearl Cloud Altar', 'static', 'nature', drawPearlCloudAltar(), 24, 22, { - depthPixels: filledDepthFromPixelsSized(drawPearlCloudAltar(), 24, 22), - lightPixels: [{ x: 12, y: 9 }, { x: 8, y: 15 }, { x: 16, y: 15 }], - lightColor: '#f3fbff', - particlePixels: [{ x: 12, y: 8, c: nearestPaletteCode('#f3fbff'), dir: 'up' }] - }), - makeAssetSized('Clockwork Conservatory', 'static', 'building', drawClockworkConservatory(), 28, 28, { - depthPixels: filledDepthFromPixelsSized(drawClockworkConservatory(), 28, 28), - lightPixels: [{ x: 8, y: 16 }, { x: 14, y: 11 }, { x: 20, y: 16 }], - lightColor: '#ffe9a8', - door: { x: 14, y: 25 } - }), - makeAssetSized('Inkcap Mouse', 'dynamic', 'animal', drawInkcapMouseRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawInkcapMouseRight(), 12, 10), - particlePixels: [{ x: 4, y: 2, c: nearestPaletteCode('#cbb8ff'), dir: 'up' }] - }), - makeAssetSized('Comet Finch', 'dynamic', 'bird', drawCometFinchRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawCometFinchRight(), 12, 10), - lightPixels: [{ x: 8, y: 4 }], - lightColor: '#fff0a8' - }), - makeAssetSized('Lotus Kappa', 'dynamic', 'animal', drawLotusKappaRight(), 12, 14, { - depthPixels: filledDepthFromPixelsSized(drawLotusKappaRight(), 12, 14), - lightPixels: [{ x: 6, y: 3 }], - lightColor: '#d9ffd6' - }), - makeAssetSized('Lantern Manta', 'dynamic', 'fish', drawLanternMantaRight(), 14, 10, { - depthPixels: filledDepthFromPixelsSized(drawLanternMantaRight(), 14, 10), - lightPixels: [{ x: 7, y: 4 }, { x: 10, y: 6 }], - lightColor: '#aef8ff', - particlePixels: [{ x: 2, y: 5, c: nearestPaletteCode('#c8f4ff'), dir: 'up' }] - }), - makeAssetSized('Pocket Astronaut', 'dynamic', 'human', drawPocketAstronautRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawPocketAstronautRight(), 12, 16), - lightPixels: [{ x: 6, y: 4 }], - lightColor: '#e8fbff' - }), - makeAssetSized('Mossy Capybara', 'dynamic', 'animal', drawMossyCapybaraRight(), 14, 10, { - depthPixels: filledDepthFromPixelsSized(drawMossyCapybaraRight(), 14, 10), - particlePixels: [{ x: 6, y: 2, c: nearestPaletteCode('#a6d66b'), dir: 'up' }] - }), - makeAssetSized('Opal Octopus', 'dynamic', 'fish', drawOpalOctopusRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawOpalOctopusRight(), 12, 12), - lightPixels: [{ x: 6, y: 4 }], - lightColor: '#ffcaf0', - particlePixels: [{ x: 5, y: 10, c: nearestPaletteCode('#f3fbff'), dir: 'up' }] - }), - makeAssetSized('Tiny Oracle', 'dynamic', 'human', drawTinyOracleRight(), 10, 16, { - depthPixels: filledDepthFromPixelsSized(drawTinyOracleRight(), 10, 16), - lightPixels: [{ x: 5, y: 5 }], - lightColor: '#e6d6ff' - }), - makeAssetSized('Amber Apiary', 'static', 'building', drawAmberApiary(), 24, 24, { - depthPixels: filledDepthFromPixelsSized(drawAmberApiary(), 24, 24), - lightPixels: [{ x: 12, y: 10 }, { x: 7, y: 15 }, { x: 17, y: 15 }], - lightColor: '#ffd66e', - particlePixels: [{ x: 12, y: 6, c: nearestPaletteCode('#ffe99a'), dir: 'up' }] - }), - makeAssetSized('Glass Moon Dock', 'static', 'ship', drawGlassMoonDock(), 32, 16, { - depthPixels: filledDepthFromPixelsSized(drawGlassMoonDock(), 32, 16), - lightPixels: [{ x: 8, y: 5 }, { x: 24, y: 5 }], - lightColor: '#bdf7ff' - }), - makeAssetSized('Bluebell Chapel', 'static', 'building', drawBluebellChapel(), 22, 30, { - depthPixels: filledDepthFromPixelsSized(drawBluebellChapel(), 22, 30), - lightPixels: [{ x: 11, y: 12 }, { x: 10, y: 24 }], - lightColor: '#d8d2ff' - }), - makeAssetSized('Stardust Orchard', 'static', 'nature', drawStardustOrchard(), 28, 24, { - depthPixels: filledDepthFromPixelsSized(drawStardustOrchard(), 28, 24), - lightPixels: [{ x: 8, y: 7 }, { x: 17, y: 6 }, { x: 22, y: 12 }], - lightColor: '#fff4a8', - particlePixels: [{ x: 17, y: 6, c: nearestPaletteCode('#fff4a8'), dir: 'up' }] - }), - makeAssetSized('Prism Ferris Wheel', 'static', 'building', drawPrismFerrisWheel(), 32, 32, { - depthPixels: filledDepthFromPixelsSized(drawPrismFerrisWheel(), 32, 32), - lightPixels: [{ x: 16, y: 9 }, { x: 9, y: 15 }, { x: 23, y: 15 }], - lightColor: '#c7f0ff' - }), - makeAssetSized('Moss Lantern Gate', 'static', 'nature', drawMossLanternGate(), 26, 24, { - depthPixels: filledDepthFromPixelsSized(drawMossLanternGate(), 26, 24), - lightPixels: [{ x: 8, y: 11 }, { x: 18, y: 11 }], - lightColor: '#b8ff9a' - }), - makeAssetSized('Shell Music Stage', 'static', 'building', drawShellMusicStage(), 30, 22, { - depthPixels: filledDepthFromPixelsSized(drawShellMusicStage(), 30, 22), - lightPixels: [{ x: 9, y: 10 }, { x: 20, y: 10 }], - lightColor: '#ffd0f0' - }), - makeAssetSized('Terracotta Bathhouse', 'static', 'building', drawTerracottaBathhouse(), 28, 26, { - depthPixels: filledDepthFromPixelsSized(drawTerracottaBathhouse(), 28, 26), - lightPixels: [{ x: 14, y: 15 }, { x: 10, y: 21 }, { x: 18, y: 21 }], - lightColor: '#ffbd7a' - }), - makeAssetSized('Copper Hedgehog', 'dynamic', 'animal', drawCopperHedgehogRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawCopperHedgehogRight(), 12, 10) - }), - makeAssetSized('Lily Crane', 'dynamic', 'bird', drawLilyCraneRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawLilyCraneRight(), 12, 16), - lightPixels: [{ x: 5, y: 4 }], - lightColor: '#f4f8ff' - }), - makeAssetSized('Pearl Angler', 'dynamic', 'human', drawPearlAnglerRight(), 12, 16, { - depthPixels: filledDepthFromPixelsSized(drawPearlAnglerRight(), 12, 16), - lightPixels: [{ x: 10, y: 6 }], - lightColor: '#dffbff' - }), - makeAssetSized('Neon Guppy', 'dynamic', 'fish', drawNeonGuppyRight(), 12, 8, { - depthPixels: filledDepthFromPixelsSized(drawNeonGuppyRight(), 12, 8), - lightPixels: [{ x: 7, y: 3 }], - lightColor: '#5ff0ff' - }), - makeAssetSized('Cloud Jelly Pup', 'dynamic', 'animal', drawCloudJellyPupRight(), 12, 12, { - depthPixels: filledDepthFromPixelsSized(drawCloudJellyPupRight(), 12, 12), - lightPixels: [{ x: 5, y: 4 }], - lightColor: '#e4f8ff' - }), - makeAssetSized('Ember Firefly', 'dynamic', 'animal', drawEmberFireflyRight(), 10, 8, { - depthPixels: filledDepthFromPixelsSized(drawEmberFireflyRight(), 10, 8), - lightPixels: [{ x: 7, y: 3 }], - lightColor: '#ffb15a', - particlePixels: [{ x: 7, y: 3, c: nearestPaletteCode('#ffb15a'), dir: 'up' }] - }), - makeAssetSized('Teal Seahare', 'dynamic', 'fish', drawTealSeahareRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawTealSeahareRight(), 12, 10), - lightPixels: [{ x: 8, y: 4 }], - lightColor: '#8ff6d1' - }), - makeAssetSized('Honeybee Courier', 'dynamic', 'bird', drawHoneybeeCourierRight(), 12, 10, { - depthPixels: filledDepthFromPixelsSized(drawHoneybeeCourierRight(), 12, 10), - lightPixels: [{ x: 8, y: 4 }], - lightColor: '#ffe66d' - }), - ]; - - const idByName = Object.fromEntries(assets.map((a) => [a.name, a.id])); - const assetById = new Map(assets.map((asset) => [asset.id, asset])); - 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 keepOnlyDisplayedSeedAssets = (gallery) => { - const isAllowedSeedAssetId = (assetId) => DEFAULT_GALLERY_DISPLAY_ASSET_NAMES.has(assetById.get(assetId)?.name || ''); - const placed = (gallery.placed || []) - .filter((object) => isAllowedSeedAssetId(object.assetId)) - .map(normalizeSeedPlacementTerrain) - .map((object) => ({ ...object, ownerAccountId: '' })); - const dynamicSummons = (gallery.dynamicSummons || []) - .filter((object) => isAllowedSeedAssetId(object.assetId)) - .map((object) => ({ ...object, ownerAccountId: '' })); - const displayedAssetIds = new Set([...placed, ...dynamicSummons].map((object) => object.assetId).filter(Boolean)); - return { - ...gallery, - assets: gallery.assets - .filter((asset) => displayedAssetIds.has(asset.id) && DEFAULT_GALLERY_DISPLAY_ASSET_NAMES.has(asset.name)) - .map((asset) => ({ ...asset, author: 'Local Artist', ownerAccountId: '' })), - placed, - dynamicSummons - }; - }; - const whalePos = findNearestTerrain('water', 62, 58); - const koiPos = findNearestTerrain('water', 66, 60); - const skiffPos = findNearestTerrain('water', 56, 58); - const minnowPos = findNearestTerrain('water', 71, 57); - const turtlePos = findNearestTerrain('water', 53, 60); - const sunsetKoiPos = findNearestTerrain('water', 60, 61); - const troutPos = findNearestTerrain('water', 75, 56); - const butterflyFishPos = findNearestTerrain('water', 68, 63); - const starreefPos = findNearestTerrain('water', 118, 84); - const lanternReefGatePos = findNearestTerrain('water', 118, 96); - const sapphireCrabPos = findNearestTerrain('water', 123, 76); - const glassJellyfishPos = findNearestTerrain('water', 124, 88); - const prismSeahorsePos = findNearestTerrain('water', 116, 92); - const mistEelPos = findNearestTerrain('water', 131, 96); - const glassMoonDockPos = findNearestTerrain('water', 104, 92); - const neonGuppyPos = findNearestTerrain('water', 110, 88); - const tealSeaharePos = findNearestTerrain('water', 132, 88); - return keepOnlyDisplayedSeedAssets({ - assets, - placed: [ - { id: uid(), assetId: idByName['Crescent Tea House'], ownerAccountId: 'island-team', x: 36, y: 36, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Prism Sakura'], ownerAccountId: 'island-team', x: 32, y: 38, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Clockwork Whale'], ownerAccountId: 'island-team', ...whalePos, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Moon Lantern'], ownerAccountId: 'island-team', x: 41, y: 37, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Glass Fern'], ownerAccountId: 'island-team', x: 29, y: 40, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Linen Cottage'], ownerAccountId: 'island-team', x: 45, y: 40, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Pine Cluster'], ownerAccountId: 'island-team', x: 47, y: 34, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Shell Rock'], ownerAccountId: 'island-team', x: 40, y: 43, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Coral Skiff'], ownerAccountId: 'island-team', ...skiffPos, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Wildflower Patch'], ownerAccountId: 'island-team', x: 50, y: 39, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['River Stones'], ownerAccountId: 'island-team', x: 52, y: 41, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Maple Canopy'], ownerAccountId: 'island-team', x: 25, y: 34, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Misty Falls'], ownerAccountId: 'island-team', x: 59, y: 33, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lotus Pond'], ownerAccountId: 'island-team', x: 53, y: 36, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Mossy Arch'], ownerAccountId: 'island-team', x: 20, y: 38, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Sunflower Grove'], ownerAccountId: 'island-team', x: 24, y: 43, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Reed Bed'], ownerAccountId: 'island-team', x: 58, y: 40, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Harbor Clocktower'], ownerAccountId: 'island-team', x: 108, y: 34, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Glass Greenhouse'], ownerAccountId: 'island-team', x: 87, y: 38, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Steam Workshop'], ownerAccountId: 'island-team', x: 78, y: 47, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Canal Bridge'], ownerAccountId: 'island-team', x: 84, y: 60, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Grand Fountain'], ownerAccountId: 'island-team', x: 96, y: 52, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Rocket Monument'], ownerAccountId: 'island-team', x: 117, y: 46, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Arcade Booth'], ownerAccountId: 'island-team', x: 101, y: 58, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Chess Knight Statue'], ownerAccountId: 'island-team', x: 92, y: 53, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Desert Train'], ownerAccountId: 'island-team', x: 23, y: 68, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Great Cedar'], ownerAccountId: 'island-team', x: 16, y: 39, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Moonfall Cascade'], ownerAccountId: 'island-team', x: 72, y: 35, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Sunblossom Gate'], ownerAccountId: 'island-team', x: 34, y: 47, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Echo Cavern'], ownerAccountId: 'island-team', x: 98, y: 42, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Worldroot Shrine'], ownerAccountId: 'island-team', x: 58, y: 31, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Moon Jelly Aquarium'], ownerAccountId: 'island-team', x: 112, y: 38, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Aurora Observatory'], ownerAccountId: 'island-team', x: 70, y: 26, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Hearthwind Mill'], ownerAccountId: 'island-team', x: 16, y: 56, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Tideglass Lighthouse'], ownerAccountId: 'island-team', x: 122, y: 28, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Mossrail Depot'], ownerAccountId: 'island-team', x: 104, y: 64, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lantern Library'], ownerAccountId: 'island-team', x: 38, y: 29, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Velvet Theater'], ownerAccountId: 'island-team', x: 86, y: 31, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Bloom Bridge Gate'], ownerAccountId: 'island-team', x: 73, y: 81, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Starreef Submarine'], ownerAccountId: 'island-team', ...starreefPos, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Smile Portrait'], ownerAccountId: 'island-team', x: 13, y: 55, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Great Wave Panel'], ownerAccountId: 'island-team', x: 68, y: 51, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Sunflower Still Life'], ownerAccountId: 'island-team', x: 34, y: 80, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Rosetta Stela'], ownerAccountId: 'island-team', x: 122, y: 74, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Pharaoh Mask'], ownerAccountId: 'island-team', x: 93, y: 27, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Stone Circle'], ownerAccountId: 'island-team', x: 73, y: 87, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Terracotta Sentinel'], ownerAccountId: 'island-team', x: 111, y: 69, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Rain Bell Tower'], ownerAccountId: 'island-team', x: 126, y: 43, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Crimson Pagoda'], ownerAccountId: 'island-team', x: 26, y: 35, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Meteor Forge'], ownerAccountId: 'island-team', x: 47, y: 66, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Ink Garden Screen'], ownerAccountId: 'island-team', x: 86, y: 77, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Meadow Totem'], ownerAccountId: 'island-team', x: 22, y: 76, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Twilight Caravan'], ownerAccountId: 'island-team', x: 18, y: 90, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Aurora Bathhouse'], ownerAccountId: 'island-team', x: 78, y: 30, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Copper Windmill'], ownerAccountId: 'island-team', x: 20, y: 49, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Moon Gate Bridge'], ownerAccountId: 'island-team', x: 64, y: 77, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Starlit Bookshop'], ownerAccountId: 'island-team', x: 93, y: 40, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Crystal Bakery'], ownerAccountId: 'island-team', x: 102, y: 51, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Kelp Observatory'], ownerAccountId: 'island-team', x: 116, y: 54, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Ember Shrine'], ownerAccountId: 'island-team', x: 30, y: 63, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Honeycomb Greenhouse'], ownerAccountId: 'island-team', x: 48, y: 51, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Celadon Pagoda'], ownerAccountId: 'island-team', x: 38, y: 66, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Tide Pearl Arch'], ownerAccountId: 'island-team', x: 125, y: 70, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lavender Market'], ownerAccountId: 'island-team', x: 83, y: 70, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Skyseed Tower'], ownerAccountId: 'island-team', x: 15, y: 80, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Orchard Terrace'], ownerAccountId: 'island-team', x: 56, y: 84, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Dreamcatcher Tree'], ownerAccountId: 'island-team', x: 30, y: 84, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Nebula Fountain'], ownerAccountId: 'island-team', x: 78, y: 50, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Coral Lighthouse'], ownerAccountId: 'island-team', x: 128, y: 60, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Origami Wind Shrine'], ownerAccountId: 'island-team', x: 66, y: 36, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Rainmaker Drum'], ownerAccountId: 'island-team', x: 99, y: 82, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Velvet Mushroom Grove'], ownerAccountId: 'island-team', x: 45, y: 76, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Star Map Monument'], ownerAccountId: 'island-team', x: 111, y: 31, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lantern Reef Gate'], ownerAccountId: 'island-team', ...lanternReefGatePos, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Midnight Ramen Cart'], ownerAccountId: 'island-team', x: 90, y: 63, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Quartz Clocktower'], ownerAccountId: 'island-team', x: 58, y: 42, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lotus Tea Pavilion'], ownerAccountId: 'island-team', x: 69, y: 91, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Meteorite Garden'], ownerAccountId: 'island-team', x: 105, y: 35, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Azure Glasshouse'], ownerAccountId: 'island-team', x: 51, y: 57, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Foxfire Torii'], ownerAccountId: 'island-team', x: 27, y: 72, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Pearl Cloud Altar'], ownerAccountId: 'island-team', x: 115, y: 77, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Clockwork Conservatory'], ownerAccountId: 'island-team', x: 42, y: 44, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Amber Apiary'], ownerAccountId: 'island-team', x: 61, y: 69, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Glass Moon Dock'], ownerAccountId: 'island-team', ...glassMoonDockPos, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Bluebell Chapel'], ownerAccountId: 'island-team', x: 81, y: 83, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Stardust Orchard'], ownerAccountId: 'island-team', x: 35, y: 91, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Prism Ferris Wheel'], ownerAccountId: 'island-team', x: 106, y: 73, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Moss Lantern Gate'], ownerAccountId: 'island-team', x: 50, y: 88, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Shell Music Stage'], ownerAccountId: 'island-team', x: 88, y: 86, placedAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Terracotta Bathhouse'], ownerAccountId: 'island-team', x: 116, y: 66, placedAt: now, publishedAt: now, version: 1 } - ], - dynamicSummons: [ - { id: uid(), assetId: idByName['Lantern Cat'], ownerAccountId: 'island-team', homeX: 38, homeY: 39, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Cloud Koi'], ownerAccountId: 'island-team', ...homeFromPos(koiPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Paper Crane'], ownerAccountId: 'island-team', homeX: 91, homeY: 31, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Sprout Fox'], ownerAccountId: 'island-team', homeX: 46, homeY: 40, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Violet Moth'], ownerAccountId: 'island-team', homeX: 31, homeY: 35, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lantern Walker'], ownerAccountId: 'island-team', homeX: 34, homeY: 34, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Azure Minnow'], ownerAccountId: 'island-team', ...homeFromPos(minnowPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Firefly Swirl'], ownerAccountId: 'island-team', homeX: 26, homeY: 37, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Meadow Hare'], ownerAccountId: 'island-team', homeX: 43, homeY: 46, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Brook Turtle'], ownerAccountId: 'island-team', ...homeFromPos(turtlePos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Leaf Sparrow'], ownerAccountId: 'island-team', homeX: 23, homeY: 34, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Red Panda'], ownerAccountId: 'island-team', homeX: 27, homeY: 41, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['River Otter'], ownerAccountId: 'island-team', homeX: 62, homeY: 42, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Amber Deer'], ownerAccountId: 'island-team', homeX: 18, homeY: 46, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Forest Owl'], ownerAccountId: 'island-team', homeX: 21, homeY: 33, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Kingfisher'], ownerAccountId: 'island-team', homeX: 57, homeY: 39, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Pond Duck'], ownerAccountId: 'island-team', homeX: 54, homeY: 38, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Heron'], ownerAccountId: 'island-team', homeX: 64, homeY: 36, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Sunset Koi'], ownerAccountId: 'island-team', ...homeFromPos(sunsetKoiPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Silver Trout'], ownerAccountId: 'island-team', ...homeFromPos(troutPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Butterfly Fish'], ownerAccountId: 'island-team', ...homeFromPos(butterflyFishPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Town Gardener'], ownerAccountId: 'island-team', homeX: 89, homeY: 39, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lantern Courier'], ownerAccountId: 'island-team', homeX: 110, homeY: 46, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Plaza Musician'], ownerAccountId: 'island-team', homeX: 97, homeY: 53, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Bridge Mechanic'], ownerAccountId: 'island-team', homeX: 84, homeY: 58, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Tea Robot'], ownerAccountId: 'island-team', homeX: 104, homeY: 59, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Balloon Vendor'], ownerAccountId: 'island-team', homeX: 98, homeY: 58, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Jelly Comet'], ownerAccountId: 'island-team', homeX: 112, homeY: 53, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Courier Bike'], ownerAccountId: 'island-team', homeX: 88, homeY: 60, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Astrolabe Courier'], ownerAccountId: 'island-team', homeX: 72, homeY: 28, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Puddle Toad'], ownerAccountId: 'island-team', homeX: 74, homeY: 79, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Ribbon Swan'], ownerAccountId: 'island-team', homeX: 56, homeY: 37, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lantern Beetle'], ownerAccountId: 'island-team', homeX: 40, homeY: 30, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Pepper Fox'], ownerAccountId: 'island-team', homeX: 28, homeY: 71, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Glass Manta'], ownerAccountId: 'island-team', homeX: 122, homeY: 86, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Festival Drummer'], ownerAccountId: 'island-team', homeX: 52, homeY: 67, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Bloom Sprite'], ownerAccountId: 'island-team', homeX: 92, homeY: 79, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Ember Salamander'], ownerAccountId: 'island-team', homeX: 50, homeY: 70, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Moon Snail'], ownerAccountId: 'island-team', homeX: 31, homeY: 90, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Ribbon Dragonfly'], ownerAccountId: 'island-team', homeX: 42, homeY: 54, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Teacup Automaton'], ownerAccountId: 'island-team', homeX: 105, homeY: 49, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Sapphire Crab'], ownerAccountId: 'island-team', ...homeFromPos(sapphireCrabPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Cloud Alpaca'], ownerAccountId: 'island-team', homeX: 21, homeY: 60, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Starling Bard'], ownerAccountId: 'island-team', homeX: 88, homeY: 72, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Glass Jellyfish'], ownerAccountId: 'island-team', ...homeFromPos(glassJellyfishPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Copper Heron'], ownerAccountId: 'island-team', homeX: 64, homeY: 72, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Paper Fox Kite'], ownerAccountId: 'island-team', homeX: 73, homeY: 36, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Prism Seahorse'], ownerAccountId: 'island-team', ...homeFromPos(prismSeahorsePos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Orchard Sprite'], ownerAccountId: 'island-team', homeX: 55, homeY: 82, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Map Turtle'], ownerAccountId: 'island-team', homeX: 75, homeY: 79, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Tea Sparrow'], ownerAccountId: 'island-team', homeX: 38, homeY: 32, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Bubble Diver'], ownerAccountId: 'island-team', homeX: 120, homeY: 69, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Mist Eel'], ownerAccountId: 'island-team', ...homeFromPos(mistEelPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Ruby Beetle'], ownerAccountId: 'island-team', homeX: 28, homeY: 49, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Snowy Tanuki'], ownerAccountId: 'island-team', homeX: 16, homeY: 88, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lighthouse Keeper'], ownerAccountId: 'island-team', homeX: 124, homeY: 43, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Inkcap Mouse'], ownerAccountId: 'island-team', homeX: 44, homeY: 76, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Comet Finch'], ownerAccountId: 'island-team', homeX: 76, homeY: 33, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lotus Kappa'], ownerAccountId: 'island-team', homeX: 69, homeY: 88, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lantern Manta'], ownerAccountId: 'island-team', homeX: 127, homeY: 82, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Pocket Astronaut'], ownerAccountId: 'island-team', homeX: 108, homeY: 37, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Mossy Capybara'], ownerAccountId: 'island-team', homeX: 60, homeY: 79, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Opal Octopus'], ownerAccountId: 'island-team', homeX: 119, homeY: 90, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Tiny Oracle'], ownerAccountId: 'island-team', homeX: 95, homeY: 74, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Copper Hedgehog'], ownerAccountId: 'island-team', homeX: 65, homeY: 72, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Lily Crane'], ownerAccountId: 'island-team', homeX: 80, homeY: 82, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Pearl Angler'], ownerAccountId: 'island-team', homeX: 106, homeY: 91, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Neon Guppy'], ownerAccountId: 'island-team', ...homeFromPos(neonGuppyPos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Cloud Jelly Pup'], ownerAccountId: 'island-team', homeX: 39, homeY: 88, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Ember Firefly'], ownerAccountId: 'island-team', homeX: 57, homeY: 72, createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Teal Seahare'], ownerAccountId: 'island-team', ...homeFromPos(tealSeaharePos), createdAt: now, publishedAt: now, version: 1 }, - { id: uid(), assetId: idByName['Honeybee Courier'], ownerAccountId: 'island-team', homeX: 63, homeY: 66, createdAt: now, publishedAt: now, version: 1 } - ] - }); - } + // Default gallery contents are seeded by the shared backend from _data/default_gallery_seed.json. function mergeDefaultGallery(baseState) { - if (!baseState || typeof baseState !== 'object') return seedState(); - const seeded = buildDefaultGalleryPack(Date.now()); - 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 resurrectedDefaultAssetIds = new Set(baseState.assets - .filter((asset) => asset?.meta?.galleryOrigin === 'default-gallery' && !DEFAULT_GALLERY_DISPLAY_ASSET_NAMES.has(asset.name)) - .map((asset) => asset.id)); - if (resurrectedDefaultAssetIds.size) { - const resurrectedDefaultObjectIds = new Set([ - ...(baseState.placed || []), - ...(baseState.dynamicSummons || []) - ].filter((object) => resurrectedDefaultAssetIds.has(object.assetId)).map((object) => object.id)); - baseState.assets = baseState.assets.filter((asset) => !resurrectedDefaultAssetIds.has(asset.id)); - baseState.placed = baseState.placed.filter((object) => !resurrectedDefaultAssetIds.has(object.assetId)); - baseState.dynamicSummons = baseState.dynamicSummons.filter((object) => !resurrectedDefaultAssetIds.has(object.assetId)); - for (const assetId of resurrectedDefaultAssetIds) { - delete baseState.hiddenAssets?.[assetId]; - delete baseState.assetVotes?.[assetId]; - } - for (const objectId of resurrectedDefaultObjectIds) { - delete baseState.hiddenObjects?.[objectId]; - delete baseState.objectVotes?.[objectId]; - } - } - const assetByName = new Map(baseState.assets.map((asset) => [asset.name, asset])); - const assetIdByName = new Map(baseState.assets.map((asset) => [asset.name, asset.id])); - const deletedAssetKeys = new Set(); - for (const tombstone of Object.values(baseState.tombstones?.assets || {})) { - if (tombstone?.contentHash) deletedAssetKeys.add(`hash:${tombstone.contentHash}`); - if (tombstone?.name) deletedAssetKeys.add(`name:${tombstone.name}`); - } - for (const name of baseState.deletedSeedAssetNames) deletedAssetKeys.add(`name:${name}`); - const isDeletedSeedAsset = (asset) => deletedAssetKeys.has(`hash:${asset.contentHash || computeAssetContentHash(asset)}`) || deletedAssetKeys.has(`name:${asset.name}`); - for (const seededAsset of seeded.assets) { - if (isDeletedSeedAsset(seededAsset)) continue; - const existing = assetByName.get(seededAsset.name); - if (!existing) { - baseState.assets.push(seededAsset); - assetByName.set(seededAsset.name, seededAsset); - assetIdByName.set(seededAsset.name, seededAsset.id); - continue; - } - 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).map((item) => item.assetId)); - for (const item of seeded.placed) { - const name = seededNameById.get(item.assetId); - const resolvedAssetId = assetIdByName.get(name); - if (resolvedAssetId && !hasPlacedAsset.has(resolvedAssetId)) { - baseState.placed.push({ ...item, id: uid(), assetId: resolvedAssetId }); - hasPlacedAsset.add(resolvedAssetId); - } - } - 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); - if (resolvedAssetId && !hasDynamicAsset.has(resolvedAssetId)) { - baseState.dynamicSummons.push({ ...item, id: uid(), assetId: resolvedAssetId }); - hasDynamicAsset.add(resolvedAssetId); - } - } - return baseState; + return baseState && typeof baseState === 'object' ? baseState : seedState(); } + function seedState() { const now = Date.now(); - const gallery = buildDefaultGalleryPack(now); return { schema: SAVE_SCHEMA, authorName: 'Local Artist', - assets: gallery.assets, - placed: gallery.placed, + assets: [], + placed: [], objectVotes: {}, assetVotes: {}, hiddenAssets: {}, @@ -7716,6 +6807,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { settings: defaultVisualSettings(), account: null, publishLog: [], + pendingPublishLog: [], worldMode: 'local', serverSync: { lastServerEventId: null, @@ -7726,50 +6818,12 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { }, tombstones: { assets: {}, objects: {} }, deletedSeedAssetNames: [], - dynamicSummons: gallery.dynamicSummons + dynamicSummons: [] }; } - function makeAsset(name, category, subtype, pixels, meta = {}, leftPixels = null) { - return makeAssetSized(name, category, subtype, pixels, 16, 16, meta, leftPixels); - } - - function makeAssetSized(name, category, subtype, pixels, width = 16, height = width, meta = {}, leftPixels = null) { - const w = clampDimension(width, 16); - const h = clampDimension(height, w); - const size = Math.max(w, h); - const id = uid(); - const right = alignPixelsToBottomRect(normalizePixels(pixels, w, h), w, h); - const left = leftPixels ? alignPixelsToBottomRect(normalizePixels(leftPixels, w, h), w, h) : null; - const encodedRight = encodePixels(right, w, h); - const pixelBlob = buildPixelBlob(encodedRight, w, h); - const asset = { - id, - name, - category, - subtype, - size, - width: w, - height: h, - blobId: pixelBlob.id, - pixels: encodedRight, - faces: category === 'dynamic' ? { right: encodedRight, left: left ? encodePixels(left, w, h) : 'mirror' } : null, - parentAssetId: null, - originalAssetId: null, - createdAt: Date.now(), - updatedAt: Date.now(), - 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), - galleryOrigin: 'default-gallery' - } - }; - asset.contentHash = computeAssetContentHash(asset); - return asset; - } + // Default asset construction lives in the backend seed data, not app.js. function alignPixelsToBottom(pixels, size) { return alignPixelsToBottomRect(pixels, size, size); @@ -7920,6 +6974,13 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { } function cachePhase2Snapshot() { + if (Phase2Sync?.cachePixelBlobs) { + const blobs = (state.assets || []).map((asset) => buildPixelBlob(asset.faces?.right || asset.pixels || blankPixels(assetWidth(asset), assetHeight(asset)), assetWidth(asset), assetHeight(asset))); + Phase2Sync.cachePixelBlobs(blobs).catch((error) => console.warn('Phase 2 pixel blob cache failed.', error)); + } + if (Phase2Sync?.cacheAssets) { + Phase2Sync.cacheAssets(state.assets || []).catch((error) => console.warn('Phase 2 asset cache failed.', error)); + } if (Phase2Sync?.makeSnapshot && Phase2Sync?.cacheSnapshot) { Phase2Sync.cacheSnapshot(Phase2Sync.makeSnapshot(state)).catch((error) => console.warn('Phase 2 snapshot cache failed.', error)); } @@ -8659,3537 +7720,7 @@ function drawShipRipples(pos, time, seedValue = '', asset = null) { } } - function artRows(rows, legend) { - return artRowsSized(rows, legend, 16, 16); - } - - function artRowsSized(rows, legend, width, height = width) { - const w = clampDimension(width, 16); - const h = clampDimension(height, w); - const out = blankPixels(w, h); - for (let y = 0; y < h; y++) { - const row = String(rows[y] || '').padEnd(w, '.').slice(0, w); - for (let x = 0; x < w; x++) { - const value = legend[row[x]]; - if (value) out[y * w + x] = value; - } - } - return out; - } - - function depthRows(rows) { - return depthRowsSized(rows, 16, 16); - } - - function depthRowsSized(rows, width, height = width) { - const w = clampDimension(width, 16); - const h = clampDimension(height, w); - const out = Array(w * h).fill(0); - for (let y = 0; y < h; y++) { - const row = String(rows[y] || '').padEnd(w, '.').slice(0, w); - for (let x = 0; x < w; x++) { - const ch = row[x]; - if (ch === '^' || ch === '+') out[y * w + x] = 1; - if (ch === 'v' || ch === '-') out[y * w + x] = -1; - } - } - return out; - } - - function drawCrescentTeaHouse() { - return artRows([ - '.......YY.......', - '......YWWY......', - '.....YWWY.......', - '......YY........', - '....NNNNNNN.....', - '...NMMMMMMMN....', - '..NMMYMMYMMN....', - '..MMMWMMWMMM....', - '..MMMMMMMMMM....', - '..MMMYDDYMMM....', - '..MMMDYYDMMM....', - '..MMMDYYDMMM....', - '...MMDDDDMM.....', - '....NNNNNN......', - '....MMDDMM......', - '...NNNNNNNN.....' - ], { - Y: '#ffe55d', W: '#fffdf7', N: '#31333d', M: '#95806b', D: '#222632' - }); - } - - function drawMoonJellyAquarium() { - return artRowsSized([ - '...........YY...........', - '..........YCCY..........', - '.........YNCCNY.........', - '........NQBBBBQN........', - '.......NQBAAAABQN.......', - '......NQBAACCAABQN......', - '......NBAAAAAAAABN......', - '.....NQBAAPPPPAABQN.....', - '.....NBAAPPYYPPAABN.....', - '....NQBAAPYYYYPPAABQN...', - '....NBAAPYYYYYYPPAABN...', - '....NBAAPYYYYYYPPAABN...', - '...NQBAAPYYYYYYPPAABQN..', - '...N.BAAPQQQQQQPPAAB.N..', - '...N.NYYQNNNNNNQYYN.N...', - '...N.TSYQY....YQYST.N...', - '....NTTSY......YSTTN....', - '....NSSSY......YSSSN....', - '....NSSSY..DD..YSSSN....', - '....NSSSY.DDDD.YSSSN....', - '....NSSSYYYYYYYYSSSN....', - '....NSSSTTTTTTTTSSSN....', - '....NTTTTSDDDDSTTTTN....', - '.....NTTTSD...DSTTTN....', - '.....NDDDSS...SSDDDN....', - '......NDDDDNNNDDDDN.....', - '.......NNNNNNNNNNN......', - '........................' - ], { - N: '#1e2030', - Y: '#ffe8a1', - C: '#f7ffff', - Q: '#9b7dff', - B: '#6dd8f0', - A: '#a6f5ff', - P: '#ff8cc8', - T: '#5f5968', - S: '#8a7f91', - D: '#3c3348' - }, 24, 28); - } - - function drawMoonJellyAquariumDepth() { - return depthRowsSized([ - '...........^^...........', - '..........^^^^..........', - '.........^^^^^^.........', - '........^^^^^^^^........', - '.......^^^^^^^^^^.......', - '......^^^^^^^^^^^^......', - '......^^^^^^^^^^^^......', - '.....^^^^^^..^^^^^^.....', - '.....^^^^^....^^^^^.....', - '....^^^^^^....^^^^^^....', - '....^^^^^^....^^^^^^....', - '....^^^^^^....^^^^^^....', - '...^^^^^^^....^^^^^^^...', - '...^^^^^^......^^^^^^...', - '...^^..^^......^^..^^...', - '...^^..^^......^^..^^...', - '....^^..^......^..^^....', - '....^^..^......^..^^....', - '....^^^^^..--..^^^^^....', - '....^^^^^.----.^^^^^....', - '....^^^^^^^^^^^^^^^^....', - '....^^^^^------^^^^^....', - '....^^^^^------^^^^^....', - '.....^^^^------^^^^.....', - '.....^^^^^....^^^^^.....', - '......^^^^^..^^^^^......', - '.......^^^^^^^^^^.......', - '........................' - ], 24, 28); - } - - function drawCrescentTeaHouseDepth() { - return depthRows([ - '.......^^.......', - '......^^^^......', - '.....^^^^.......', - '......^^........', - '....-------.....', - '...-^^^^^^^-....', - '..-^^^^^^^^-....', - '..-^^^^^^^^-....', - '..-^^^^^^^^-....', - '..-^^^--^^^-....', - '..-^^^--^^^-....', - '..-^^^--^^^-....', - '...--------.....', - '....------......', - '....--^^--......', - '...--------.....' - ]); - } - - function drawPrismSakura() { - return artRows([ - '................', - '......PP.P......', - '....PPRRPPP.....', - '...PRRPPPRP.....', - '..PPPRPPRPPP....', - '...PPRRRPP......', - '.....PRP........', - '......T.........', - '.....GTG........', - '....GTTTG.......', - '...GGTTTGG......', - '.....TTT........', - '.....TNT........', - '....NNNNN.......', - '...NNNNNNN......', - '................' - ], { - P: '#ffb0c7', R: '#ef6b88', T: '#75614f', G: '#57cf78', N: '#454048' - }); - } - - function drawPrismSakuraDepth() { - return depthRows([ - '................', - '......^^.^......', - '....^^^^^^^.....', - '...^^^^^^^^.....', - '..^^^^^^^^^^....', - '...^^^^^^^......', - '.....^^^........', - '......-.........', - '.....---........', - '....-----.......', - '...-------......', - '.....---........', - '.....---........', - '....-----.......', - '...-------......', - '................' - ]); - } - - function drawClockworkWhale() { - return artRows([ - '................', - '................', - '.......Y........', - '......YY........', - '...BBQQQQQB.....', - '..BQQQQQQQQB....', - '.BQQQWQQQWQB....', - '.BQQQQQQQQQB.Y..', - '..BQQQQQQQB.YY..', - '...BBBBBBB.YY...', - '....NNNNNN......', - '.....NNNN.......', - '................', - '................', - '................', - '................' - ], { - B: '#1f7fa2', Q: '#60d6e8', W: '#fffdf7', Y: '#ffe55d', N: '#5a4d43' - }); - } - - function drawClockworkWhaleDepth() { - return depthRows([ - '................', - '................', - '.......^........', - '......^^........', - '...^^^^^^^^.....', - '..^^^^^^^^^^....', - '.^^^^^^^^^^^....', - '.^^^^^^^^^^^.^..', - '..^^^^^^^^^.^^..', - '...-------.^^...', - '....------......', - '.....----.......', - '................', - '................', - '................', - '................' - ]); - } - - function drawLanternCatRight() { - return artRows([ - '................', - '................', - '.....N..N.......', - '....NSSSSN......', - '....SWSWS.......', - '....SSKSS.......', - '...OOOOOOO......', - '..OOOYYOOOY.....', - '..OOOOOOOYY.....', - '...O..OOO.......', - '..NN..N.N.......', - '................', - '................', - '................', - '................', - '................' - ], { - N: '#151923', S: '#ffc071', W: '#fffdf7', K: '#31333d', O: '#ee9a55', Y: '#ffe55d' - }); - } - - function drawCloudKoiRight() { - return artRows([ - '................', - '................', - '................', - '.....C..........', - '....CCC.........', - '...CQQQCCC......', - '..CQQQQQQQC.....', - '.CQQWQQQWQQC....', - '..CQQQQQQQC..P..', - '...CQQQCCC..PP..', - '.....C......P...', - '................', - '................', - '................', - '................', - '................' - ], { - C: '#aef8ff', Q: '#60d6e8', W: '#fffdf7', P: '#ffb0c7' - }); - } - - function drawPaperCraneRight() { - return artRows([ - '................', - '................', - '.......W........', - '......WWW.......', - '.....WWCWW......', - '....WWCCWWW.....', - '...WWCCNCCWW....', - '.....WCNW.......', - '......NN........', - '.....N..N.......', - '....N....N......', - '................', - '................', - '................', - '................', - '................' - ], { - W: '#eef4ff', C: '#cbdcff', N: '#151923' - }); - } - - function drawMoonLantern() { - return artRows([ - '.......Y........', - '......YYY.......', - '......YWY.......', - '.....YYYYY......', - '....NNNNNNN.....', - '...NMMMMMMMN....', - '...MBBMMBBM.....', - '..NMMYYYYMMN....', - '..MMMMYYMMMM....', - '..MMMDYYDMMM....', - '..MMMDDDDMMM....', - '..MMMRDDRRMM....', - '...MMRDDRM......', - '...MMMMMMMM.....', - '....MMMMMM......', - '....NNNNNN......' - ], { - Y: '#ffe99a', W: '#fffdf7', N: '#3f3e42', M: '#806f5f', B: '#8aa7ef', D: '#191a22', R: '#de4d6d' - }); - } - - function drawMoonLanternDepth() { - return depthRows([ - '.......^........', - '......^^^.......', - '......^^^.......', - '.....^^^^^......', - '....-------.....', - '...-^^^^^^^-....', - '...-^^^^^^-.....', - '..-^^^^^^^^-....', - '..-^^^^^^^^-....', - '..-^^^--^^^-....', - '..-^^^--^^^-....', - '..-^^^--^^^-....', - '...-^^^^^^-.....', - '...--------.....', - '....------......', - '....------......' - ]); - } - - function drawGlassFern() { - return artRows([ - '................', - '.......G........', - '......GEG.......', - '.....GEEEG......', - '..C..GEGEG..C...', - '...C.GEGEG.C....', - '....CGEEEGC.....', - '.....GEGEG......', - '....GEEEEE......', - '...GEEEGEG......', - '..GEGEGEGEG.....', - '....TTTTT.......', - '....TNNNT.......', - '...TTNNNTT......', - '...NNNNNNN......', - '................' - ], { - G: '#45b96b', E: '#aaf0ce', C: '#bff7ff', T: '#674d0b', N: '#806f5f' - }); - } - - function drawStarTotem() { - return artRows([ - '.......Y........', - '......YYY.......', - '.......Y........', - '.....PYPYP......', - '....PPPPPPP.....', - '.....PBPBP......', - '......BBB.......', - '.....BNNNB......', - '.....NNYNN......', - '.....NNYNN......', - '....NNNYNNN.....', - '....NNNNNNN.....', - '.....NNNNN......', - '....MMMMMMM.....', - '...MMMMMMMMM....', - '................' - ], { - Y: '#ffe99a', P: '#c79bed', B: '#8aa7ef', N: '#5e554e', M: '#3f3e42' - }); - } - - function drawStarTotemDepth() { - return depthRows([ - '.......^........', - '......^^^.......', - '.......^........', - '.....^^^^^......', - '....^^^^^^^.....', - '.....^^^^^......', - '......---.......', - '.....-^^^-......', - '.....-^^^-......', - '.....-^^^-......', - '....-^^^^^-.....', - '....-------.....', - '.....-----......', - '....-------.....', - '...---------....', - '................' - ]); - } - - function drawCoralSkiff() { - return artRows([ - '................', - '........N.......', - '........N.......', - '.......WYN......', - '......WWYN......', - '.....WWYYN......', - '....WYYYYN......', - '.......NNN......', - '...R...NN...R...', - '..RRRMMMMMMRR...', - '..RMMMMMMMMMR...', - '...MMMMMMMM.....', - '....DDDDDD......', - '.....DDDD.......', - '................', - '................' - ], { - N: '#5e554e', W: '#fffdf7', Y: '#ffe99a', R: '#fa6e5a', M: '#a95a32', D: '#3d2218' - }); - } - - function drawCoralSkiffDepth() { - return depthRows([ - '................', - '........^.......', - '........^.......', - '.......^^.......', - '......^^^.......', - '.....^^^^.......', - '....^^^^^.......', - '.......---......', - '...^...--...^...', - '..^^^-------^...', - '..-----------...', - '...--------.....', - '....------......', - '.....----.......', - '................', - '................' - ]); - } - - function drawLanternWalkerRight() { - return artRows([ - '................', - '................', - '......NNN.......', - '.....NSSSN......', - '.....NSKSN......', - '......SSS.......', - '.....RRRRR......', - '....RRRRYRY.....', - '....RRRR.YY.....', - '.....BBBR.......', - '.....B.BB.......', - '.....B..B.......', - '....NN..NN......', - '................', - '................', - '................' - ], { - N: '#191a22', S: '#ffd59c', K: '#3f3e42', R: '#de4d6d', Y: '#ffe99a', B: '#354ca3' - }); - } - - function drawSproutFoxRight() { - return artRows([ - '................', - '................', - '................', - '.........G......', - '........GEG.....', - '....OOO..G......', - '...OWWOO........', - '..OOOWWOOO......', - '..ODODDOWOO.....', - '..OOOOOOO.......', - '...O..O.O.......', - '..NN..NNN.......', - '................', - '................', - '................', - '................' - ], { - O: '#ffad63', W: '#fffdf7', D: '#a95a32', N: '#3d2218', G: '#45b96b', E: '#aaf0ce' - }); - } - - function drawAzureMinnowRight() { - return artRows([ - '................', - '................', - '................', - '................', - '................', - '.....CQQQC......', - '...CCQQQQQC.....', - '..CQQQWBQQQCC...', - '...CCQQQQQC.....', - '.....CQQQC......', - '.......C........', - '................', - '................', - '................', - '................', - '................' - ], { - C: '#bff7ff', Q: '#35b9d0', W: '#fffdf7', B: '#141942' - }); - } - - function drawVioletMothRight() { - return artRows([ - '................', - '................', - '.......P........', - '.....PPBPP......', - '....PPBKBPP.....', - '...PPBBKBBPP....', - '.....PBKBP......', - '......NKN.......', - '.....I.N.I......', - '....I.....I.....', - '................', - '................', - '................', - '................', - '................', - '................' - ], { - P: '#c79bed', B: '#9e65d8', K: '#4b206f', N: '#191a22', I: '#ead1ff' - }); - } - - - function drawWildflowerPatch() { - return artRowsSized([ - '........', - '.gPgYg..', - 'gWgBgPg.', - 'ggggggg.', - 'gYgPgWg.', - 'ggggggg.', - '.GgGgG..', - '..NN....' - ], { - g: '#69c96f', G: '#3f9a46', P: '#f38db6', Y: '#f2dd59', W: '#fffdf7', B: '#77d7ff', N: '#7e5a3e' - }, 8, 8); - } - - function drawRiverStones() { - return artRowsSized([ - '........', - '..bb....', - '.bssb...', - 'bsmmsb..', - '.bssssb.', - '..bgGb..', - '...bb...', - '........' - ], { - b: '#7ba9cf', s: '#c7cdd1', m: '#8d959c', g: '#86d06c', G: '#4d9d45' - }, 8, 8); - } - - function drawMapleCanopy() { - return artRows([ - '......OO........', - '....OORROO......', - '...OORRYYOO.....', - '..OORRYYYYOO....', - '..ORRYYGGYYO....', - '.ORRYYGGGGYYO...', - '.ORYYGGGGGGYO...', - '.OYYGGWWGGGYO...', - '..OYYGGGGGGYO...', - '..OOYGGGGYYO....', - '...OOYYYYYO.....', - '.....TTTT.......', - '....TTTTTT......', - '....TTTTTT......', - '...NNNNNNNN.....', - '................' - ], { - O: '#f08c4a', R: '#de4d5d', Y: '#f2d255', G: '#6fbf5e', W: '#fff1d6', T: '#7a4f2c', N: '#4f3828' - }); - } - - function drawMapleCanopyDepth() { - return depthRows([ - '......^^........', - '....^^^^^^......', - '...^^^^^^^^.....', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '.^^^^^^^^^^^^...', - '.^^^^^^^^^^^^...', - '.^^^^^^^^^^^^...', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '...^^^^^^^^.....', - '.....----.......', - '....------......', - '....------......', - '...--------.....', - '................' - ]); - } - - function drawMistyFalls() { - return artRows([ - '....GGGGGG......', - '...GSSSSSSG.....', - '..GSSCCCCSSG....', - '..GSCWWWWCSG....', - '..GSCWBBWCSG....', - '..GSCWBBWCSG....', - '..GSCWBBWCSG....', - '..GSCWBBWCSG....', - '..GSCWWWWCSG....', - '...GCWWWWCG.....', - '....CCWWCC......', - '....WWWWWW......', - '...WTTTTTTW.....', - '..WTTTTTTTTW....', - '..WWWWWWWWWW....', - '................' - ], { - G: '#63b867', S: '#747f88', C: '#b7f2ff', W: '#e8fbff', B: '#63c8ef', T: '#7bb7d6' - }); - } - - function drawMistyFallsDepth() { - return depthRows([ - '....^^^^^^......', - '...^^^^^^^^.....', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '..^^^^^^^^^^....', - '...^^^^^^^^.....', - '....^^^^^^......', - '....------......', - '...--------.....', - '..----------....', - '..----------....', - '................' - ]); - } - - function drawLotusPond() { - return artRowsSized([ - '................', - '....gG....gG....', - '..gGGGg..gGGg...', - '..GGPPGGGGPWG...', - '.GGPPWPPGGPPGG..', - '.GGBBBBBBBBBBG..', - '.GBBBBBBBBBBBG..', - '.GGBBBBBBBBBGG..', - '..GGGBBBBGGG....', - '...GGGGGGGG.....', - '....n....n......', - '................' - ], { - g: '#7dd27c', G: '#4ea759', P: '#f59bc4', W: '#fff8f3', B: '#5bc3e7', n: '#56713d' - }, 16, 12); - } - - function drawLotusPondDepth() { - return depthRowsSized([ - '................', - '....^^....^^....', - '..^^^^^..^^^^...', - '..^^^^^^^^^^^...', - '.^^^^^^^^^^^^^..', - '.^^^^vvvv^^^^^..', - '.^^^vvvvvvv^^^..', - '.^^^^vvvv^^^^^..', - '..^^^^^^^^^^....', - '...^^^^^^^^.....', - '....-....-......', - '................' - ], 16, 12); - } - - function drawMossyArch() { - return artRows([ - '................', - '.....GGGG.......', - '....GSSSSG......', - '...GSSSSSSG.....', - '..GSSGGGGSSG....', - '..GSG....GSG....', - '..GSG....GSG....', - '..GSG....GSG....', - '..GSG....GSG....', - '..GSG....GSG....', - '..GSSGGGGSSG....', - '..GGGLLLLGGG....', - '....LLLLLL......', - '...LLL..LLL.....', - '..NNNN..NNNN....', - '................' - ], { - G: '#58b96c', S: '#8e8f92', L: '#c1d57f', N: '#556046' - }); - } - - function drawMossyArchDepth() { - return depthRows([ - '................', - '.....^^^^.......', - '....^^^^^^......', - '...^^^^^^^^.....', - '..^^^^..^^^^....', - '..^^^....^^^....', - '..^^^....^^^....', - '..^^^....^^^....', - '..^^^....^^^....', - '..^^^....^^^....', - '..^^^^..^^^^....', - '..^^^^^^^^^^....', - '....------......', - '...---..---.....', - '..----..----....', - '................' - ]); - } - - function drawSunflowerGrove() { - return artRowsSized([ - '............', - '.yy..yy..yy.', - 'yOOy.OOy.yOO', - 'yOWOyOWOyOWO', - '.yOO..OO..OO', - '..GG..GG..GG', - '..GgGGGgGGgG', - '.gGGGGGGGGGg', - '.gGgGgGgGgGg', - '..G..G..G..G', - '..T..T..T..T', - '..T..T..T..T', - '.TTTTTTTTTT.', - '.GgGgGgGgGg.', - '..NNNNNNNN..', - '............' - ], { - y: '#f6df56', O: '#f09a3c', W: '#6f4b2a', G: '#64bd62', g: '#8fe17c', T: '#6b8f3f', N: '#7b5a41' - }, 12, 16); - } - - function drawReedBed() { - return artRowsSized([ - '............', - '....rrr.....', - '..r.rrR.r...', - '..RrrrRrR...', - '..RrrrRrr...', - '..RrrrRrr...', - '.wwwwwccww..', - 'wcccccbbccw.', - 'wcbbbbbbbcw.', - '.wwccccccw..', - '..GGGGGG....', - '...NNNN.....' - ], { - r: '#b8d96f', R: '#7fb04a', w: '#d8f5ff', c: '#7bd1e6', b: '#4ca7d3', G: '#67bf68', N: '#7b6347' - }, 12, 12); - } - - function drawFireflySwirlRight() { - return artRowsSized([ - '........', - '...Y....', - '..YGY...', - '.YGWGY..', - '..YGY...', - '...Y....', - '..S.S...', - '........' - ], { - Y: '#fff08a', G: '#7dcf56', W: '#fffdf7', S: '#6bb2ff' - }, 8, 8); - } - - function drawMeadowHareRight() { - return artRowsSized([ - '............', - '....EE......', - '...EFFE.....', - '...EFFFE....', - '..EFFWWFE...', - '..EFFGGFE...', - '..EFFFFFE...', - '..EFFFFF....', - '...F.FF.....', - '..NN.N.N....', - '............', - '............' - ], { - E: '#d8b08a', F: '#c79267', W: '#fff4ec', G: '#5a4333', N: '#6a574d' - }, 12, 12); - } - - function drawBrookTurtleRight() { - return artRowsSized([ - '............', - '............', - '....GGG.....', - '..GYYYYG....', - '.GYYWWYYG...', - '.GYYGGYYGG..', - '..GYYYYYG...', - '...GGGG..B..', - '....N..NBB..', - '............' - ], { - G: '#5ebc68', Y: '#a3d56c', W: '#fff5d8', B: '#87dfff', N: '#516046' - }, 12, 10); - } - - function drawLeafSparrowRight() { - return artRowsSized([ - '........', - '...G....', - '..GGW...', - '.GWWWW..', - '.WWCCW..', - '..WCCG..', - '...N.N..', - '........' - ], { - G: '#70bf5d', W: '#eef9ec', C: '#8edc9e', N: '#5b4e43' - }, 8, 8); - } - - - function drawRedPandaRight() { - return artRowsSized([ - '............', - '...EE..EE...', - '..EOEOEOOE..', - '..EOOWWOOE..', - '..EOOGGOOE..', - '...ORRRRO...', - '..ORRRRRRO..', - '..ORRWWRRO..', - '..ORRRRRRO..', - '...R..R.R...', - '..N...N.N...', - '............' - ], { - E: '#f4efe9', O: '#d87436', W: '#fff7ef', G: '#4b3428', R: '#c95f2c', N: '#7b624e' - }, 12, 12); - } - - function drawRiverOtterRight() { - return artRowsSized([ - '..............', - '...BBB........', - '..BCCCCBB.....', - '.BCCWWCCCBB...', - '.BCCGGCCCCCB..', - '.BCCCCCCCCCB..', - '..BCCCC..CC...', - '...NNN...N....' - ], { - B: '#7a5b44', C: '#9b7658', W: '#efe1d2', G: '#3a2b24', N: '#5c534f' - }, 14, 8); - } - - function drawAmberDeerRight() { - return artRowsSized([ - '..............', - '.....A........', - '....A.A.......', - '...AAWAA......', - '...AWWWAA.....', - '..AAWGGWAA....', - '..AWWWWWWA....', - '..AWWWWWWAA...', - '..AWWWWWWWA...', - '...AWWWWWA....', - '...A.AA.A.....', - '..N..AA..N.....', - '..N..AA..N.....', - '..............' - ], { - A: '#d5954c', W: '#efc27d', G: '#3f2c21', N: '#71584a' - }, 14, 14); - } - - function drawForestOwlRight() { - return artRowsSized([ - '..........', - '...EE.E...', - '..EBBBBE..', - '..BWWWWB..', - '.BWWGGWWB.', - '.BWWWWWWB.', - '.BBWWWWBB.', - '..BYYYYB..', - '..BYYYYB..', - '..N.BB.N..', - '...N..N...', - '..........' - ], { - E: '#c9a05b', B: '#7b5d40', W: '#f3e9d7', G: '#46352a', Y: '#b99658', N: '#6c5b4b' - }, 10, 12); - } - - function drawKingfisherRight() { - return artRowsSized([ - '..........', - '....C.....', - '...CCCW...', - '..CBWWWWY.', - '.CCBWWGGY.', - '..CBBBBY..', - '....N.N...', - '..........' - ], { - C: '#2b91c9', B: '#1d5d8e', W: '#f8f5ed', G: '#44403b', Y: '#e6a74c', N: '#5a514a' - }, 10, 8); - } - - function drawPondDuckRight() { - return artRowsSized([ - '............', - '............', - '....GG......', - '...GWWWW....', - '..GWWWGGGY...', - '..GWWWWWWWY..', - '..GGGWWWWY...', - '....W..W.....', - '...N....N....', - '............' - ], { - G: '#4f8f46', W: '#efe6d8', Y: '#e7aa55', N: '#6a5e4f' - }, 12, 10); - } - - function drawHeronRight() { - return artRowsSized([ - '..........', - '.....W....', - '....WWW...', - '.....WWY..', - '....WWWW..', - '...WWWGG..', - '...WWWWW..', - '....WWWW..', - '.....WW...', - '.....WW...', - '.....WW...', - '.....WW...', - '....N..N..', - '....N..N..', - '..........', - '..........' - ], { - W: '#f4f6f7', Y: '#d5b073', G: '#49525e', N: '#756b5f' - }, 10, 16); - } - - function drawSunsetKoiRight() { - return artRowsSized([ - '............', - '............', - '....O.......', - '..OORRRROO..', - '.ORRWWWRRRY.', - '..ORRRRRROO.', - '....O.O.....', - '............' - ], { - O: '#f39a44', R: '#ef635b', W: '#fff6ea', Y: '#f4c14a' - }, 12, 8); - } - - function drawSilverTroutRight() { - return artRowsSized([ - '............', - '....S.......', - '..SSCCCSS...', - '.SCCWWCCCB..', - '..SSCCCSS...', - '....S.......' - ], { - S: '#8aa6bf', C: '#c6d6e3', W: '#f5fbff', B: '#3f596b' - }, 12, 6); - } - - function drawButterflyFishRight() { - return artRowsSized([ - '..........', - '....Y.....', - '..YYWWYY..', - '.YWWBBWYY.', - '..YYWWYYK.', - '....Y.....', - '..........', - '..........' - ], { - Y: '#f3d454', W: '#fff7e6', B: '#263347', K: '#f09a42' - }, 10, 8); - } - - function drawTownGardenerRight() { - return artRowsSized([ - '............', - '....NN......', - '...NSSN.....', - '...SWWS.....', - '...SSGSS....', - '....GRR.....', - '...GRRRR....', - '...GRRRR....', - '....BBB.....', - '...BB.BB....', - '...B...B....', - '..NN...NN...', - '............', - '............', - '............', - '............' - ], { - N: '#27252b', S: '#f0c099', W: '#fff6ef', G: '#4b6f3d', R: '#89c96a', B: '#6e4f3a' - }, 12, 16); - } - - function drawLanternCourierRight() { - return artRowsSized([ - '............', - '....NN......', - '...NSSN.....', - '...SWWS.....', - '...SSSS.....', - '....RRRR....', - '...RRRRRY...', - '...RRRR.Y...', - '....BBBB....', - '...BB.BB....', - '...B...B....', - '..NN...NN...', - '............', - '............', - '............', - '............' - ], { - N: '#232531', S: '#f2c7a3', W: '#fff7ef', R: '#c85252', Y: '#ffe99a', B: '#3f5078' - }, 12, 16); - } - - function drawPlazaMusicianRight() { - return artRowsSized([ - '..............', - '.....NN.......', - '....NSSN......', - '....SWWS......', - '....SSSS......', - '...PPPPPP.....', - '...PBBQQQ.....', - '...PBBQQQ.....', - '....QTTQ......', - '...QT..TQ.....', - '...N....N.....', - '..NN....NN....', - '..............', - '..............', - '..............', - '..............' - ], { - N: '#2b2530', S: '#f0c39b', W: '#fff7ee', P: '#8a5ab1', B: '#5a3f26', Q: '#d8a45a', T: '#6e5749' - }, 14, 16); - } - - function drawBridgeMechanicRight() { - return artRowsSized([ - '..............', - '.....NN.......', - '....NSSN......', - '....SWWS......', - '....SSSS......', - '...CCCCCC.....', - '...CCKCCC.....', - '...CCKCCY.....', - '....BBBB......', - '...BB.BB......', - '...B...B......', - '..NN...NN.....', - '..............', - '..............', - '..............', - '..............' - ], { - N: '#272934', S: '#efc39e', W: '#fff8f1', C: '#4f8ca8', K: '#293645', Y: '#e8b65d', B: '#6b5241' - }, 14, 16); - } - - function drawHarborClocktower() { - const w = 32, h = 40, p = blankPixels(w, h); - rect(p, w, 12, 6, 8, 26, '#b08867'); - rect(p, w, 11, 30, 10, 7, '#9a7658'); - rect(p, w, 10, 35, 12, 3, '#6f5746'); - rect(p, w, 13, 1, 6, 5, '#8f5c4d'); - for (let y = 0; y < 3; y++) for (let x = 13 - y; x <= 18 + y; x++) px(p, w, x, y, y === 0 ? '#6d4650' : '#835764'); - rect(p, w, 13, 10, 6, 6, '#f0eadb'); - rect(p, w, 14, 11, 4, 4, '#fffdf7'); - px(p, w, 15, 13, '#4f535f'); px(p, w, 16, 13, '#4f535f'); px(p, w, 16, 12, '#4f535f'); - rect(p, w, 13, 19, 2, 3, '#ffe99a'); rect(p, w, 17, 19, 2, 3, '#ffe99a'); - rect(p, w, 14, 25, 2, 3, '#ffe99a'); rect(p, w, 17, 25, 2, 3, '#ffe99a'); - rect(p, w, 14, 32, 4, 5, '#3b3348'); - rect(p, w, 8, 37, 16, 2, '#544539'); - return p; - } - - function drawGlassGreenhouse() { - const w = 28, h = 20, p = blankPixels(w, h); - for (let y = 2; y <= 6; y++) { - const inset = 10 - y; - for (let x = 6 + inset; x < w - 6 - inset; x++) px(p, w, x, y, y % 2 ? '#dff7ff' : '#b7ecff'); - } - rect(p, w, 4, 7, 20, 10, '#dff7ff'); - for (let x = 4; x <= 23; x += 4) rect(p, w, x, 7, 1, 10, '#7aa6a0'); - for (let y = 7; y <= 16; y += 3) rect(p, w, 4, y, 20, 1, '#7aa6a0'); - rect(p, w, 12, 12, 4, 5, '#fff4c2'); - rect(p, w, 11, 17, 6, 2, '#6d5442'); - rect(p, w, 7, 14, 3, 2, '#6fc26b'); rect(p, w, 18, 14, 3, 2, '#6fc26b'); - rect(p, w, 8, 12, 2, 2, '#8adf73'); rect(p, w, 18, 11, 2, 2, '#8adf73'); - return p; - } - - function drawSteamWorkshop() { - const w = 40, h = 24, p = blankPixels(w, h); - rect(p, w, 4, 8, 32, 12, '#8b6a58'); - rect(p, w, 6, 10, 10, 8, '#a27d66'); - rect(p, w, 18, 10, 16, 8, '#9a735d'); - rect(p, w, 6, 6, 13, 2, '#5f4d43'); - rect(p, w, 18, 5, 17, 3, '#4d4e55'); - rect(p, w, 9, 11, 3, 3, '#ffd979'); rect(p, w, 14, 11, 3, 3, '#ffd979'); - rect(p, w, 24, 11, 3, 3, '#ffd979'); rect(p, w, 29, 11, 3, 3, '#ffd979'); - rect(p, w, 18, 15, 6, 5, '#3d3340'); - rect(p, w, 10, 0, 4, 8, '#6d5548'); - rect(p, w, 27, 0, 5, 8, '#5b5b62'); - rect(p, w, 11, 0, 2, 3, '#9fc8d4'); rect(p, w, 28, 0, 3, 3, '#9fc8d4'); - rect(p, w, 2, 20, 36, 3, '#5b4940'); - return p; - } - - function drawCanalBridge() { - const w = 48, h = 16, p = blankPixels(w, h); - rect(p, w, 3, 12, 42, 2, '#6a5446'); - rect(p, w, 6, 10, 36, 2, '#8e715d'); - rect(p, w, 8, 8, 32, 2, '#b08c72'); - for (let i = 0; i < 8; i++) { - rect(p, w, 10 + i * 4, 5, 1, 5, '#d9c4a8'); - rect(p, w, 11 + i * 4, 4, 2, 1, '#d9c4a8'); - } - for (let y = 10; y <= 13; y++) { - const inset = Math.abs(11 - y) * 2; - rect(p, w, 16 + inset, y, 16 - inset * 2, 1, '#3a78a6'); - } - return p; - } - - function drawGrandFountain() { - return artRowsSized([ - '........................', - '...........WW...........', - '..........WWWW..........', - '.........WBBBBW.........', - '..........WBBW..........', - '.........WWBBWW.........', - '........WBBBBBBW........', - '.........WBBBBW.........', - '.......SSSWWWWSSS.......', - '......SCCCSSSSCCCS......', - '.....SCCBBBBBBCCCS......', - '....SCCBBBBBBBBCCCS.....', - '....SCBBBBWWBBBBCCS.....', - '....SCCBBBBBBBBCCCS.....', - '.....SCCBBBBBBCCCS......', - '......SCCCSSSSCCS.......', - '.......SSSSSSSSS........', - '.......NNNNNNNNN........', - '......NNNSSSSNNNN.......', - '.....NNSSSSSSSSNN.......', - '....NNNNNNNNNNNNNN......', - '....NNBBBBBBBBBBNN......', - '.....NNNNNNNNNNNN.......', - '........................' - ], { W: '#eafcff', B: '#7ed3ef', C: '#bff2ff', S: '#9b8b7d', N: '#6f6258' }, 24, 24); - } - - function drawRocketMonument() { - return artRowsSized([ - '.........RR.........', - '........RWWR........', - '.......RWWWWR.......', - '.......RWWWWR.......', - '......RRWGGWRR......', - '......RWWGGWWR......', - '......RWWWWWWR......', - '......RWWBBWWR......', - '......RWWBBWWR......', - '......RWWWWWWR......', - '.....RRWWWWWWRR.....', - '.....RWWWWWWWWR.....', - '.....RWWWWWWWWR.....', - '.....RWWRRWWWWR.....', - '.....RWWRRWWWWR.....', - '.....RWWWWWWWWR.....', - '.....RRWWWWWWRR.....', - '......RWWWWWWR......', - '.....RRWWWWWWRR.....', - '....RRRWWWWWWRRR....', - '....RWWWWWWWWWWR....', - '....RWWWWWWWWWWR....', - '....RRRRWWWWRRRR....', - '.....BBBWWWWBBB.....', - '.....BBYYYYYYBB.....', - '.....BBOOYYOOBB.....', - '.....BBYYYYYYBB.....', - '.....BBBBBBBBBB.....', - '......NNN..NNN......', - '.....NNN....NNN.....', - '.....NN......NN.....', - '....................' - ], { R: '#d95f5f', W: '#f3f4f8', G: '#86d9ff', B: '#776154', Y: '#ffd979', O: '#ff8a5c', N: '#514740' }, 20, 32); - } - - function drawArcadeBooth() { - const w = 24, h = 20, p = blankPixels(w,h); - rect(p,w,3,5,18,11,'#5a3d70'); - rect(p,w,4,3,16,3,'#8d5cc2'); - rect(p,w,6,7,12,6,'#161c2f'); - rect(p,w,7,8,10,4,'#2bb4ff'); - rect(p,w,9,14,6,2,'#3b3348'); - rect(p,w,8,16,8,2,'#6f5142'); - px(p,w,10,4,'#ffe66a'); px(p,w,13,4,'#ff8cc8'); px(p,w,16,4,'#8ef2ff'); - rect(p,w,2,18,20,1,'#47352b'); - return p; - } - - function drawChessKnightStatue() { - return artRowsSized([ - '................', - '......GG........', - '.....GWWG.......', - '....GWWWWG......', - '....GWWBWWG.....', - '....GWWWWGG.....', - '...GWWWWWWG.....', - '...GWWGWWWG.....', - '...GWWGGWWG.....', - '....GWWWWG......', - '....GWWWWG......', - '...GGWWWWGG.....', - '..GGGWWWWGGG....', - '..GWWWWWWWWG....', - '..GGGGWWGGGG....', - '....NNWWNN......', - '...NNNWWNNN.....', - '..NNNNNNNNNN....', - '..NNNSSSSNNN....', - '..NNSSSSSSNN....', - '.NNNNNNNNNNNN...', - '.NBBBBBBBBBBN...', - '..NNNNNNNNNN....', - '................' - ], { G: '#c8cfda', W: '#f8fbff', B: '#8fa1bd', N: '#706257', S: '#9e9184' }, 16, 24); - } - - function drawDesertTrain() { - const w = 44, h = 16, p = blankPixels(w, h); - rect(p, w, 4, 8, 7, 4, '#8b6b58'); - rect(p, w, 11, 7, 13, 5, '#b27d4c'); - rect(p, w, 24, 7, 13, 5, '#c58a54'); - rect(p, w, 37, 8, 5, 4, '#915f40'); - rect(p, w, 5, 5, 4, 3, '#645a5f'); - rect(p, w, 12, 5, 10, 2, '#744f45'); - rect(p, w, 25, 5, 10, 2, '#744f45'); - rect(p, w, 13, 8, 3, 2, '#ffd979'); - rect(p, w, 18, 8, 3, 2, '#ffd979'); - rect(p, w, 27, 8, 3, 2, '#ffd979'); - rect(p, w, 32, 8, 3, 2, '#ffd979'); - rect(p, w, 7, 7, 2, 2, '#ffe7a4'); - rect(p, w, 6, 3, 2, 2, '#d9e4e8'); - rect(p, w, 8, 2, 2, 1, '#eef4f7'); - rect(p, w, 11, 12, 24, 1, '#684f41'); - for (const x of [6, 15, 21, 29, 35, 40]) { - circle(p, w, x, 13, 2, '#302a2a'); - px(p, w, x, 13, '#8f857b'); - } - rect(p, w, 0, 15, 44, 1, '#5f4a3f'); - rect(p, w, 2, 14, 40, 1, '#b98d5d'); - return p; - } - - function drawTeaRobotRight() { - return artRowsSized([ - '............', - '.....CC.....', - '....CWWC....', - '....CWGWC...', - '....CWWWC...', - '.....MMM....', - '....MKKKM...', - '....MKYKMY..', - '.....MKKM...', - '....B.BBB...', - '...BB..BB...', - '...N....N...', - '............', - '............', - '............', - '............' - ], {C:'#8fd3e8',W:'#f5fbff',G:'#4b525d',M:'#c38f61',K:'#6d584b',Y:'#9be6ff',B:'#86736b',N:'#4d4c55'}, 12, 16); - } - - function drawBalloonVendorRight() { - return artRowsSized([ - '..P..Y..B.....', - '..P..Y..B.....', - '..PP.YY.BB....', - '.....S........', - '.....N........', - '....NSSN......', - '....SWWS......', - '....SSSS......', - '...RRRRRR.....', - '...RRRRRR.....', - '....BB.BB.....', - '...BB...B.....', - '...N....N.....', - '..NN....NN....', - '..............', - '..............', - '..............', - '..............' - ], {P:'#f38db6',Y:'#f3d454',B:'#8ecfff',S:'#7c5b48',N:'#27252b',W:'#fff6ef',R:'#d76b52'}, 14, 18); - } - - function drawJellyCometRight() { - return artRowsSized([ - '............', - '......C.....', - '....CCCCC...', - '..CCWWWWWCC.', - '..CWWGGGWWC.', - '..CWWWWWWWC.', - '...CWWWWWC..', - '....CCCCC...', - '.....T.T....', - '....T...T...', - '............', - '............' - ], {C:'#98c3ff',W:'#dff0ff',G:'#5b5d8c',T:'#b7d8ff'}, 12, 12); - } - - function drawCourierBikeRight() { - return artRowsSized([ - '................', - '......NN........', - '.....NSSN.......', - '.....SWWS.......', - '.....SSSS.......', - '....RRRRRR......', - '....RBBB.R......', - '....R.BBBR......', - '.....K.K........', - '...OO...OO......', - '..OOWOOOOWO.....', - '...OO...OO......' - ], {N:'#252732',S:'#f0c39b',W:'#fff7ef',R:'#4da0d8',B:'#6b4d39',K:'#4a4f58',O:'#d9dce5'}, 16, 12); - } - - function drawGreatCedar() { - return artRowsSized([ - '..........GGGG..........', - '........GGLLLLGG........', - '......GGLLLWWLLGG.......', - '.....GLLLWWWWLLLGG......', - '....GLLLWWGGWWLLLG......', - '...GLLLWWGGGGWWLLLG.....', - '...GLLWWGGGGGGWWLLG.....', - '..GLLWWGGDDDDGGWWLLG....', - '..GLLWGGDDDDDDGGWLLG....', - '.GLLWWGDDGGGGDDGWWLLG...', - '.GLLWGGDGGGGGGDGGWLLG...', - '.GLWWGGGGGGGGGGGGWWLG...', - '.GLWGGGGGGGGGGGGGGWLG...', - 'GLWWGGGGGGGGGGGGGGWWLG..', - 'GLWGGGGGGGGGGGGGGGGWLG..', - 'GLWGGGGGGGGGGGGGGGGWLG..', - '.GLWGGGGGGGGGGGGGGWLG...', - '.GLWWGGGGGGGGGGGGWWLG...', - '.GLLWGGGGGGGGGGGGWLLG...', - '.GLLWWGGGGGGGGGGWWLLG...', - '..GLLWGGGGGGGGGGWLLG....', - '..GLLWWGGGGGGGGWWLLG....', - '...GLLWWGGGGGGWWLLG.....', - '....GLLLWWGGWWLLLG......', - '.....GLLLWWWWLLLG.......', - '......GLLLTTLLLG........', - '.......GLLTTLLG.........', - '.......TTTTTTTT.........', - '......TTTTTTTTTT........', - '......TTTTRRTTTT........', - '.....NNNNNNNNNNNN.......', - '........................' - ], { - G: '#4ca35b', L: '#71c872', W: '#b7f2b6', D: '#3a7b44', T: '#7a4f30', R: '#c79664', N: '#4f3828' - }, 24, 32); - } - - function drawGreatCedarDepth() { - return depthRowsSized([ - '..........^^^^..........', - '........^^^^^^^^........', - '......^^^^^^^^^^^.......', - '.....^^^^^^^^^^^^^^......', - '....^^^^^^^^^^^^^^^......', - '...^^^^^^^^^^^^^^^^^.....', - '...^^^^^^^^^^^^^^^^^.....', - '..^^^^^^^^^^^^^^^^^^^....', - '..^^^^^^^^^^^^^^^^^^^....', - '.^^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^^...', - '^^^^^^^^^^^^^^^^^^^^^^^..', - '^^^^^^^^^^^^^^^^^^^^^^^..', - '^^^^^^^^^^^^^^^^^^^^^^^..', - '.^^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^^...', - '..^^^^^^^^^^^^^^^^^^^....', - '..^^^^^^^^^^^^^^^^^^^....', - '...^^^^^^^^^^^^^^^^^.....', - '....^^^^^^^^^^^^^^^......', - '.....^^^^^^^^^^^^^.......', - '......^^^^----^^^........', - '.......^^^----^^.........', - '.......--------.........', - '......----------........', - '......----------........', - '.....------------.......', - '........................' - ], 24, 32); - } - - function drawMoonfallCascade() { - return artRowsSized([ - '......GGGGGGGGGGGG......', - '....GGSSSSSSSSSSSSGG....', - '...GSSSCCWWWWCCSSSSG....', - '..GSSCCWWWWWWWWCCSSSG...', - '..GSCWWWWBBBBWWWWCSG...', - '.GSCWWWBBBBBBBBWWCSG...', - '.GSWWWBBBTTTTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBTWWTBBBWWSG...', - '.GSWWWBBBWWWWBBBWWSG...', - '.GSWWWWWWWWWWWWWWWSG...', - '..GSWWWWCCWWCCWWWSG....', - '..GSWWWCCWWWWCCWWSG....', - '...GSWWWWWWWWWWWSG.....', - '....GSWWWWWWWWWWSG.....', - '....GGWWWWWWWWWWGG.....', - '.....WWTTTTTTTTWW......', - '....WWTTTTTTTTTTWW.....', - '....WTTTTTTTTTTTTW.....', - '....WWWWWWWWWWWWWW.....', - '....BBBBBBBBBBBBBB.....', - '........................' - ], { - G: '#58a55d', S: '#6f7e87', C: '#c7f3ff', W: '#eafcff', B: '#58c0ea', T: '#7aaec8' - }, 24, 32); - } - - function drawMoonfallCascadeDepth() { - return depthRowsSized([ - '......^^^^^^^^^^^^......', - '....^^^^^^^^^^^^^^^^....', - '...^^^^^^^^^^^^^^^^^....', - '..^^^^^^^^^^^^^^^^^^^...', - '..^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^^^^^^^^^^^^^^...', - '.^^^^^^^vvvv^^^^^^^...', - '..^^^^^^vvvv^^^^^^....', - '..^^^^^^vvvv^^^^^^....', - '...^^^^^vvvv^^^^^.....', - '....^^^^vvvv^^^^.....', - '....^^^^vvvv^^^^.....', - '.....------------......', - '....--------------.....', - '....--------------.....', - '....--------------.....', - '....--------------.....', - '........................' - ], 24, 32); - } - - function drawSunblossomGate() { - return artRowsSized([ - '........YYYY........', - '......YYOOOOYY......', - '.....YOOOGGOOOY.....', - '....YOOOGGGGOOOY....', - '...YYOOGGWWGGOOYY...', - '..YYOOGGWWWWGGOOYY..', - '..YOOOGWWWWWWGOOOY..', - '.YOOOGWW....WWGOOOY.', - '.YOOOGW......WGOOOY.', - '.YOOOGW......WGOOOY.', - '.YOOOGW......WGOOOY.', - '.YOOOGW......WGOOOY.', - '.YOOOGW......WGOOOY.', - '.YOOOGW......WGOOOY.', - '..YOOOGG....GGOOOY..', - '..YYOOOGGGGGGOOYY...', - '...YYOONNNNNNOYY....', - '....NNNNTTTTNNN.....', - '....NNNNTTTTNNN.....', - '....NNNNTTTTNNN.....', - '....NNNNNNNNNNN.....', - '.....GGGGGGGG.......', - '.....GGGGGGGG.......', - '........................' - ], { - Y: '#f3dd5c', O: '#ef9b43', G: '#69bf67', W: '#fff3d9', N: '#866b52', T: '#6a4a33' - }, 24, 24); - } - - function drawSunblossomGateDepth() { - return depthRowsSized([ - '........^^^^........', - '......^^^^^^^^......', - '.....^^^^^^^^^^.....', - '....^^^^^^^^^^^^....', - '...^^^^^^^^^^^^^^...', - '..^^^^^^^^^^^^^^^^..', - '..^^^^^^^^^^^^^^^^..', - '.^^^^^^^....^^^^^^^.', - '.^^^^^^......^^^^^^.', - '.^^^^^^......^^^^^^.', - '.^^^^^^......^^^^^^.', - '.^^^^^^......^^^^^^.', - '.^^^^^^......^^^^^^.', - '.^^^^^^......^^^^^^.', - '..^^^^^^....^^^^^^..', - '..^^^^^^^^^^^^^^^^..', - '...^^^^^------^^^...', - '....----^^^^----....', - '....----^^^^----....', - '....----^^^^----....', - '....-------------....', - '.....--------.......', - '.....--------.......', - '........................' - ], 24, 24); - } - - function drawEchoCavern() { - return artRowsSized([ - '................................', - '..........SSSSSSSSSS............', - '.......SSSSGGGGGGGGSSSS.........', - '.....SSGGGGGNNNNGGGGGGSS........', - '....SGGGGNNNNNNNNNNGGGGS........', - '...SGGGNNNNNNNNNNNNNNGGGS.......', - '..SGGNNNNNNNN..NNNNNNNNGGS......', - '.SGGNNNNNN......NNNNNNNGGS......', - '.SGNNNNN..........NNNNNNGS......', - '.SGNNNN............NNNNNGS......', - '.SGNNN..............NNNNGS......', - '.SGNNN..............NNNNGS......', - '.SGNNNN............NNNNNGS......', - '.SGNNNNN..........NNNNNNGS......', - '.SGGNNNNNN......NNNNNNNGGS......', - '..SGGNNNNNNNN..NNNNNNNNGGS......', - '...SGGGNNNNNNNNNNNNNNGGGS.......', - '....SGGGGNNNNNNNNNNGGGGS........', - '.....SSGGGGGNNNNGGGGGSS.........', - '......BBBBBBBBBBBBBBBB..........' - ], { - S: '#8a8c90', G: '#5ea362', N: '#21252d', B: '#556048' - }, 32, 20); - } - - function drawEchoCavernDepth() { - return depthRowsSized([ - '................................', - '..........^^^^^^^^^^............', - '.......^^^^^^^^^^^^^^^^.........', - '.....^^^^^^^^^^^^^^^^^^^^........', - '....^^^^^^^^^^^^^^^^^^^^^........', - '...^^^^^^^^^^^^^^^^^^^^^^^.......', - '..^^^^^^^^^^^^..^^^^^^^^^^......', - '.^^^^^^^^^^......^^^^^^^^^......', - '.^^^^^^^^..........^^^^^^^^......', - '.^^^^^^^............^^^^^^^......', - '.^^^^^^..............^^^^^^......', - '.^^^^^^..............^^^^^^......', - '.^^^^^^^............^^^^^^^......', - '.^^^^^^^^..........^^^^^^^^......', - '.^^^^^^^^^^......^^^^^^^^^......', - '..^^^^^^^^^^^^..^^^^^^^^^^......', - '...^^^^^^^^^^^^^^^^^^^^^^^.......', - '....^^^^^^^^^^^^^^^^^^^^^........', - '.....^^^^^^^^^^^^^^^^^^^.........', - '......----------------..........' - ], 32, 20); - } - - function drawWorldrootShrine() { - return artRowsSized([ - '............GGGG............', - '..........GGLLLLGG..........', - '........GGLLLWWLLLGG........', - '.......GLLLWWWWWWLLLG.......', - '......GLLWWGGGGGGWWLLG......', - '.....GLLWGGGGGGGGGGWLLG.....', - '....GLLWGGGGTTGGGGGGWLLG....', - '....GLLWGGGTTTTGGGGGWLLG....', - '...GLLWGGTTTTTTTTGGGWLLG....', - '...GLLWGGTTTNNNTTTGGWLLG....', - '..GLLWGGTTTNNWWNNTTGGWLLG...', - '..GLLWGGTTNNWWWWNNTGGWLLG...', - '..GLLWGGTTNNWYYWNNTGGWLLG...', - '..GLLWGGTTNNYYYYNNTGGWLLG...', - '..GLLWGGTTNNWYYWNNTGGWLLG...', - '..GLLWGGTTNNWWWWNNTGGWLLG...', - '..GLLWGGTTTNNWWNNTTGGWLLG...', - '...GLLWGGTTTNNNTTTGGWLLG....', - '...GLLWGGTTTTTTTTGGGWLLG....', - '....GLLWGGGTTTTGGGGGWLLG....', - '....GLLWGGGGTTGGGGGGWLLG....', - '.....GLLWGGGGGGGGGGWLLG.....', - '......GLLWWGGGGGGWWLLG......', - '.......GLLLWWTTWWLLLG.......', - '........GLLLTTTTLLGG........', - '.........TTTTTTTTTT.........', - '........TTTTRRTTTTTT........', - '.......NNNNNNNNNNNNNN.......' - ], { - G: '#4fa85c', L: '#7ad07b', W: '#c6f5be', T: '#7c5232', N: '#69523b', Y: '#ffe99a', R: '#c49668' - }, 28, 28); - } - - function drawWorldrootShrineDepth() { - return depthRowsSized([ - '............^^^^............', - '..........^^^^^^^^..........', - '........^^^^^^^^^^^^........', - '.......^^^^^^^^^^^^^^.......', - '......^^^^^^^^^^^^^^^^......', - '.....^^^^^^^^^^^^^^^^^^.....', - '....^^^^^^^^^^^^^^^^^^^^....', - '....^^^^^^^^^^^^^^^^^^^^....', - '...^^^^^^^^^^^^^^^^^^^^^^....', - '...^^^^^^^^^^^^^^^^^^^^^^....', - '..^^^^^^^^^^^^^^^^^^^^^^^^...', - '..^^^^^^^^^^^^^^^^^^^^^^^^...', - '..^^^^^^^^^^^^++^^^^^^^^^^...', - '..^^^^^^^^^^^++++^^^^^^^^^...', - '..^^^^^^^^^^^^++^^^^^^^^^^...', - '..^^^^^^^^^^^^^^^^^^^^^^^^...', - '..^^^^^^^^^^^^^^^^^^^^^^^^...', - '...^^^^^^^^^^^^^^^^^^^^^^....', - '...^^^^^^^^^^^^^^^^^^^^^^....', - '....^^^^^^^^^^^^^^^^^^^^....', - '....^^^^^^^^^^^^^^^^^^^^....', - '.....^^^^^^^^^^^^^^^^^^.....', - '......^^^^^^^^^^^^^^^^......', - '.......^^^^^^^^^^^^^^.......', - '........^^^^----^^^^........', - '.........----------.........', - '........------------........', - '.......----------------.......' - ], 28, 28); - } - - function drawSmallRaisedDepth() { - const d = Array(16 * 16).fill(0); - for (let y = 2; y <= 12; y++) for (let x = 3; x <= 12; x++) d[y * 16 + x] = 1; - return d; - } - - function filledDepthFromPixels(pixels) { - return filledDepthFromPixelsSized(pixels, 16, 16); - } - - function filledDepthFromPixelsSized(pixels, width, height = width) { - const w = clampDimension(width, 16); - const h = clampDimension(height, w); - const src = normalizePixels(pixels, w, h); - const out = Array(w * h).fill(0); - for (let i = 0; i < src.length; i++) { - if (src[i]) out[i] = 1; - } - return out; - } - - function drawCottage() { - const s = 16, p = blankPixels(s); - rect(p, s, 4, 8, 8, 6, '#b86b4f'); - rect(p, s, 5, 9, 6, 4, '#d68b5e'); - rect(p, s, 6, 11, 3, 3, '#3b3348'); - rect(p, s, 10, 9, 2, 2, '#ffe38a'); - rect(p, s, 3, 7, 10, 1, '#6a485d'); - for (let y = 3; y <= 7; y++) { - const inset = Math.abs(6 - y); - for (let x = 3 + inset; x <= 12 - inset; x++) px(p, s, x, y, y < 5 ? '#8e4f61' : '#74465b'); - } - px(p, s, 6, 8, '#ffcf75'); px(p, s, 11, 12, '#7b493f'); - return p; - } - - function drawPineCluster() { - const s = 16, p = blankPixels(s); - rect(p, s, 6, 9, 2, 5, '#765238'); - rect(p, s, 10, 10, 2, 4, '#765238'); - for (let y = 2; y <= 10; y++) { - const w = Math.floor((y + 1) / 2); - for (let x = 7 - w; x <= 7 + w; x++) px(p, s, x, y, y % 2 ? '#348f5a' : '#42aa68'); - } - for (let y = 5; y <= 11; y++) { - const w = Math.floor((y - 2) / 2); - for (let x = 11 - w; x <= 11 + w; x++) px(p, s, x, y, y % 2 ? '#2f7f55' : '#3d9d62'); - } - px(p, s, 5, 4, '#8ee68c'); px(p, s, 10, 6, '#8ee68c'); - return p; - } - - function drawShellRock() { - const s = 16, p = blankPixels(s); - rect(p, s, 5, 10, 7, 3, '#b9ad9f'); - rect(p, s, 4, 11, 9, 2, '#968b83'); - px(p, s, 6, 9, '#f0e0ca'); px(p, s, 8, 9, '#f0e0ca'); px(p, s, 10, 9, '#f0e0ca'); - rect(p, s, 7, 8, 4, 2, '#ffb6b1'); - px(p, s, 9, 7, '#ffd7cb'); px(p, s, 11, 10, '#6d625f'); - return p; - } - - function drawShip() { - const s = 16, p = blankPixels(s); - rect(p, s, 4, 10, 8, 2, '#7d513c'); - rect(p, s, 5, 12, 6, 1, '#50392f'); - px(p, s, 3, 9, '#7d513c'); px(p, s, 12, 9, '#7d513c'); - rect(p, s, 8, 4, 1, 6, '#5b4b48'); - for (let y = 4; y <= 8; y++) for (let x = 9; x <= 12 - Math.floor((y - 4) / 2); x++) px(p, s, x, y, '#f3ead8'); - for (let y = 5; y <= 8; y++) for (let x = 5 + Math.floor((y - 5) / 2); x <= 7; x++) px(p, s, x, y, '#ffd66e'); - px(p, s, 10, 5, '#ffffff'); px(p, s, 6, 6, '#fff7d1'); - return p; - } - - function drawShipDepth() { - const s = 16, d = Array(s * s).fill(0); - for (let y = 10; y <= 12; y++) for (let x = 4; x <= 12; x++) d[y * s + x] = -1; - for (let y = 4; y <= 8; y++) d[y * s + 8] = 1; - return d; - } - - function drawFisherKidRight() { - const s = 16, p = blankPixels(s); - rect(p, s, 7, 3, 3, 3, '#d9a06c'); - rect(p, s, 6, 6, 5, 4, '#e2b94f'); - rect(p, s, 6, 10, 2, 4, '#31577e'); - rect(p, s, 9, 10, 2, 4, '#31577e'); - rect(p, s, 6, 2, 5, 1, '#35434a'); - px(p, s, 10, 4, '#202631'); - px(p, s, 12, 7, '#5b4b48'); px(p, s, 13, 8, '#5b4b48'); - return p; - } - - function drawCatRight() { - const s = 16, p = blankPixels(s); - rect(p, s, 4, 8, 7, 4, '#5c8a57'); - rect(p, s, 10, 7, 3, 3, '#6fa064'); - px(p, s, 10, 6, '#6fa064'); px(p, s, 12, 6, '#6fa064'); - px(p, s, 12, 8, '#1e2430'); px(p, s, 13, 9, '#d9a06c'); - rect(p, s, 5, 12, 1, 2, '#3e633e'); rect(p, s, 9, 12, 1, 2, '#3e633e'); - px(p, s, 3, 8, '#5c8a57'); px(p, s, 2, 7, '#5c8a57'); - return p; - } - - function drawKoiRight() { - const s = 16, p = blankPixels(s); - rect(p, s, 5, 7, 6, 3, '#f7f2e8'); - px(p, s, 6, 7, '#ff7d5a'); px(p, s, 8, 8, '#ff7d5a'); px(p, s, 10, 9, '#ff7d5a'); - px(p, s, 11, 8, '#1d2530'); - px(p, s, 4, 7, '#ffb165'); px(p, s, 3, 6, '#ffb165'); px(p, s, 3, 10, '#ffb165'); - px(p, s, 7, 6, '#fffdf7'); px(p, s, 9, 10, '#fffdf7'); - return p; - } - - function drawGullRight() { - const s = 16, p = blankPixels(s); - rect(p, s, 7, 6, 3, 3, '#fffdf7'); - rect(p, s, 4, 6, 3, 1, '#dfe6ee'); - rect(p, s, 10, 5, 4, 1, '#dfe6ee'); - px(p, s, 10, 7, '#f0a45c'); - px(p, s, 8, 5, '#1e2430'); - px(p, s, 6, 8, '#c9d2dc'); px(p, s, 9, 8, '#c9d2dc'); - return p; - } - - - function drawAuroraObservatory() { - return artRowsSized([ - '............NNNN............', - '..........NNYYYYNN..........', - '.........NYYCCCCYYN.........', - '........NYYCCCCCCYYN........', - '.......NYYCCBBBBCCYYN.......', - '......NYYCBBBBBBBBCYYN......', - '......NYYBBBBBBBBBBYYN......', - '.....NYYBBBBBBBBBBBBYYN.....', - '.....NYYBBBBBBBBBBBBYYN.....', - '.....NYYYYYYYYYYYYYYYYN.....', - '....NNWWWWWWNNWWWWWWWWNN....', - '....NWWWWWWN..NWWWWWWWWN....', - '...NNWWWLLWNNNNWWLLWWWNN....', - '...NWWWLLLLWWWWLLLLWWWWN....', - '...NWWLLLLLLWWLLLLLLWWWN....', - '...NWWLLDDLLWWLLDDLLWWWN....', - '...NWWLLLLLLWWLLLLLLWWWN....', - '...NWWWWWWWWWWWWWWWWWWWN....', - '...NWWGGGGGGGGGGGGGGWWWN....', - '...NWWGTTTGGTTTTGGTTGWWN....', - '...NWWGTTTGGTTTTGGTTGWWN....', - '...NWWGGGGGGGGGGGGGGWWWN....', - '...NWWGGTTTGGGGGGTTGWWWN....', - '...NWWGGTTTGGDDGGTTGWWWN....', - '...NWWGGGGGGDDGGGGGGWWWN....', - '....NWWWWWWWWDDWWWWWWWN.....', - '....NNNNNNNNNNNNNNNNNN......', - '............................' - ], { - N: '#212532', Y: '#8b74dd', C: '#d9edff', B: '#8fb7f5', W: '#746453', L: '#fff0b4', G: '#c1ab84', T: '#70563f', D: '#3d2f28' - }, 28, 28); - } - - function drawHearthwindMill() { - return artRowsSized([ - '..........NNNN..........', - '.........NYYYYN.........', - '.........NYWWYN.........', - '........NYYWWYYN........', - '........NNNWWNNN........', - '.......N..NWWN..N.......', - '...NNNNN..NWWN..NNNNN...', - '..NPPPPNNNNWWNNNNPPPPN..', - '..NPPPPPPNNWWNNPPPPPPN..', - '..NPPPPPPPNNNNPPPPPPPN..', - '..NPPPPPPPPPPPPPPPPPPN..', - '..NQQQQQQQQQQQQQQQQQQN..', - '..NQQQQQQQQQQQQQQQQQQN..', - '..NQQLLQQQQQQQQQQLLQQN..', - '..NQLLLLQQQNNQQLLLLQQN..', - '..NQQLLQQQQNNQQQQLLQQN..', - '..NQQQQQGGGGGGQQQQQQQN..', - '..NQQGGGTTGGGGGGTTGQQN..', - '..NQQGTTTTGGDDGGTTTGQN..', - '..NQQGTTTTGGDDGGTTTGQN..', - '..NQQGGGGGGGDDGGGGGGQN..', - '..NQQQGGGGGGDDGGGGQQQN..', - '..NQQQQQQQQQDDQQQQQQQN..', - '..NQQQQQQQQQDDQQQQQQQN..', - '..NQQQQQQQQQDDQQQQQQQN..', - '..NQQQQQQQNNNNQQQQQQQN..', - '...NNNNNNNN..NNNNNNNN...', - '........................' - ], { - N: '#252934', Y: '#fff5d0', W: '#8bb8ff', P: '#8f5e47', Q: '#caa77f', L: '#ffe8a0', G: '#d6c29b', T: '#77543b', D: '#403027' - }, 24, 28); - } - - function drawTideglassLighthouse() { - return artRowsSized([ - '........NNNN........', - '.......NYYYYN.......', - '.......NYCCYN.......', - '......NYYCCYYN......', - '......NNNCCNNN......', - '.......N.CC.N.......', - '.......N.BB.N.......', - '......NN.BB.NN......', - '......NWWWWWWN......', - '......NWWLLWWN......', - '.....NNWWLLWWNN.....', - '.....NWWWWWWWWN.....', - '.....NWWGGGGWWN.....', - '.....NWWGTTGWWN.....', - '.....NWWGGGGWWN.....', - '....NNWWWWWWWWNN....', - '....NWWWWLLWWWWN....', - '....NWWWWLLWWWWN....', - '....NWWWWWWWWWWN....', - '....NWWWGGGGWWWN....', - '....NWWWGTTGWWWN....', - '....NWWWGGGGWWWN....', - '...NNWWWWWWWWWWNN...', - '...NWWWWWWWWWWWWN...', - '...NWWWGGWWGGWWWN...', - '...NWWWTTWWTTWWWN...', - '...NWWWGGWWGGWWWN...', - '..NNWWWWWWWWWWWWNN..', - '..NWWWWWWWWWWWWWWN..', - '..NWWWWWWDDWWWWWWN..', - '..NWWWWWWDDWWWWWWN..', - '..NWWWWGGDDGGWWWWN..', - '..NWWWWTTDDTTWWWWN..', - '..NWWWWGGDDGGWWWWN..', - '...NNNNNNDDNNNNNN...', - '........NNNN........' - ], { - N: '#212632', Y: '#fff0ad', C: '#eef7ff', B: '#8fc1ff', W: '#f2efe8', L: '#fff0b2', G: '#d7c5a8', T: '#8f6a4e', D: '#413128' - }, 20, 36); - } - - function drawMossrailDepot() { - return artRowsSized([ - '....NNNNNNNNNNNNNNNNNNNNNNNN....', - '...NYYYYYYYYYYYYYYYYYYYYYYYYN...', - '..NYYNNNNNNNNNNNNNNNNNNNNNNYYN..', - '..NYYNPPPPPPPPPPPPPPPPPPPPNYYN..', - '..NYYNPPPPPPPPPPPPPPPPPPPPNYYN..', - '..NYYNNNNNNNNNNNNNNNNNNNNNNYYN..', - '.NNWWWWWWWWWWWWWWWWWWWWWWWWWWNN.', - '.NWWWWWWWWWWWWWWWWWWWWWWWWWWWWN.', - '.NWWLLLLWWWWLLLLWWWWLLLLWWWWWN..', - '.NWWLLLLWWWWLLLLWWWWLLLLWWWWWN..', - '.NWWWWWWWWWWWWWWWWWWWWWWWWWWWN..', - '.NWWGGGGGGGGGGGGGGGGGGGGGGWWWN..', - '.NWWGTTTGGGGGGGGGGGGGGTTTGWWWN..', - '.NWWGGGGGGGGGNNNNGGGGGGGGGWWWN..', - '.NWWGTTTGGGGGNDDNGGGGGTTTGWWWN..', - '.NWWGGGGGGGGGNDDNGGGGGGGGGWWWN..', - '.NWWWWWWWWWWWWDDWWWWWWWWWWWWWN..', - '.NWWWWWWWWWWWWDDWWWWWWWWWWWWWN..', - '.NNNNNNNNNNNNNNNNNNNNNNNNNNNNNN.', - '................................' - ], { - N: '#232733', Y: '#f5d48f', P: '#7b5651', W: '#a9b8bf', L: '#ffeda7', G: '#d0b48d', T: '#7d5b45', D: '#3b2f2a' - }, 32, 20); - } - - function drawLanternLibrary() { - return artRowsSized([ - '.......NNNNNNNNNNNNNN.......', - '......NYYYYYYYYYYYYYYN......', - '.....NYYNNNNNNNNNNNNYYN.....', - '.....NYNPPPPPPPPPPPPNYN.....', - '....NNNNNNNNNNNNNNNNNNNN....', - '....NWWWWWWWWWWWWWWWWWWN....', - '...NNWWWLLLWWNNWWLLLWWWNN...', - '...NWWWLLLLWWWWWWLLLLWWWN...', - '...NWWWWLLWWWWWWWWLLWWWWN...', - '...NWWWWWWWWWWWWWWWWWWWWN...', - '...NWWGGGGGGGGGGGGGGGGWWN...', - '...NWWGTTTGGTTGGTTGGTTGWN...', - '...NWWGGGGGGGGGGGGGGGGWWN...', - '...NWWGTTTGGTTGGTTGGTTGWN...', - '...NWWGGGGGGGGGGGGGGGGWWN...', - '...NWWGGTTGGGGGGGGTTGGWWN...', - '...NWWGGTTGGGNNDGGTTGGWWN...', - '...NWWGGGGGGGNNDGGGGGGWWN...', - '...NWWWWWWWWWWDDWWWWWWWWN...', - '...NWWWWWWWWWWDDWWWWWWWWN...', - '....NWWWWWWWWNNNNWWWWWWN....', - '....NNNNNNNNN....NNNNNN.....', - '............................', - '............................' - ], { - N: '#222633', Y: '#e7c36c', P: '#704944', W: '#8a6a4a', L: '#ffeeb5', G: '#d7c0a0', T: '#6b5139', D: '#382c26' - }, 28, 24); - } - - function drawVelvetTheater() { - return artRowsSized([ - '....NNNNNNNNNNNNNNNNNNNN.....', - '...NYYYYYYYYYYYYYYYYYYYYN....', - '..NYYNNNNNNNNNNNNNNNNNNYYN...', - '..NYNPPPPPPPPPPPPPPPPPPNYN...', - '.NNNNNNNNNNNNNNNNNNNNNNNNNN..', - '.NWWWWWWWWWWWWWWWWWWWWWWWWN..', - '.NWWLLLLLLLLWWWWLLLLLLLLWWN..', - '.NWWLAAAAALLWWWWLLAAAAALWWN..', - '.NWWLLLLLLLLWWWWLLLLLLLLWWN..', - '.NWWWWWWWWWWWWWWWWWWWWWWWWN..', - '.NWWGGGGGGGGGGGGGGGGGGGGWWN..', - '.NWWGTTGGTTGGGGGGTTGGTTGWWN..', - '.NWWGGGGGGGGGNNGGGGGGGGGWWN..', - '.NWWGTTGGTTGGNDDNGGTTGGTGWWN.', - '.NWWGGGGGGGGGNDDNGGGGGGGGWWN.', - '.NWWWWWWWWWWWDDDWWWWWWWWWWN..', - '.NWWWWWWWWWWNDDDNWWWWWWWWWN..', - '.NNNNNNNNNNNNNNNNNNNNNNNNNN..', - '............................', - '............................' - ], { - N: '#231f2c', Y: '#f2bf68', P: '#7f2d47', W: '#5d4858', L: '#ffd692', A: '#f08ec0', G: '#b99a7d', T: '#654633', D: '#39262a' - }, 28, 20); - } - - function drawBloomBridgeGate() { - return artRowsSized([ - '..........NNNN..........', - '........NNYYYYNN........', - '.......NYYPPPPYYN.......', - '......NYYPYYYYPYYN......', - '.....NYYPYYNNYYPYYN.....', - '....NNYYYYN..NYYYYNN....', - '...NNWWWWWN..NWWWWWNN...', - '..NNWWWNNWWNNWWNNWWWNN..', - '..NWWWN..NWWWWN..NWWWN..', - '..NWWN....NWWN....NWWN..', - '..NWWN....NWWN....NWWN..', - '..NWWN....NWWN....NWWN..', - '..NWWNNNNNNWWNNNNNNWWN..', - '..NWWGGGGGGWWGGGGGGWWN..', - '..NWWGTTTGGWWGGTTTGWWN..', - '..NWWGGGGGGWWGGGGGGWWN..', - '..NWWPPPPPPWWPPPPPPWWN..', - '..NWWPPYYPPWWPPYYPPWWN..', - '..NWWPPPPPPWWPPPPPPWWN..', - '...NNWWWWWWWWWWWWWWNN...', - '....NNNNNNWWNNNNNNNN....', - '........NNWWNN..........', - '........NWWWWN..........', - '........NNNNNN..........' - ], { - N: '#242835', Y: '#f6d56c', P: '#f29fc3', W: '#7f6a5a', G: '#9aca7c', T: '#5b8e49' - }, 24, 24); - } - - function drawStarreefSubmarine() { - return artRowsSized([ - '................................', - '.............NNNNNN.............', - '..........NNNYYYYYYNNN..........', - '........NNYYCCCCCCCCYYNN........', - '......NNYYCCBBBBBBBBCCYYNN......', - '.....NYYCCBBBBBBBBBBBBCCYYN.....', - '....NYYCBBBBBNNNNBBBBBBCYYN.....', - '...NYYCBBBBBNGGGGNBBBBBCYYN.....', - '..NNYYBBBBBBNGTTGNBBBBBBYYNN....', - '.NNYYYYYYYYYNGGGGNYYYYYYYYYNN...', - '.NWWWWWWWWWWNNNNNNWWWWWWWWWWWN..', - '.NWWWWLLWWWWWWWWWWWWLLWWWWWWWN..', - '.NNWWWWWWWWWWWWWWWWWWWWWWWWWNN..', - '..NNNNNNNNNNNNNNNNNNNNNNNNNN....', - '................................', - '................................' - ], { - N: '#1f2431', Y: '#f4dc7a', C: '#d0f1ff', B: '#57b7d7', W: '#73544d', G: '#eaf5ff', T: '#89b2ff', L: '#fff2b6' - }, 32, 16); - } - - function drawAstrolabeCourierRight() { - return artRowsSized([ - '....NNNN....', - '...NYYYYN...', - '...NYYYYN...', - '...NNNNNN...', - '..NNWWWWNN..', - '..NWWCCWWN..', - '..NWWWCWWN..', - '...NWWWWN...', - '..NNWWWWNN..', - '.NNWWNNWWNN.', - '.NWWN..NWWN.', - '.NWWNNNNWWN.', - '.NWWWGGWWWN.', - '.NWWNGGNWWN.', - '..NNN..NNN..', - '...N....N...' - ], { - N: '#232833', Y: '#d6a17a', W: '#375476', C: '#d9eeff', G: '#9f6e46' - }, 12, 16); - } - - function drawPuddleToadRight() { - return artRowsSized([ - '............', - '...NN..NN...', - '..NYYNNYYN..', - '.NYYYYYYYYN.', - '.NYYNYYNYYN.', - '.NYYYYYYYYN.', - '.NWWYYYYWWN.', - '..NWWWWWWN..', - '...NNNNNN...', - '............' - ], { - N: '#202632', Y: '#7eb85c', W: '#d9eeff' - }, 12, 10); - } - - function drawRibbonSwanRight() { - return artRowsSized([ - '............', - '......NN....', - '.....NYYN...', - '....NYYYYN..', - '...NYYNNYYN.', - '.NNNYYNNYYN.', - 'NWWWWYYYYYN.', - 'NWWWWWWWWNN.', - '.NWWWWWWWN..', - '..NWWWWWN...', - '...NNNNN....', - '............' - ], { - N: '#222633', Y: '#ffd3dc', W: '#f7fbff' - }, 12, 12); - } - - function drawLanternBeetleRight() { - return artRowsSized([ - '............', - '....NNNN....', - '...NYYYYN...', - '..NYYCCYYN..', - '..NYYCCYYN..', - '.NNYYNNYYNN.', - '.NWWYYYYWWN.', - '..NWWWWWWN..', - '..NWN..NWN..', - '.NNN....NNN.', - '.N........N.', - '............' - ], { - N: '#202531', Y: '#f0d25f', C: '#fff1b5', W: '#7d4933' - }, 12, 12); - } - - function drawSmilePortrait() { - return artRowsSized([ - '......FFFFFFFFFFFF......', - '.....FNNNNNNNNNNNNF.....', - '....FNBBBBBBBBBBBBNF....', - '....FNBHHHHHHHHHHBNF....', - '....FNBHHHSSSSHHHBNF....', - '....FNBHHSSSSSSHHBNF....', - '....FNBHHSSSSSSHHBNF....', - '....FNBHHSEEESHHHBNF....', - '....FNBHHSSSSSSHHBNF....', - '....FNBHHHSDDSSHHBNF....', - '....FNBGGGSSSSGGGBNF....', - '....FNBGGGGGGGGGGBNF....', - '....FNBGGGGGGGGGGBNF....', - '....FNBGGGGGGGGGGBNF....', - '....FNBGGGGGGGGGGBNF....', - '....FNBGGTTGGGGTGBNF....', - '....FNBGTTTTGGTTTBNF....', - '....FNBGTTTTGGTTTBNF....', - '....FNBGGTTTGGTTGBNF....', - '....FNBGGGGGGGGGGBNF....', - '....FNBGGGGDDGGGGBNF....', - '....FNBGGGGDDGGGGBNF....', - '....FNBGGGGDDGGGGBNF....', - '....FNBGGGGDDGGGGBNF....', - '....FNBGGGGDDGGGGBNF....', - '....FNBGGGDDDDGGGBNF....', - '....FNBGGGDDDDGGGBNF....', - '....FNBBBBBBBBBBBBNF....', - '....FNNNNNNNNNNNNNNF....', - '.....FFFFFFFFFFFFFF.....', - '........................', - '........................' - ], { - F: '#a67c37', N: '#252836', B: '#67816b', H: '#4b3228', S: '#d5ad87', E: '#242834', D: '#5f4130', G: '#4e6b3f', T: '#395835' - }, 24, 32); - } - - function drawGreatWavePanel() { - return artRowsSized([ - '....FFFFFFFFFFFFFFFFFFFFFFFF....', - '...FNNNNNNNNNNNNNNNNNNNNNNNNF...', - '..FNCCCCCCCCCCCCCCCCCCCCCCCCNF..', - '..FNCWWWWWWWWWWWWWWWWWWCCCCCNF..', - '..FNCWWWWWWWWWWWWWWWWWCCCCCCNF..', - '..FNCWWWWWWWWWWWWWWCCCCCYYCCNF..', - '..FNCWWWWWWWWWWWWCCCCCCCCCMMNF..', - '..FNCWWWWWWWWWWCCCCCAAACCCCMNF..', - '..FNCWWWWWWWWCCCCCAAAAAACCCMNF..', - '..FNCWWWWWWCCCCCAAAWWWAAACCMNF..', - '..FNCWWWWCCCCCAAAWWWWWWAACCMNF..', - '..FNCWWWCCCAAACWWWWWWWWWACCMNF..', - '..FNCCWCCAAAACWWWWWWWWWWWCCMNF..', - '..FNCCCAAAAACWWWWWWWWWWWWWCMNF..', - '..FNCCAAAAACWWWWWWWWWWWWWWMNF..', - '..FNCCAAAACCWWWWWWWWWWWWWWMNF..', - '..FNCCCAACCCWWWWWWWWWWWWWWMNF..', - '..FNCCCCCCCCWWWWWWWWWWWWWWMNF..', - '..FNCCMMMMMCCCCCMMMMMMMCCCMNF..', - '..FNCCMMMMMMMCCMMMMMMMMCCCMNF..', - '..FNCCMMMMMMMMMMMMMMMMMCCCMNF..', - '..FNCCMMMMMMMMMMMMMMMMMCCCMNF..', - '..FNCCCCCCCCCCCCCCCCCCCCCCCNF..', - '..FNNNNNNNNNNNNNNNNNNNNNNNNNF..' - ], { - F: '#a67c37', N: '#252836', C: '#d3eaf9', W: '#4a97d6', A: '#ffffff', Y: '#f3d36a', M: '#745f56' - }, 32, 24); - } - - function drawSunflowerStillLife() { - return artRowsSized([ - '........NN..NN..........', - '.......NYYNNYYN.........', - '......NYYYYYYYYN........', - '.......NYYNNYYN.........', - '........NN..NN..........', - '..........GG............', - '.....NN..NN..NN..NN.....', - '....NYYNNYYNNYYNNYYN....', - '...NYYYYYYYYYYYYYYYYN...', - '....NYYNNYYNNYYNNYYN....', - '.....NN..NN..NN..NN.....', - '...........GG...........', - '......NN..NN..NN........', - '.....NYYNNYYNNYYN.......', - '....NYYYYYYYYYYYYN......', - '.....NYYNNYYNNYYN.......', - '......NN..NN..NN........', - '........GGGG............', - '........GGGG............', - '.......NBBBBBBN.........', - '......NBBBBBBBBN........', - '......NBBBBBBBBN........', - '......NBBBBBBBBN........', - '.......NBBBBBBN.........', - '........NBBBBN..........', - '........NBBBBN..........', - '.......NMMMMMMN.........', - '......NMMMMMMMMN........', - '......NNNNNNNNNN........', - '........................', - '........................', - '........................' - ], { - N: '#252836', Y: '#f0cc57', G: '#5e8b48', B: '#b48255', M: '#8c6a53' - }, 24, 32); - } - - function drawRosettaStela() { - return artRowsSized([ - '........NNNNNNNN........', - '.......NSSSSSSSSN.......', - '......NSSSSSSSSSSN......', - '......NSSSSSSSSSSN......', - '......NSSLLLSSLLSN......', - '......NSSSSSSSSSSN......', - '......NSSLLSSLLSSN......', - '......NSSSSSSSSSSN......', - '......NSSSLLLSSSSN......', - '......NSSSSSSSSSSN......', - '......NSSLLSSSSLSN......', - '......NSSSSSSSSSSN......', - '......NSSLLLSSLLSN......', - '......NSSSSSSSSSSN......', - '......NSSLLSSLLSSN......', - '......NSSSSSSSSSSN......', - '......NSSSLLLSSSSN......', - '......NSSSSSSSSSSN......', - '......NSSLLSSSSLSN......', - '......NSSSSSSSSSSN......', - '......NSSLLLSSLLSN......', - '......NSSSSSSSSSSN......', - '......NSSLLSSLLSSN......', - '......NSSSSSSSSSSN......', - '......NSSSLLLSSSSN......', - '......NSSSSSSSSSSN......', - '......NSSSSDDSSSSN......', - '......NSSSSDDSSSSN......', - '......NSSSSDDSSSSN......', - '.......NSSSDDSSSN.......', - '........NNNNNNNN........', - '........................' - ], { - N: '#252836', S: '#887b66', L: '#5d5446', D: '#6e5842' - }, 24, 32); - } - - function drawPharaohMask() { - return artRowsSized([ - '.........NNNNNN.........', - '........NYYYYYYN........', - '.......NYYBBBBYYN.......', - '......NYYBBBBBBYYN......', - '......NYYBSSSSBYYN......', - '......NYYBSSSSBYYN......', - '......NYYBSDDSBYYN......', - '......NYYBSEESBYYN......', - '......NYYBSSSSBYYN......', - '......NYYBSDDSBYYN......', - '......NYYBSSSSBYYN......', - '......NYYBSSSSBYYN......', - '......NYYBSDDSBYYN......', - '......NYYBSSSSBYYN......', - '......NYYBSSSSBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '......NYYBBSSBBYYN......', - '.......NYYBSSBYYN.......', - '........NYYSSYYN........', - '........NYYSSYYN........', - '.........NNNNNN.........', - '........................' - ], { - N: '#252836', Y: '#e0b94d', B: '#4a78bf', S: '#d7b585', E: '#232834', D: '#8c6035' - }, 24, 32); - } - - function drawStoneCircle() { - return artRowsSized([ - '................................', - '................................', - '............NN..NN............', - '..........NNSSNNSSNN..........', - '........NNSSSSSSSSSSNN........', - '.......NSSSSSSSSSSSSSSN.......', - '......NSSSSSSSSSSSSSSSSN......', - '.....NSSSNSSSN..NSSSNSSSN.....', - '....NSSSNSSSSN..NSSSSNSSSN....', - '....NSSSNSSSSN..NSSSSNSSSN....', - '....NSSSNSSSSN..NSSSSNSSSN....', - '....NSSSNSSSSN..NSSSSNSSSN....', - '....NSSSNSSSSNNNNSSSSNSSSN....', - '....NSSSNSSSSSSSSSSSSNSSSN....', - '....NSSSNSSSSSSSSSSSSNSSSN....', - '.....NSSSNSSSSSSSSSSNSSSN.....', - '......NSSSSSSSSSSSSSSSSN......', - '.......NSSSSSSSSSSSSSSN.......', - '........NNNNNNNNNNNNNN........', - '........GGGGGGGGGGGGGG........', - '.......GGTTGGGGGGGGTTGG.......', - '.......GGGGGGGGDDGGGGGG.......', - '.......GGGGGGGGDDGGGGGG.......', - '........NNNNNNNNNNNNNN........' - ], { - N: '#252836', S: '#8b8274', G: '#93b670', T: '#5d8d46', D: '#6c4a35' - }, 32, 24); - } - - function drawTerracottaSentinel() { - return artRowsSized([ - '.......NNNNNN.......', - '......NBBBBBBN......', - '.....NBBSSSSBBN.....', - '.....NBSSSSSSBN.....', - '.....NBSSDDSSBN.....', - '.....NBSSEESSBN.....', - '.....NBSSSSSSBN.....', - '.....NBBSSSSBBN.....', - '......NBBBBBBN......', - '......NBBBBBBN......', - '......NBBBBBBN......', - '......NBBBBBBN......', - '......NBBBBBBN......', - '.....NBBBBBBBBN.....', - '.....NBBBGGBBBN.....', - '.....NBBBGGBBBN.....', - '.....NBBBGGBBBN.....', - '.....NBBBGGBBBN.....', - '.....NBBBGGBBBN.....', - '.....NBBBBBBBBN.....', - '.....NBBNNNNBBN.....', - '.....NBN....NBN.....', - '....NBBN....NBBN....', - '....NBBN....NBBN....', - '....NBBN....NBBN....', - '....NBBN....NBBN....', - '....NBBN....NBBN....', - '....NBBN....NBBN....', - '....NMMN....NMMN....', - '....NMMN....NMMN....', - '.....NN......NN.....', - '....................' - ], { - N: '#252836', B: '#9a6a4f', S: '#be8964', E: '#1f2230', D: '#6f4838', G: '#7a5746', M: '#6b5143' - }, 20, 32); - } - - function drawRainBellTower() { - return artRowsSized([ - '........NNNN........', - '.......NYYYYN.......', - '......NYYYYYYN......', - '......NYYNNYYN......', - '......NNNWWNNN......', - '.......NWWWWN.......', - '.......NWWWWN.......', - '......NNWWWWNN......', - '......NWWGGWWN......', - '......NWWGGWWN......', - '......NWWGGWWN......', - '......NWWLLWWN......', - '......NWWGGWWN......', - '.....NNWWWWWWNN.....', - '.....NWWGGGGWWN.....', - '.....NWWGTTGWWN.....', - '.....NWWGGGGWWN.....', - '.....NWWWWWWWWN.....', - '.....NWWWLLWWWN.....', - '.....NWWWGGWWWN.....', - '.....NWWWGGWWWN.....', - '.....NWWWGGWWWN.....', - '....NNWWWGGWWWNN....', - '....NWWWWWWWWWWN....', - '....NWWWGGGGWWWN....', - '....NWWWGTTGWWWN....', - '....NWWWGGGGWWWN....', - '....NWWWWWWWWWWN....', - '....NWWWWDDWWWWN....', - '....NWWWWDDWWWWN....', - '....NWWGGDDGGWWN....', - '....NWWGGDDGGWWN....', - '....NWWWWDDWWWWN....', - '.....NNNNDDNNNN.....', - '........NNNN........', - '....................' - ], {N:'#242836',Y:'#f4d276',W:'#d9d8df',G:'#a49da3',L:'#fff0b6',T:'#6a5245',D:'#40322c'}, 20, 36); - } - - function drawCrimsonPagoda() { - return artRowsSized([ - '.........NNNNNN.........', - '.......NNYYYYYYNN.......', - '......NYYYYYYYYYYN......', - '......NNNNWWNNNNNN......', - '.....NNRRRRRRRRRRNN.....', - '.....NWRRRRRRRRRRWN.....', - '....NNWWWWWWWWWWWWNN....', - '....NWWWWWWWWWWWWWWN....', - '...NNRRRRRRRRRRRRRRNN...', - '...NWRRLLRRRRRRLLRRWN...', - '...NWWWWWWWWWWWWWWWWN...', - '..NNWWWWWWWWWWWWWWWWNN..', - '..NWRRRRRRRRRRRRRRRRWN..', - '..NWRRLLRRRNNRRRLLRRWN..', - '..NWWWWWWWWNNWWWWWWWWN..', - '.NNWWWWWWWWWWWWWWWWWWNN.', - '.NWRRRRRRRRRRRRRRRRRRWN.', - '.NWRRLLRRRRRRRRRRLLRRWN.', - '.NWWWWWWWWWWWWWWWWWWWWN.', - '.NWWGGGGGGGGGGGGGGGGWWN.', - '.NWWGTTTGGTTTTGGTTTGWWN.', - '.NWWGGGGGGGGGGGGGGGGWWN.', - '.NWWWWWWWWWWWWWWWWWWWWN.', - '.NWWWWWWWWWWWWWWWWWWWWN.', - '.NWWGGGGGGGGDDGGGGGGWWN.', - '.NWWGTTTGGGGDDGGGTTGWWN.', - '.NWWGGGGGGGGDDGGGGGGWWN.', - '.NWWWWWWWWWWDDWWWWWWWWN.', - '.NWWWWWWWWWWDDWWWWWWWWN.', - '..NNNNNNNNNNNNNNNNNNNN..', - '........................', - '........................' - ], {N:'#242833',Y:'#efc05e',W:'#4f403f',R:'#9c4354',L:'#ffd99f',G:'#cbb28d',T:'#6c513b',D:'#362a25'}, 24, 32); - } - - function drawMeteorForge() { - return artRowsSized([ - '................................', - '........NNNNNNNNNNNN............', - '......NNYYYYYYYYYYYYNN..........', - '.....NYYNNNNNNNNNNNNYYN.........', - '....NNNNNNNNNNNNNNNNNNNN........', - '...NWWWWWWWWWWWWWWWWWWWWN.......', - '..NNWWWLLLLWWWWWWLLLLWWWNN......', - '..NWWWLLLLLLWWWWLLLLLLWWWN......', - '..NWWWWLLLLLWWWWLLLLLWWWWN......', - '..NWWWWWWWWWWWWWWWWWWWWWWN......', - '.NNWWWWWWWWWWWWWWWWWWWWWWNN.....', - '.NWWGGGGGGGGGGGGGGGGGGGGWWN.....', - '.NWWGTTGGGTTGGGGGGTTGGGTGWN.....', - '.NWWGGGGGGGGGNNNNGGGGGGGGWN.....', - '.NWWGTTGGGGGGNDDNGGGGGTTGWN.....', - '.NWWGGGGGGGGGNDDNGGGGGGGGWN.....', - '.NWWWWWWWWWWWWDDWWWWWWWWWWN.....', - '.NWWWWLLWWWWWWDDWWWWWWLLWWN.....', - '.NWWWWWWWWWWWWDDWWWWWWWWWWN.....', - '.NWWGGGGGGGGGGDDGGGGGGGGWWN.....', - '.NWWGTTTGGGGGGDDGGGGGTTTGWN.....', - '.NWWGGGGGGGGGGDDGGGGGGGGWWN.....', - '.NNNNNNNNNNNNNNNNNNNNNNNNNN.....', - '................................' - ], {N:'#242833',Y:'#6f8fd8',W:'#5f544f',L:'#ffcf8a',G:'#b9916a',T:'#ff8f52',D:'#352924'}, 32, 24); - } - - function drawInkGardenScreen() { - return artRowsSized([ - 'NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NCMMMCCCCCCCCCCCCCCCCCCCCCCMMM CN'.replace(/ /g,''), - 'NCMMMMCCCCCCCCCCCCCCCCCCCCMMMMCN', - 'NCMMMMMCCCCCCCGGGGCCCCCCMMMMMCN', - 'NCMMMMMMCCCCCGGGGGGCCCCMMMMMMCN', - 'NCMMMMMMMCCCCGGGGGGCCCCMMMMMMCN', - 'NCMMMMMMCCCCCGGGGGGCCCCMMMMMMCN', - 'NCMMMMMCCCCCCCGGGGCCCCMMMMMMCN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NCCCCCCCCCCCCCNNNNCCCCCCCCCCCCN', - 'NCCCCCCCCCCCCNDDDDNCCCCCCCCCCCN', - 'NCCCCCCCCCCCCNDDDDNCCCCCCCCCCCN', - 'NCCCCCCCCCCCCNNNNNNCCCCCCCCCCCN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NCGGGCCCCCCCCCCCCCCCCCCCCGGGCCN', - 'NCGTGCCCCCCCCCCCCCCCCCCCCGTGCCN', - 'NCGGGCCCCCCCCCCCCCCCCCCCCGGGCCN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NCCCCCCCCCCCCCCCCCCCCCCCCCCCCCN', - 'NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN' - ], {N:'#252836',C:'#ece6d8',M:'#2a554c',G:'#7ab08a',T:'#4f8a5e',D:'#716159'}, 32, 24); - } - - function drawMeadowTotem() { - return artRowsSized([ - '........NNNN........', - '.......NYYYYN.......', - '......NYYGGYYN......', - '......NYYGGYYN......', - '......NYYNNYYN......', - '......NYYSSYYN......', - '......NYYSSYYN......', - '......NYYNNYYN......', - '......NYYGGYYN......', - '......NYYGGYYN......', - '......NYYSSYYN......', - '......NYYSSYYN......', - '......NYYNNYYN......', - '......NYYYYYYN......', - '......NYYYYYYN......', - '......NYYYYYYN......', - '......NYYYYYYN......', - '......NYYYYYYN......', - '......NYYYYYYN......', - '......NYYYYYYN......', - '......NYYYYYYN......', - '......NYYDDYYN......', - '......NYYDDYYN......', - '......NYYDDYYN......', - '......NYYDDYYN......', - '.......NDDDDN.......', - '........NNNN........', - '....................' - ], {N:'#252836',Y:'#a57b5c',G:'#97cb6f',S:'#f1d691',D:'#5b4337'}, 20, 28); - } - - function drawTwilightCaravan() { - return artRowsSized([ - '................................', - '...........NNNNNNNN.............', - '........NNNYYYYYYYYNN...........', - '......NNYYPPPPPPPPYYNN..........', - '.....NYYPPPPPPPPPPPPYYN.........', - '....NYYPPPPPPPPPPPPPPYYN........', - '...NNWWWWWWWWWWWWWWWWWWNN.......', - '...NWWWWLLWWWWLLWWWWLLWWN.......', - '...NWWWWWWWWWWWWWWWWWWWWN.......', - '...NWWGGGGGGGGGGGGGGGGWWN.......', - '...NWWGTTTGGTTGGTTGGTTGWN.......', - '...NWWGGGGGGGGGGGGGGGGWWN.......', - '..NNWWWWWWWWWWWWWWWWWWWWNN......', - '..NWWWWNNWWWWNNWWWWNNWWWWN......', - '..NWWWN..NWWWN..NWWWN..NWWN......', - '..NNNN....NNNN....NNNN..NN......' - ], {N:'#252836',Y:'#c49b5b',P:'#7e5aa8',W:'#7b6654',L:'#ffe09c',G:'#8db873',T:'#5d8b45'}, 32, 16); - } - - function drawPepperFoxRight() { - return artRowsSized([ - '................', - '................', - '.......NN.......', - '......NYYN......', - '.....NYYYYNN....', - '....NYYYYYYN....', - '...NYYYYYYYYN...', - '...NYYYYNYYYN...', - '..NYYYYYYYNWN...', - '..NYYYYYYYWWN...', - '..NYYYYYYWWN....', - '...NYYYYWWN.....', - '....NYYWWWNN....', - '....NWWWWWWN....', - '.....NN..NN.....', - '................' - ], {N:'#252836',Y:'#d56f3f',W:'#fff1dd'}, 16, 16); - } - - function drawGlassMantaRight() { - return artRowsSized([ - '................', - '.......NN.......', - '.....NNCCNN.....', - '...NNCCCCCCNN...', - '..NCCCCCCCCCCN..', - '.NCCCCCCCCCCCCN.', - 'NNCCCCCCCCCCCCNN', - '.NCCCCCCCCCCCCN.', - '..NNCCCCCCCCNN..', - '....NNCCCCNN....', - '......NCCN......', - '.......NN.......' - ], {N:'#252836',C:'#7fdaf2'}, 16, 12); - } - - function drawFestivalDrummerRight() { - return artRowsSized([ - '.....NNNN.....', - '....NYYYYN....', - '....NYYYYN....', - '....NNNNNN....', - '...NNRRRRNN...', - '...NRRWWRRN...', - '...NRRWWRRN...', - '....NWWWWN....', - '..NNNWWWWNN...', - '.NNWWNNNNWWN..', - '.NWWN.TTNWWN..', - '.NWWNNNNNWWN..', - '.NWWN....NWN..', - '.NWWN....NWN..', - '..NN......NN..', - '..............' - ], {N:'#252836',Y:'#e0b18c',R:'#d4505f',W:'#425fb4',T:'#a26e4b'}, 14, 16); - } - - function drawBloomSpriteRight() { - return artRowsSized([ - '............', - '....NNNN....', - '...NYYYYN...', - '..NYYGGYYN..', - '..NYYGGYYN..', - '.NNYYYYYYNN.', - '.NWWYYYYWWN.', - '..NWWWWWWN..', - '..NWN..NWN..', - '.NN....NNN..', - '............', - '............' - ], {N:'#252836',Y:'#ffd3a8',G:'#ff8fc0',W:'#7bc56a'}, 12, 12); - } - - - - function drawMidnightRamenCart() { - const w = 28, h = 20, p = blankPixels(w, h); - const N = '#252836', R = '#b83b4a', Y = '#ffd58a', W = '#fff7d2', D = '#65433b', B = '#75d7e8', S = '#f7eee6'; - rectWH(p, w, h, 4, 8, 20, 8, N); rectWH(p, w, h, 5, 9, 18, 6, D); - rectWH(p, w, h, 3, 6, 22, 3, N); rectWH(p, w, h, 4, 6, 20, 2, R); - for (let x = 5; x < 24; x += 4) rectWH(p, w, h, x, 6, 2, 2, Y); - rectWH(p, w, h, 7, 10, 4, 3, W); rectWH(p, w, h, 15, 10, 5, 3, B); rectWH(p, w, h, 11, 13, 4, 3, N); rectWH(p, w, h, 12, 13, 2, 3, S); - rectWH(p, w, h, 4, 16, 20, 2, N); ellipseWH(p, w, h, 8, 18, 2, 2, N); ellipseWH(p, w, h, 20, 18, 2, 2, N); pxWH(p, w, h, 8, 18, B); pxWH(p, w, h, 20, 18, B); - rectWH(p, w, h, 24, 10, 2, 7, N); pxWH(p, w, h, 25, 9, Y); pxWH(p, w, h, 25, 8, R); - return p; - } - - function drawQuartzClocktower() { - const w = 22, h = 34, p = blankPixels(w, h); - const N = '#252836', Q = '#cbeef5', C = '#edfaff', B = '#7894ad', G = '#4e6680', Y = '#ffe6a4'; - rectWH(p, w, h, 7, 12, 8, 19, N); rectWH(p, w, h, 8, 13, 6, 17, Q); - rectWH(p, w, h, 6, 30, 10, 2, G); rectWH(p, w, h, 9, 25, 4, 6, N); rectWH(p, w, h, 10, 25, 2, 6, B); - for (let y = 4; y <= 12; y++) { const inset = Math.abs(12 - y) / 2 | 0; rectWH(p, w, h, 5 + inset, y, 12 - inset * 2, 1, N); rectWH(p, w, h, 6 + inset, y, 10 - inset * 2, 1, C); } - ellipseWH(p, w, h, 11, 9, 4, 4, N); ellipseWH(p, w, h, 11, 9, 3, 3, C); lineWH(p, w, h, 11, 9, 11, 6, G); lineWH(p, w, h, 11, 9, 14, 10, G); - rectWH(p, w, h, 9, 16, 4, 3, Y); rectWH(p, w, h, 9, 21, 4, 3, Y); pxWH(p, w, h, 11, 2, C); pxWH(p, w, h, 10, 3, B); pxWH(p, w, h, 12, 3, B); - return p; - } - - function drawLotusTeaPavilion() { - const w = 30, h = 24, p = blankPixels(w, h); - const N = '#252836', P = '#ef86b8', D = '#7b5b6e', W = '#fff0cf', G = '#8dc985', B = '#8fd9e8', Y = '#ffe7a4'; - ellipseWH(p, w, h, 15, 20, 13, 3, B); rectWH(p, w, h, 5, 14, 20, 7, N); rectWH(p, w, h, 6, 15, 18, 5, W); - rectWH(p, w, h, 4, 11, 22, 3, N); rectWH(p, w, h, 5, 11, 20, 2, P); rectWH(p, w, h, 8, 9, 14, 2, N); rectWH(p, w, h, 9, 9, 12, 1, D); - rectWH(p, w, h, 9, 15, 4, 3, Y); rectWH(p, w, h, 17, 15, 4, 3, Y); rectWH(p, w, h, 13, 17, 4, 4, D); - for (let i = 0; i < 5; i++) { ellipseWH(p, w, h, 8 + i * 3, 7 + (i % 2), 2, 1, G); pxWH(p, w, h, 8 + i * 3, 7 + (i % 2), P); } - return p; - } - - function drawMeteoriteGarden() { - const w = 26, h = 20, p = blankPixels(w, h); - const N = '#252836', M = '#646c86', L = '#b7e9ff', V = '#8b7bd6', G = '#86b66d', D = '#3d4c5a'; - rectWH(p, w, h, 2, 17, 22, 2, G); ellipseWH(p, w, h, 13, 10, 6, 5, N); ellipseWH(p, w, h, 13, 10, 5, 4, M); ellipseWH(p, w, h, 14, 9, 2, 2, L); - pxWH(p, w, h, 9, 11, D); pxWH(p, w, h, 17, 12, D); pxWH(p, w, h, 11, 7, V); - for (let i = 0; i < 5; i++) { lineWH(p, w, h, 4 + i * 4, 15, 5 + i * 4, 12 - (i % 2), D); pxWH(p, w, h, 5 + i * 4, 12 - (i % 2), L); } - rectWH(p, w, h, 6, 16, 14, 1, N); - return p; - } - - function drawAzureGlasshouse() { - const w = 28, h = 26, p = blankPixels(w, h); - const N = '#252836', A = '#7ed8ea', C = '#dffbff', G = '#7aba6b', D = '#43556f', Y = '#fff0a6'; - rectWH(p, w, h, 5, 12, 18, 11, N); rectWH(p, w, h, 6, 13, 16, 9, A); - for (let y = 5; y <= 12; y++) { const inset = Math.abs(12 - y); rectWH(p, w, h, 5 + inset, y, 18 - inset * 2, 1, N); rectWH(p, w, h, 6 + inset, y, 16 - inset * 2, 1, C); } - for (let x = 8; x < 22; x += 4) lineWH(p, w, h, x, 7, x, 22, D); - rectWH(p, w, h, 12, 18, 4, 5, N); rectWH(p, w, h, 13, 18, 2, 5, Y); rectWH(p, w, h, 7, 17, 3, 3, G); rectWH(p, w, h, 18, 16, 3, 4, G); - return p; - } - - function drawFoxfireTorii() { - const w = 24, h = 24, p = blankPixels(w, h); - const N = '#252836', R = '#d34b42', D = '#7d2f3a', F = '#ffb36b', Y = '#ffe58e'; - rectWH(p, w, h, 4, 5, 16, 3, N); rectWH(p, w, h, 5, 5, 14, 2, R); rectWH(p, w, h, 2, 8, 20, 2, N); rectWH(p, w, h, 3, 8, 18, 1, D); - rectWH(p, w, h, 6, 10, 3, 11, N); rectWH(p, w, h, 7, 10, 1, 11, R); rectWH(p, w, h, 15, 10, 3, 11, N); rectWH(p, w, h, 16, 10, 1, 11, R); - rectWH(p, w, h, 5, 20, 5, 2, D); rectWH(p, w, h, 14, 20, 5, 2, D); - ellipseWH(p, w, h, 6, 15, 2, 3, F); pxWH(p, w, h, 6, 14, Y); ellipseWH(p, w, h, 17, 15, 2, 3, F); pxWH(p, w, h, 17, 14, Y); - return p; - } - - function drawPearlCloudAltar() { - const w = 24, h = 22, p = blankPixels(w, h); - const N = '#252836', C = '#eaf8ff', B = '#b9d7ea', P = '#fff5d8', S = '#9b8cd7'; - ellipseWH(p, w, h, 8, 14, 5, 3, C); ellipseWH(p, w, h, 15, 13, 6, 4, C); ellipseWH(p, w, h, 12, 15, 8, 4, B); - rectWH(p, w, h, 5, 17, 14, 3, N); rectWH(p, w, h, 6, 17, 12, 2, B); rectWH(p, w, h, 8, 15, 8, 2, N); rectWH(p, w, h, 9, 15, 6, 1, C); - ellipseWH(p, w, h, 12, 9, 4, 4, N); ellipseWH(p, w, h, 12, 9, 3, 3, P); pxWH(p, w, h, 11, 8, C); pxWH(p, w, h, 14, 7, S); - return p; - } - - function drawClockworkConservatory() { - const w = 28, h = 28, p = blankPixels(w, h); - const N = '#252836', G = '#78c88e', C = '#c8f4ff', B = '#536886', M = '#c88b45', Y = '#ffe9a8'; - rectWH(p, w, h, 5, 13, 18, 12, N); rectWH(p, w, h, 6, 14, 16, 10, G); - ellipseWH(p, w, h, 14, 13, 9, 8, N); ellipseWH(p, w, h, 14, 13, 7, 6, C); rectWH(p, w, h, 6, 13, 16, 6, C); - lineWH(p, w, h, 14, 6, 14, 24, B); lineWH(p, w, h, 7, 14, 21, 14, B); lineWH(p, w, h, 9, 9, 19, 19, B); lineWH(p, w, h, 19, 9, 9, 19, B); - ellipseWH(p, w, h, 14, 13, 3, 3, N); ellipseWH(p, w, h, 14, 13, 2, 2, M); pxWH(p, w, h, 14, 13, Y); rectWH(p, w, h, 12, 21, 4, 4, N); rectWH(p, w, h, 13, 21, 2, 4, M); - return p; - } - - function drawInkcapMouseRight() { - const w = 12, h = 10, p = blankPixels(w, h); - const N = '#252836', M = '#7b7184', P = '#d7c0ff', E = '#ffe0ed'; - ellipseWH(p, w, h, 5, 6, 4, 2, N); ellipseWH(p, w, h, 5, 6, 3, 2, M); ellipseWH(p, w, h, 8, 5, 2, 2, N); pxWH(p, w, h, 9, 5, E); - ellipseWH(p, w, h, 4, 3, 3, 2, P); rectWH(p, w, h, 2, 3, 5, 2, N); pxWH(p, w, h, 9, 4, N); lineWH(p, w, h, 2, 7, 0, 6, M); pxWH(p, w, h, 4, 8, N); pxWH(p, w, h, 7, 8, N); - return p; - } - - function drawCometFinchRight() { - const w = 12, h = 10, p = blankPixels(w, h); - const N = '#252836', B = '#6fb7e8', Y = '#fff0a8', O = '#ff9f67', W = '#fff8d8'; - ellipseWH(p, w, h, 5, 5, 3, 2, N); ellipseWH(p, w, h, 5, 5, 2, 1, B); ellipseWH(p, w, h, 8, 4, 2, 2, N); pxWH(p, w, h, 9, 4, W); pxWH(p, w, h, 10, 4, O); - lineWH(p, w, h, 2, 5, 0, 3, Y); lineWH(p, w, h, 2, 6, 0, 7, O); lineWH(p, w, h, 4, 7, 4, 9, N); lineWH(p, w, h, 7, 6, 7, 8, N); pxWH(p, w, h, 8, 4, N); - return p; - } - - function drawLotusKappaRight() { - const w = 12, h = 14, p = blankPixels(w, h); - const N = '#252836', G = '#78b96d', D = '#3d8a63', P = '#f6a8cf', B = '#d8f8e0', Y = '#ffe68a'; - ellipseWH(p, w, h, 6, 5, 4, 4, N); ellipseWH(p, w, h, 6, 5, 3, 3, G); ellipseWH(p, w, h, 6, 2, 3, 1, P); pxWH(p, w, h, 8, 5, N); pxWH(p, w, h, 9, 6, Y); - rectWH(p, w, h, 4, 8, 5, 4, N); rectWH(p, w, h, 5, 8, 3, 4, D); pxWH(p, w, h, 4, 12, N); pxWH(p, w, h, 8, 12, N); pxWH(p, w, h, 3, 9, B); pxWH(p, w, h, 9, 9, B); - return p; - } - - function drawLanternMantaRight() { - const w = 14, h = 10, p = blankPixels(w, h); - const N = '#252836', A = '#5fc7d9', C = '#bff8ff', Y = '#ffe58e', D = '#3d8298'; - ellipseWH(p, w, h, 6, 5, 5, 3, N); ellipseWH(p, w, h, 6, 5, 4, 2, A); lineWH(p, w, h, 2, 5, 0, 3, D); lineWH(p, w, h, 2, 6, 0, 8, D); lineWH(p, w, h, 9, 5, 13, 4, D); lineWH(p, w, h, 9, 6, 13, 8, D); - pxWH(p, w, h, 8, 4, C); pxWH(p, w, h, 11, 6, Y); lineWH(p, w, h, 5, 8, 4, 9, C); lineWH(p, w, h, 7, 8, 7, 9, C); - return p; - } - - function drawPocketAstronautRight() { - const w = 12, h = 16, p = blankPixels(w, h); - const N = '#252836', W = '#f0f5ff', B = '#9be8ff', R = '#e85b72', G = '#77859e', Y = '#ffe58e'; - ellipseWH(p, w, h, 6, 4, 4, 4, N); ellipseWH(p, w, h, 6, 4, 3, 3, W); rectWH(p, w, h, 5, 3, 4, 2, B); pxWH(p, w, h, 8, 4, Y); - rectWH(p, w, h, 3, 8, 6, 5, N); rectWH(p, w, h, 4, 8, 4, 5, W); pxWH(p, w, h, 5, 9, R); pxWH(p, w, h, 7, 9, B); lineWH(p, w, h, 3, 9, 1, 11, G); lineWH(p, w, h, 9, 9, 11, 10, G); rectWH(p, w, h, 4, 13, 2, 2, N); rectWH(p, w, h, 7, 13, 2, 2, N); - return p; - } - - function drawMossyCapybaraRight() { - const w = 14, h = 10, p = blankPixels(w, h); - const N = '#252836', B = '#9b6b4e', D = '#684536', G = '#91c76b', W = '#fff1d8'; - ellipseWH(p, w, h, 6, 6, 5, 3, N); ellipseWH(p, w, h, 6, 6, 4, 2, B); ellipseWH(p, w, h, 10, 5, 3, 2, N); ellipseWH(p, w, h, 10, 5, 2, 1, B); pxWH(p, w, h, 11, 5, N); pxWH(p, w, h, 12, 6, W); - rectWH(p, w, h, 3, 3, 6, 2, G); pxWH(p, w, h, 4, 8, N); pxWH(p, w, h, 9, 8, N); lineWH(p, w, h, 1, 6, 0, 7, D); - return p; - } - - function drawOpalOctopusRight() { - const w = 12, h = 12, p = blankPixels(w, h); - const N = '#252836', P = '#e58ac8', O = '#ffcaec', B = '#aef8ff', D = '#9d61a8'; - ellipseWH(p, w, h, 6, 4, 4, 4, N); ellipseWH(p, w, h, 6, 4, 3, 3, P); pxWH(p, w, h, 8, 4, N); pxWH(p, w, h, 9, 5, O); pxWH(p, w, h, 5, 3, B); - for (let i = 0; i < 4; i++) lineWH(p, w, h, 3 + i * 2, 7, 2 + i * 2, 10, D); lineWH(p, w, h, 8, 7, 10, 10, D); - return p; - } - - function drawTinyOracleRight() { - const w = 10, h = 16, p = blankPixels(w, h); - const N = '#252836', V = '#806bd6', P = '#e6d6ff', S = '#f3d0b6', Y = '#fff0a8'; - ellipseWH(p, w, h, 5, 4, 3, 3, N); ellipseWH(p, w, h, 5, 4, 2, 2, S); pxWH(p, w, h, 6, 4, N); - rectWH(p, w, h, 3, 7, 5, 7, N); rectWH(p, w, h, 4, 7, 3, 7, V); rectWH(p, w, h, 4, 6, 3, 1, P); pxWH(p, w, h, 5, 9, Y); lineWH(p, w, h, 2, 9, 0, 8, P); lineWH(p, w, h, 8, 9, 9, 7, P); pxWH(p, w, h, 4, 14, N); pxWH(p, w, h, 6, 14, N); - return p; - } - - - function drawAmberApiary() { - const w = 24, h = 24, p = blankPixels(w, h); - const N = '#252836', H = '#d99937', Y = '#ffe07a', D = '#744b2d', G = '#6ea85d', C = '#fff0b4'; - rectWH(p, w, h, 6, 9, 12, 12, N); rectWH(p, w, h, 7, 10, 10, 10, H); - for (let y = 11; y <= 18; y += 3) rectWH(p, w, h, 7, y, 10, 1, Y); - rectWH(p, w, h, 9, 14, 6, 5, D); rectWH(p, w, h, 10, 15, 4, 3, N); - rectWH(p, w, h, 5, 7, 14, 3, N); rectWH(p, w, h, 6, 7, 12, 2, C); - for (let i = 0; i < 5; i++) { pxWH(p, w, h, 4 + i * 4, 4 + (i % 2), Y); pxWH(p, w, h, 5 + i * 4, 4 + (i % 2), N); pxWH(p, w, h, 6 + i * 4, 3 + (i % 2), C); } - rectWH(p, w, h, 3, 20, 18, 2, G); - return p; - } - - function drawGlassMoonDock() { - const w = 32, h = 16, p = blankPixels(w, h); - const N = '#252836', W = '#8b6a4a', L = '#c8a77a', B = '#8ae7ff', C = '#dffbff', Y = '#fff1a8'; - rectWH(p, w, h, 4, 11, 24, 3, N); rectWH(p, w, h, 5, 11, 22, 2, W); - for (let x = 7; x < 27; x += 4) rectWH(p, w, h, x, 8, 2, 5, L); - lineWH(p, w, h, 6, 7, 26, 7, N); lineWH(p, w, h, 8, 6, 24, 6, B); - ellipseWH(p, w, h, 16, 7, 6, 3, N); ellipseWH(p, w, h, 16, 7, 5, 2, C); ellipseWH(p, w, h, 18, 6, 3, 2, B); - pxWH(p, w, h, 8, 5, Y); pxWH(p, w, h, 24, 5, Y); pxWH(p, w, h, 16, 3, C); - return p; - } - - function drawBluebellChapel() { - const w = 22, h = 30, p = blankPixels(w, h); - const N = '#252836', B = '#7084d9', L = '#b9c9ff', W = '#fff4df', D = '#4a3d68', Y = '#fff1a8'; - rectWH(p, w, h, 4, 13, 14, 13, N); rectWH(p, w, h, 5, 14, 12, 11, W); - for (let y = 6; y <= 13; y++) { const inset = Math.abs(13 - y); rectWH(p, w, h, 5 + inset, y, 12 - inset * 2, 1, N); rectWH(p, w, h, 6 + inset, y, 10 - inset * 2, 1, B); } - rectWH(p, w, h, 9, 16, 4, 5, L); rectWH(p, w, h, 9, 23, 4, 3, D); pxWH(p, w, h, 11, 10, Y); - lineWH(p, w, h, 11, 2, 11, 7, N); lineWH(p, w, h, 9, 4, 13, 4, N); pxWH(p, w, h, 11, 3, Y); - rectWH(p, w, h, 4, 26, 14, 2, N); return p; - } - - function drawStardustOrchard() { - const w = 28, h = 24, p = blankPixels(w, h); - const N = '#252836', T = '#7a4d34', G = '#5eaa65', B = '#96db7d', P = '#d7b8ff', Y = '#fff1a8'; - for (const [cx, cy] of [[8, 12], [17, 10], [22, 15]]) { - rectWH(p, w, h, cx - 1, cy + 3, 3, 7, T); ellipseWH(p, w, h, cx, cy, 6, 5, N); ellipseWH(p, w, h, cx, cy, 5, 4, G); ellipseWH(p, w, h, cx - 2, cy - 1, 3, 2, B); - pxWH(p, w, h, cx + 2, cy - 2, Y); pxWH(p, w, h, cx - 3, cy + 1, P); - } - rectWH(p, w, h, 2, 21, 24, 2, '#78ad62'); - return p; - } - - function drawPrismFerrisWheel() { - const w = 32, h = 32, p = blankPixels(w, h); - const N = '#252836', C = '#bdf5ff', R = '#ff8db6', Y = '#ffe78e', G = '#8ee68e', B = '#89a8ff', D = '#6b5a7e'; - ellipseWH(p, w, h, 16, 12, 10, 10, N); ellipseWH(p, w, h, 16, 12, 8, 8, null); - lineWH(p, w, h, 16, 12, 16, 2, C); lineWH(p, w, h, 16, 12, 26, 12, C); lineWH(p, w, h, 16, 12, 16, 22, C); lineWH(p, w, h, 16, 12, 6, 12, C); - lineWH(p, w, h, 16, 12, 23, 5, C); lineWH(p, w, h, 16, 12, 23, 19, C); lineWH(p, w, h, 16, 12, 9, 19, C); lineWH(p, w, h, 16, 12, 9, 5, C); - const cars = [[16,2,R],[26,12,Y],[16,22,G],[6,12,B],[23,5,R],[23,19,G],[9,19,Y],[9,5,B]]; - for (const [x,y,col] of cars) { rectWH(p, w, h, x-2, y, 4, 3, N); rectWH(p, w, h, x-1, y+1, 2, 1, col); } - lineWH(p, w, h, 16, 14, 10, 29, D); lineWH(p, w, h, 16, 14, 22, 29, D); rectWH(p, w, h, 8, 29, 16, 2, N); - pxWH(p, w, h, 16, 12, Y); return p; - } - - function drawMossLanternGate() { - const w = 26, h = 24, p = blankPixels(w, h); - const N = '#252836', G = '#5f8f52', M = '#93c56b', W = '#8a6c4d', Y = '#ffe28a', C = '#d8ffca'; - rectWH(p, w, h, 4, 6, 18, 3, N); rectWH(p, w, h, 5, 6, 16, 2, G); - rectWH(p, w, h, 6, 8, 3, 13, N); rectWH(p, w, h, 17, 8, 3, 13, N); rectWH(p, w, h, 7, 9, 1, 11, W); rectWH(p, w, h, 18, 9, 1, 11, W); - lineWH(p, w, h, 5, 5, 13, 2, M); lineWH(p, w, h, 13, 2, 21, 5, M); - rectWH(p, w, h, 7, 11, 3, 4, N); rectWH(p, w, h, 8, 12, 1, 2, Y); rectWH(p, w, h, 17, 11, 3, 4, N); rectWH(p, w, h, 18, 12, 1, 2, Y); - rectWH(p, w, h, 4, 21, 18, 2, C); return p; - } - - function drawShellMusicStage() { - const w = 30, h = 22, p = blankPixels(w, h); - const N = '#252836', S = '#ffd3df', P = '#f29bc1', B = '#82d6ff', D = '#6e5062', Y = '#fff1a8'; - ellipseWH(p, w, h, 15, 10, 12, 8, N); ellipseWH(p, w, h, 15, 10, 10, 6, S); - for (let x = 8; x <= 22; x += 4) lineWH(p, w, h, 15, 4, x, 16, P); - rectWH(p, w, h, 4, 16, 22, 4, N); rectWH(p, w, h, 5, 16, 20, 3, D); - rectWH(p, w, h, 9, 11, 2, 6, N); rectWH(p, w, h, 20, 11, 2, 6, N); pxWH(p, w, h, 9, 10, Y); pxWH(p, w, h, 21, 10, Y); - lineWH(p, w, h, 13, 14, 17, 12, B); lineWH(p, w, h, 13, 15, 17, 15, B); return p; - } - - function drawTerracottaBathhouse() { - const w = 28, h = 26, p = blankPixels(w, h); - const N = '#252836', R = '#b85d44', O = '#e07d52', W = '#f4d2b1', D = '#614036', C = '#bdf5ff', Y = '#ffd66e'; - rectWH(p, w, h, 5, 10, 18, 12, N); rectWH(p, w, h, 6, 11, 16, 10, W); - rectWH(p, w, h, 4, 7, 20, 4, N); rectWH(p, w, h, 5, 7, 18, 3, R); rectWH(p, w, h, 7, 5, 14, 3, N); rectWH(p, w, h, 8, 5, 12, 2, O); - rectWH(p, w, h, 8, 14, 4, 4, C); rectWH(p, w, h, 16, 14, 4, 4, C); rectWH(p, w, h, 12, 17, 4, 5, D); pxWH(p, w, h, 14, 18, Y); - for (let i = 0; i < 5; i++) { pxWH(p, w, h, 9 + i * 3, 3 - (i % 2), C); pxWH(p, w, h, 10 + i * 3, 2 - (i % 2), '#ffffff'); } - rectWH(p, w, h, 4, 22, 20, 2, N); return p; - } - - function drawCopperHedgehogRight() { - const w = 12, h = 10, p = blankPixels(w, h); - const N = '#252836', C = '#b76a38', D = '#684536', S = '#f0c08a', Y = '#ffe99a'; - ellipseWH(p, w, h, 5, 6, 5, 3, N); ellipseWH(p, w, h, 5, 6, 4, 2, C); ellipseWH(p, w, h, 9, 5, 3, 2, N); ellipseWH(p, w, h, 9, 5, 2, 1, S); - for (let x = 2; x <= 7; x += 2) lineWH(p, w, h, x, 3, x + 1, 1, D); pxWH(p, w, h, 10, 5, N); pxWH(p, w, h, 11, 6, Y); pxWH(p, w, h, 3, 8, N); pxWH(p, w, h, 8, 8, N); - return p; - } - - function drawLilyCraneRight() { - const w = 12, h = 16, p = blankPixels(w, h); - const N = '#252836', W = '#f3f8ff', B = '#9ecaff', G = '#7ac776', Y = '#ffe07a'; - ellipseWH(p, w, h, 5, 8, 4, 3, N); ellipseWH(p, w, h, 5, 8, 3, 2, W); lineWH(p, w, h, 7, 7, 10, 4, N); lineWH(p, w, h, 8, 6, 10, 4, W); pxWH(p, w, h, 10, 4, Y); - lineWH(p, w, h, 4, 10, 4, 15, N); lineWH(p, w, h, 7, 10, 8, 15, N); rectWH(p, w, h, 3, 14, 3, 1, N); rectWH(p, w, h, 7, 14, 3, 1, N); - ellipseWH(p, w, h, 2, 9, 3, 2, B); pxWH(p, w, h, 3, 4, G); pxWH(p, w, h, 4, 3, '#ffd2ef'); return p; - } - - function drawPearlAnglerRight() { - const w = 12, h = 16, p = blankPixels(w, h); - const N = '#252836', S = '#f0c6a8', C = '#74d4e8', B = '#3c5f7e', W = '#f7fbff', Y = '#fff1a8'; - ellipseWH(p, w, h, 5, 4, 3, 3, N); ellipseWH(p, w, h, 5, 4, 2, 2, S); pxWH(p, w, h, 6, 4, N); - rectWH(p, w, h, 3, 7, 5, 7, N); rectWH(p, w, h, 4, 7, 3, 7, C); rectWH(p, w, h, 4, 6, 3, 1, W); - lineWH(p, w, h, 8, 8, 11, 5, B); pxWH(p, w, h, 11, 5, Y); lineWH(p, w, h, 2, 9, 0, 11, B); pxWH(p, w, h, 4, 14, N); pxWH(p, w, h, 7, 14, N); return p; - } - - function drawNeonGuppyRight() { - const w = 12, h = 8, p = blankPixels(w, h); - const N = '#252836', B = '#16c7ff', G = '#61ffb8', Y = '#f5ff79', P = '#ff7bd1'; - ellipseWH(p, w, h, 6, 4, 4, 2, N); ellipseWH(p, w, h, 6, 4, 3, 1, B); lineWH(p, w, h, 2, 4, 0, 2, P); lineWH(p, w, h, 2, 4, 0, 6, P); - pxWH(p, w, h, 8, 3, N); pxWH(p, w, h, 9, 4, Y); lineWH(p, w, h, 5, 2, 7, 1, G); lineWH(p, w, h, 5, 6, 7, 7, G); return p; - } - - function drawCloudJellyPupRight() { - const w = 12, h = 12, p = blankPixels(w, h); - const N = '#252836', C = '#dff6ff', B = '#9ce8ff', P = '#ffc6e5', D = '#6f88a0'; - ellipseWH(p, w, h, 5, 6, 5, 3, N); ellipseWH(p, w, h, 5, 6, 4, 2, C); ellipseWH(p, w, h, 9, 5, 3, 2, N); ellipseWH(p, w, h, 9, 5, 2, 1, B); - pxWH(p, w, h, 10, 5, N); pxWH(p, w, h, 11, 6, P); lineWH(p, w, h, 2, 8, 1, 11, D); lineWH(p, w, h, 5, 8, 4, 11, D); lineWH(p, w, h, 8, 8, 9, 11, D); pxWH(p, w, h, 3, 3, C); return p; - } - - function drawEmberFireflyRight() { - const w = 10, h = 8, p = blankPixels(w, h); - const N = '#252836', O = '#ff9a3d', Y = '#ffe66d', B = '#bdf5ff', D = '#7a4430'; - ellipseWH(p, w, h, 5, 4, 3, 2, N); ellipseWH(p, w, h, 5, 4, 2, 1, D); ellipseWH(p, w, h, 7, 4, 2, 2, O); pxWH(p, w, h, 8, 4, Y); - lineWH(p, w, h, 4, 3, 2, 1, B); lineWH(p, w, h, 5, 3, 7, 1, B); pxWH(p, w, h, 7, 4, Y); pxWH(p, w, h, 3, 4, N); return p; - } - - function drawTealSeahareRight() { - const w = 12, h = 10, p = blankPixels(w, h); - const N = '#252836', T = '#36c7a7', L = '#a7ffe0', B = '#5d89a6', Y = '#fff1a8'; - ellipseWH(p, w, h, 6, 6, 4, 2, N); ellipseWH(p, w, h, 6, 6, 3, 1, T); ellipseWH(p, w, h, 9, 5, 3, 2, N); ellipseWH(p, w, h, 9, 5, 2, 1, L); - lineWH(p, w, h, 9, 3, 8, 1, B); lineWH(p, w, h, 10, 3, 11, 1, B); pxWH(p, w, h, 10, 5, N); pxWH(p, w, h, 11, 6, Y); lineWH(p, w, h, 3, 6, 1, 4, B); return p; - } - - function drawHoneybeeCourierRight() { - const w = 12, h = 10, p = blankPixels(w, h); - const N = '#252836', Y = '#ffd94c', D = '#5a3b2c', B = '#bdf5ff', W = '#fff8d7'; - ellipseWH(p, w, h, 6, 5, 4, 2, N); ellipseWH(p, w, h, 6, 5, 3, 1, Y); rectWH(p, w, h, 5, 4, 1, 3, D); rectWH(p, w, h, 7, 4, 1, 3, D); - ellipseWH(p, w, h, 4, 3, 3, 2, B); ellipseWH(p, w, h, 8, 3, 3, 2, B); pxWH(p, w, h, 9, 5, N); pxWH(p, w, h, 10, 6, W); rectWH(p, w, h, 1, 5, 2, 1, D); pxWH(p, w, h, 3, 7, Y); return p; - } - - function drawAuroraBathhouse() { - const w = 28, h = 28, p = blankPixels(w, h); - const N = '#252836', W = '#8ed9e6', C = '#dffbff', R = '#7b5ed6', D = '#3b3f62', Y = '#ffe99a', S = '#bff7ff'; - for (let y = 5; y <= 11; y++) { const inset = Math.abs(y - 11); rectWH(p, w, h, 5 + inset, y, 18 - inset * 2, 1, N); rectWH(p, w, h, 6 + inset, y, 16 - inset * 2, 1, R); } - rectWH(p, w, h, 5, 12, 18, 2, N); rectWH(p, w, h, 6, 12, 16, 1, D); - rectWH(p, w, h, 6, 14, 16, 10, N); rectWH(p, w, h, 7, 15, 14, 8, W); - rectWH(p, w, h, 9, 17, 3, 3, C); rectWH(p, w, h, 16, 17, 3, 3, C); rectWH(p, w, h, 12, 20, 4, 4, D); rectWH(p, w, h, 13, 20, 2, 4, Y); - rectWH(p, w, h, 4, 23, 20, 2, N); rectWH(p, w, h, 6, 24, 16, 2, D); - for (let i = 0; i < 5; i++) { pxWH(p, w, h, 7 + i * 3, 3 - (i % 2), S); pxWH(p, w, h, 8 + i * 3, 2 - (i % 2), C); } - return p; - } - - function drawCopperWindmill() { - const w = 24, h = 32, p = blankPixels(w, h); - const N = '#252836', C = '#c97844', B = '#f0c48a', W = '#fff4d6', D = '#5b3c35', G = '#86bf6b'; - for (let y = 12; y < 30; y++) { const inset = Math.floor((y - 12) / 6); rectWH(p, w, h, 8 - inset, y, 8 + inset * 2, 1, N); rectWH(p, w, h, 9 - inset, y, 6 + inset * 2, 1, B); } - rectWH(p, w, h, 7, 28, 10, 2, D); rectWH(p, w, h, 10, 22, 4, 7, N); rectWH(p, w, h, 11, 22, 2, 7, D); - rectWH(p, w, h, 9, 15, 2, 3, W); rectWH(p, w, h, 13, 15, 2, 3, W); - lineWH(p, w, h, 12, 11, 12, 2, N); lineWH(p, w, h, 12, 11, 4, 8, N); lineWH(p, w, h, 12, 11, 20, 8, N); lineWH(p, w, h, 12, 11, 6, 18, N); lineWH(p, w, h, 12, 11, 18, 18, N); - lineWH(p, w, h, 12, 3, 12, 7, C); lineWH(p, w, h, 5, 8, 9, 10, C); lineWH(p, w, h, 19, 8, 15, 10, C); lineWH(p, w, h, 7, 18, 10, 14, C); lineWH(p, w, h, 17, 18, 14, 14, C); ellipseWH(p, w, h, 12, 11, 2, 2, N); pxWH(p, w, h, 12, 11, W); - rectWH(p, w, h, 4, 30, 16, 1, G); - return p; - } - - function drawMoonGateBridge() { - const w = 32, h = 20, p = blankPixels(w, h); - const N = '#252836', S = '#9a8a7a', L = '#d8c6a9', C = '#dffbff', B = '#79c8de', Y = '#fff1a8'; - rectWH(p, w, h, 2, 14, 28, 3, N); rectWH(p, w, h, 3, 14, 26, 2, S); - for (let x = 4; x < 29; x += 3) rectWH(p, w, h, x, 12, 1, 4, L); - ellipseWH(p, w, h, 16, 11, 9, 9, N); ellipseWH(p, w, h, 16, 11, 7, 7, L); ellipseWH(p, w, h, 16, 13, 5, 6, null); - for (let y = 6; y < 17; y++) for (let x = 9; x < 24; x++) if (!p[y * w + x] && ((x - 16) ** 2) / 25 + ((y - 13) ** 2) / 36 <= 1) p[y * w + x] = B; - pxWH(p, w, h, 9, 9, Y); pxWH(p, w, h, 22, 9, Y); pxWH(p, w, h, 16, 5, C); - return p; - } - - function drawStarlitBookshop() { - const w = 24, h = 28, p = blankPixels(w, h); - const N = '#252836', W = '#7d4f6f', B = '#c18bb5', D = '#3c3348', Y = '#ffe99a', C = '#9ee7ff'; - rectWH(p, w, h, 5, 9, 14, 16, N); rectWH(p, w, h, 6, 10, 12, 14, W); - rectWH(p, w, h, 4, 7, 16, 3, N); rectWH(p, w, h, 5, 7, 14, 2, D); rectWH(p, w, h, 8, 4, 8, 4, N); rectWH(p, w, h, 9, 5, 6, 2, B); - rectWH(p, w, h, 7, 13, 4, 5, C); rectWH(p, w, h, 13, 13, 4, 5, C); rectWH(p, w, h, 10, 20, 4, 5, D); rectWH(p, w, h, 11, 20, 2, 5, Y); - lineWH(p, w, h, 12, 2, 12, 6, Y); lineWH(p, w, h, 10, 4, 14, 4, Y); pxWH(p, w, h, 12, 4, '#fff8d7'); - return p; - } - - function drawCrystalBakery() { - const w = 24, h = 24, p = blankPixels(w, h); - const N = '#252836', P = '#f3a4cf', W = '#ffe5ef', C = '#98e7ff', B = '#615586', D = '#4b3548', Y = '#fff1a8'; - rectWH(p, w, h, 4, 9, 16, 12, N); rectWH(p, w, h, 5, 10, 14, 10, W); - for (let x = 4; x < 20; x += 4) { rectWH(p, w, h, x, 7, 2, 3, N); rectWH(p, w, h, x + 2, 7, 2, 3, P); } - rectWH(p, w, h, 6, 13, 5, 4, C); rectWH(p, w, h, 13, 13, 5, 4, C); rectWH(p, w, h, 10, 17, 4, 4, D); - lineWH(p, w, h, 8, 5, 10, 2, C); lineWH(p, w, h, 10, 2, 12, 5, C); lineWH(p, w, h, 15, 5, 17, 2, P); lineWH(p, w, h, 17, 2, 19, 5, P); - pxWH(p, w, h, 7, 15, Y); pxWH(p, w, h, 16, 15, Y); rectWH(p, w, h, 4, 21, 16, 1, B); - return p; - } - - function drawKelpObservatory() { - const w = 28, h = 32, p = blankPixels(w, h); - const N = '#252836', B = '#4f7896', C = '#92e4f2', D = '#2d4057', G = '#5dbb7a', Y = '#ffe99a'; - ellipseWH(p, w, h, 14, 9, 9, 7, N); ellipseWH(p, w, h, 14, 9, 7, 5, C); rectWH(p, w, h, 5, 13, 18, 3, N); rectWH(p, w, h, 7, 14, 14, 2, B); - rectWH(p, w, h, 7, 16, 14, 13, N); rectWH(p, w, h, 8, 17, 12, 11, D); rectWH(p, w, h, 10, 19, 3, 4, C); rectWH(p, w, h, 16, 19, 3, 4, C); rectWH(p, w, h, 12, 25, 4, 4, B); - for (let i = 0; i < 5; i++) lineWH(p, w, h, 3 + i * 5, 30, 5 + i * 4, 20 + i % 3, G); - pxWH(p, w, h, 14, 9, Y); pxWH(p, w, h, 13, 8, '#ffffff'); - return p; - } - - function drawEmberShrine() { - const w = 20, h = 28, p = blankPixels(w, h); - const N = '#252836', R = '#b94a42', O = '#ff9f45', D = '#5b3030', Y = '#ffd66e', W = '#f7e7d0'; - rectWH(p, w, h, 3, 6, 14, 2, N); rectWH(p, w, h, 4, 6, 12, 1, R); rectWH(p, w, h, 5, 8, 2, 14, N); rectWH(p, w, h, 13, 8, 2, 14, N); - rectWH(p, w, h, 4, 18, 12, 7, N); rectWH(p, w, h, 5, 19, 10, 5, D); rectWH(p, w, h, 8, 20, 4, 5, W); - ellipseWH(p, w, h, 10, 14, 4, 5, O); ellipseWH(p, w, h, 10, 15, 2, 3, Y); rectWH(p, w, h, 6, 24, 8, 2, N); - return p; - } - - function drawHoneycombGreenhouse() { - const w = 28, h = 24, p = blankPixels(w, h); - const N = '#252836', G = '#6fba6a', C = '#d9ffe8', Y = '#ffe99a', D = '#6b8e5a'; - ellipseWH(p, w, h, 14, 13, 12, 9, N); ellipseWH(p, w, h, 14, 13, 10, 7, C); rectWH(p, w, h, 3, 13, 22, 8, N); rectWH(p, w, h, 4, 14, 20, 6, C); - for (let y = 10; y < 20; y += 4) for (let x = 6; x < 23; x += 5) { lineWH(p, w, h, x, y, x + 2, y + 2, D); lineWH(p, w, h, x + 2, y + 2, x, y + 4, D); lineWH(p, w, h, x + 2, y + 2, x + 4, y, D); } - rectWH(p, w, h, 11, 18, 6, 3, G); pxWH(p, w, h, 14, 10, Y); - return p; - } - - function drawCeladonPagoda() { - const w = 24, h = 36, p = blankPixels(w, h); - const N = '#252836', G = '#74b57a', W = '#e5d9b8', D = '#4c4a3f', Y = '#fff1a8'; - for (const [y, wide] of [[6, 16], [14, 20], [22, 18]]) { rectWH(p, w, h, Math.floor((w - wide) / 2), y, wide, 2, N); rectWH(p, w, h, Math.floor((w - wide) / 2) + 1, y, wide - 2, 1, G); rectWH(p, w, h, 8, y + 2, 8, 6, N); rectWH(p, w, h, 9, y + 3, 6, 4, W); rectWH(p, w, h, 11, y + 4, 2, 2, Y); } - rectWH(p, w, h, 7, 30, 10, 4, N); rectWH(p, w, h, 8, 30, 8, 3, D); rectWH(p, w, h, 11, 31, 2, 3, Y); lineWH(p, w, h, 12, 2, 12, 6, N); pxWH(p, w, h, 12, 2, Y); - return p; - } - - function drawTidePearlArch() { - const w = 24, h = 24, p = blankPixels(w, h); - const N = '#252836', S = '#b0a18f', C = '#dffbff', B = '#7fdaf2', P = '#fffaf0'; - ellipseWH(p, w, h, 12, 14, 9, 10, N); ellipseWH(p, w, h, 12, 14, 7, 8, S); ellipseWH(p, w, h, 12, 16, 5, 6, null); - rectWH(p, w, h, 4, 19, 5, 3, N); rectWH(p, w, h, 15, 19, 5, 3, N); ellipseWH(p, w, h, 12, 8, 3, 3, P); pxWH(p, w, h, 11, 7, C); lineWH(p, w, h, 6, 17, 18, 17, B); - return p; - } - - function drawLavenderMarket() { - const w = 32, h = 20, p = blankPixels(w, h); - const N = '#252836', L = '#b084d6', W = '#fff0c8', D = '#5b496b', Y = '#ffe99a', G = '#77bd76'; - rectWH(p, w, h, 3, 8, 26, 10, N); rectWH(p, w, h, 4, 9, 24, 8, W); - for (let x = 3; x < 29; x += 4) { rectWH(p, w, h, x, 6, 2, 3, L); rectWH(p, w, h, x + 2, 6, 2, 3, D); } - rectWH(p, w, h, 6, 12, 5, 4, G); rectWH(p, w, h, 14, 12, 4, 5, D); rectWH(p, w, h, 21, 12, 5, 4, G); pxWH(p, w, h, 8, 12, Y); pxWH(p, w, h, 23, 12, Y); - return p; - } - - function drawSkyseedTower() { - const w = 24, h = 36, p = blankPixels(w, h); - const N = '#252836', G = '#73bf83', C = '#d9ffe8', D = '#456b5d', Y = '#fff1a8'; - ellipseWH(p, w, h, 12, 6, 5, 5, N); ellipseWH(p, w, h, 12, 6, 3, 3, C); pxWH(p, w, h, 12, 5, Y); - for (let y = 11; y < 34; y++) { const inset = Math.floor((y - 11) / 9); rectWH(p, w, h, 8 - inset, y, 8 + inset * 2, 1, N); rectWH(p, w, h, 9 - inset, y, 6 + inset * 2, 1, D); } - for (let y = 15; y < 30; y += 5) { rectWH(p, w, h, 10, y, 4, 2, C); } - lineWH(p, w, h, 7, 12, 3, 22, G); lineWH(p, w, h, 17, 12, 21, 22, G); rectWH(p, w, h, 9, 31, 6, 3, N); - return p; - } - - function drawOrchardTerrace() { - const w = 32, h = 24, p = blankPixels(w, h); - const N = '#252836', G = '#6fbf65', D = '#496d3b', B = '#8a6748', R = '#ff7f6e', Y = '#ffd66e'; - for (let y = 10; y < 22; y += 3) { rectWH(p, w, h, 3, y, 26, 2, N); rectWH(p, w, h, 4, y, 24, 1, G); } - for (const x of [8, 15, 23]) { rectWH(p, w, h, x, 8, 2, 8, B); ellipseWH(p, w, h, x + 1, 7, 5, 4, N); ellipseWH(p, w, h, x + 1, 7, 4, 3, G); pxWH(p, w, h, x - 1, 7, R); pxWH(p, w, h, x + 3, 8, Y); } - return p; - } - - function drawDreamcatcherTree() { - const w = 28, h = 32, p = blankPixels(w, h); - const N = '#252836', B = '#7b4f36', G = '#548b5f', L = '#8fd17e', P = '#ead1ff', Y = '#fff1a8'; - rectWH(p, w, h, 12, 16, 4, 13, N); rectWH(p, w, h, 13, 16, 2, 13, B); ellipseWH(p, w, h, 14, 11, 11, 9, N); ellipseWH(p, w, h, 14, 11, 9, 7, G); ellipseWH(p, w, h, 10, 9, 4, 3, L); ellipseWH(p, w, h, 18, 12, 4, 3, L); - ellipseWH(p, w, h, 14, 10, 5, 5, N); ellipseWH(p, w, h, 14, 10, 4, 4, null); lineWH(p, w, h, 10, 10, 18, 10, P); lineWH(p, w, h, 14, 6, 14, 14, P); pxWH(p, w, h, 14, 10, Y); - return p; - } - - function drawNebulaFountain() { - const w = 24, h = 24, p = blankPixels(w, h); - const N = '#252836', S = '#7e7f96', C = '#8ee8ff', P = '#d7c6ff', Y = '#fff1a8'; - rectWH(p, w, h, 5, 17, 14, 3, N); rectWH(p, w, h, 6, 17, 12, 2, S); ellipseWH(p, w, h, 12, 16, 7, 3, N); ellipseWH(p, w, h, 12, 16, 5, 2, C); - rectWH(p, w, h, 10, 9, 4, 8, N); rectWH(p, w, h, 11, 9, 2, 8, S); ellipseWH(p, w, h, 12, 8, 4, 3, P); pxWH(p, w, h, 12, 7, Y); lineWH(p, w, h, 8, 12, 5, 9, C); lineWH(p, w, h, 16, 12, 19, 9, C); - return p; - } - - function drawCoralLighthouse() { - const w = 24, h = 36, p = blankPixels(w, h); - const N = '#252836', C = '#ff7f6e', W = '#ffe7d5', B = '#5b668d', Y = '#fff1a8'; - rectWH(p, w, h, 8, 8, 8, 24, N); for (let y = 9; y < 32; y++) rectWH(p, w, h, 9, y, 6, 1, y % 6 < 3 ? W : C); - rectWH(p, w, h, 6, 5, 12, 4, N); rectWH(p, w, h, 7, 6, 10, 2, B); rectWH(p, w, h, 9, 2, 6, 4, N); rectWH(p, w, h, 10, 3, 4, 2, Y); - rectWH(p, w, h, 10, 17, 4, 3, B); rectWH(p, w, h, 10, 31, 4, 3, B); rectWH(p, w, h, 6, 33, 12, 2, N); - return p; - } - - function drawOrigamiWindShrine() { - const w = 20, h = 24, p = blankPixels(w, h); - const N = '#252836', W = '#f4f7ff', C = '#bfe8ff', D = '#6e6a88', Y = '#fff1a8'; - rectWH(p, w, h, 8, 12, 4, 9, N); rectWH(p, w, h, 9, 13, 2, 8, D); rectWH(p, w, h, 5, 20, 10, 2, N); - lineWH(p, w, h, 10, 11, 4, 4, N); lineWH(p, w, h, 10, 11, 16, 4, N); lineWH(p, w, h, 10, 11, 4, 16, N); lineWH(p, w, h, 10, 11, 16, 16, N); - lineWH(p, w, h, 5, 4, 10, 7, W); lineWH(p, w, h, 15, 4, 10, 7, C); lineWH(p, w, h, 5, 16, 10, 13, C); lineWH(p, w, h, 15, 16, 10, 13, W); pxWH(p, w, h, 10, 11, Y); - return p; - } - - function drawRainmakerDrum() { - const w = 20, h = 20, p = blankPixels(w, h); - const N = '#252836', B = '#8a6748', D = '#5b3c35', C = '#9ee7ff', Y = '#fff1a8'; - ellipseWH(p, w, h, 10, 12, 8, 5, N); ellipseWH(p, w, h, 10, 12, 6, 3, B); rectWH(p, w, h, 4, 12, 12, 4, D); lineWH(p, w, h, 4, 9, 16, 15, Y); lineWH(p, w, h, 16, 9, 4, 15, Y); - for (let x = 5; x <= 15; x += 5) { pxWH(p, w, h, x, 4, C); pxWH(p, w, h, x + 1, 5, C); } - return p; - } - - function drawVelvetMushroomGrove() { - const w = 24, h = 22, p = blankPixels(w, h); - const N = '#252836', M = '#a04f91', P = '#ffb8f2', S = '#e9d6b8', G = '#6caf68'; - for (const [x, y, r] of [[7, 10, 5], [15, 8, 6], [18, 13, 4]]) { rectWH(p, w, h, x - 1, y, 2, 8, N); rectWH(p, w, h, x, y + 1, 1, 7, S); ellipseWH(p, w, h, x, y, r, 4, N); ellipseWH(p, w, h, x, y, r - 1, 3, M); pxWH(p, w, h, x - 1, y - 1, P); pxWH(p, w, h, x + 2, y, P); } - rectWH(p, w, h, 3, 20, 18, 1, G); - return p; - } - - function drawStarMapMonument() { - const w = 20, h = 20, p = blankPixels(w, h); - const N = '#252836', S = '#6c7188', D = '#3f455a', Y = '#fff1a8', C = '#dce8ff'; - rectWH(p, w, h, 6, 5, 8, 12, N); rectWH(p, w, h, 7, 6, 6, 10, S); rectWH(p, w, h, 8, 8, 4, 6, D); rectWH(p, w, h, 5, 17, 10, 2, N); - const pts = [[10, 5], [7, 10], [13, 11], [9, 14], [12, 8]]; for (const [x, y] of pts) pxWH(p, w, h, x, y, Y); lineWH(p, w, h, 10, 5, 7, 10, C); lineWH(p, w, h, 7, 10, 13, 11, C); lineWH(p, w, h, 13, 11, 9, 14, C); - return p; - } - - function drawLanternReefGate() { - const w = 32, h = 20, p = blankPixels(w, h); - const N = '#252836', C = '#ff8a7a', B = '#6dd8f0', G = '#5fbf79', Y = '#ffd98a', D = '#80605a'; - rectWH(p, w, h, 5, 14, 22, 3, N); rectWH(p, w, h, 6, 14, 20, 2, D); rectWH(p, w, h, 7, 8, 3, 8, N); rectWH(p, w, h, 22, 8, 3, 8, N); rectWH(p, w, h, 7, 8, 18, 2, N); rectWH(p, w, h, 8, 8, 16, 1, C); - ellipseWH(p, w, h, 6, 15, 4, 3, G); ellipseWH(p, w, h, 25, 15, 4, 3, B); rectWH(p, w, h, 10, 9, 2, 4, Y); rectWH(p, w, h, 20, 9, 2, 4, Y); - return p; - } - - function drawEmberSalamanderRight() { - return artRowsSized([ - '..............', - '....NNN.......', - '..NNORNN......', - '.NORRRRONN....', - 'NORRYYRRRON...', - '.NNORRRRNNN...', - '...NNRRN..NN..', - '..NN..NN......', - '..............', - '..............' - ], {N:'#252836', O:'#ff9f45', R:'#d4505f', Y:'#ffd66e'}, 14, 10); - } - - function drawMoonSnailRight() { - return artRowsSized([ - '............', - '....NNNN....', - '...NCCYYN...', - '..NCCYYYYN..', - '.NCCYNNYYN..', - '.NSSSNNNNN..', - '..NSSSSSSN..', - '...NN..NN...', - '............', - '............' - ], {N:'#252836', C:'#dfe9ff', Y:'#fff1a8', S:'#b8a7d8'}, 12, 10); - } - - function drawRibbonDragonflyRight() { - return artRowsSized([ - '..............', - '..NN....NN....', - '.NPPN..NCCN...', - '..NPPNNCCNYY..', - '....NNNNNYYN..', - '..NCCNNPPN....', - '.NCCN..NPPN...', - '..............' - ], {N:'#252836', P:'#ead1ff', C:'#bff7ff', Y:'#ffd66e'}, 14, 8); - } - - function drawTeacupAutomatonRight() { - return artRowsSized([ - '....NNNN....', - '...NYYYYN...', - '..NYYYYYYN..', - '..NYNNYNNN..', - '...NYYYYN...', - '..NNCCCCNN..', - '.NCCNNNNCCN.', - '.NCCYCCYCCN.', - '..NCCCCCCN..', - '...NCCCCN...', - '..NNNCCNNN..', - '.NN..CC..NN.', - '.....NN.....', - '....N..N....', - '...NN..NN...', - '............' - ], {N:'#252836', Y:'#ffe2bd', C:'#8ac6d1'}, 12, 16); - } - - function drawSapphireCrabRight() { - return artRowsSized([ - '............', - '..NN....NN..', - '.NCCN..NCCN.', - '..NCCCCCCN..', - '.NCCYCCYCCN.', - 'NCCNNNNCCCN.', - '.NN....NN...', - '............' - ], {N:'#252836', C:'#357bd1', Y:'#dffbff'}, 12, 8); - } - - function drawCloudAlpacaRight() { - return artRowsSized([ - '..............', - '....NNNNN.....', - '..NNWWWWWNN...', - '.NWWWWWWWWWN..', - '.NWWNWWWNWWN..', - '.NWWWWWWWWN...', - '..NNWWWWNN....', - '...NWWNWN.....', - '...N..N.N.....', - '..NN..NNN.....', - '..............', - '..............' - ], {N:'#252836', W:'#f4f7ff'}, 14, 12); - } - - function drawStarlingBardRight() { - return artRowsSized([ - '....NNNN....', - '...NYYYYN...', - '...NYYYYN...', - '....NNNN....', - '...NNPPNN...', - '..NPPPPPPN..', - '..NPPYPPPN..', - '...NPPPPN...', - '....NDDN....', - '...NNDDNN...', - '..NN.DD.NN..', - '.....DD.....', - '....N..N....', - '...NN..NN...', - '............', - '............' - ], {N:'#252836', Y:'#e0b18c', P:'#5a67b8', D:'#6b4b38'}, 12, 16); - } - - function drawGlassJellyfishRight() { - return artRowsSized([ - '............', - '....NNNN....', - '...NCCCCN...', - '..NCCCCCCN..', - '.NCCYYYYCCN.', - '.NCCCCCCCCN.', - '..NNCCCCNN..', - '...N.CC.N...', - '..N..CC..N..', - '.....CC.....', - '....N..N....', - '...N....N...', - '............', - '............', - '............', - '............' - ], {N:'#252836', C:'#9ef4ff', Y:'#fff1a8'}, 12, 16); - } - - function drawCopperHeronRight() { - return artRowsSized([ - '.....NN.....', - '....NYYN....', - '.....NNN....', - '......NN....', - '......NN....', - '....NNCCN...', - '...NCCCCCNN.', - '..NCCCCCCCN.', - '...NNCCCCN..', - '.....NNNN...', - '......N.....', - '......N.....', - '.....N.N....', - '....NN.NN...', - '............', - '............' - ], {N:'#252836', C:'#c97844', Y:'#fff1a8'}, 12, 16); - } - - function drawPaperFoxKiteRight() { - return artRowsSized([ - '..............', - '.....NNN......', - '....NWWYN.....', - '...NWWYYYN....', - '..NWWYYYYN....', - '.NWWYYNNYN....', - '..NNYYYYYN....', - '....NYYYNN....', - '...NN..NN.....', - '..N....N......', - '..............', - '..............' - ], {N:'#252836', W:'#f4f7ff', Y:'#ffd66e'}, 14, 12); - } - - function drawPrismSeahorseRight() { - return artRowsSized([ - '..........', - '....NN....', - '...NCCN...', - '..NCPCCN..', - '..NCCCCN..', - '...NCCN...', - '....NCN...', - '....NCN...', - '...NCCN...', - '..NCCN....', - '..NCCNN...', - '...NNCCN..', - '.....NN...', - '..........' - ], {N:'#252836', C:'#7fdaf2', P:'#d7c6ff'}, 10, 14); - } - - function drawOrchardSpriteRight() { - return artRowsSized([ - '..........', - '...NNNN...', - '..NYYYYN..', - '.NYYRYYYN.', - '.NYYYYYYN.', - '..NNGGNN..', - '...NGGN...', - '..NNGGNN..', - '..N....N..', - '.NN....NN.', - '..........', - '..........' - ], {N:'#252836', Y:'#ffd1a8', R:'#ff7f6e', G:'#7bc56a'}, 10, 12); - } - - function drawMapTurtleRight() { - return artRowsSized([ - '............', - '....NNNN....', - '..NNGGGGNN..', - '.NGGYGGYGN..', - '.NGGGGGGGN..', - '..NNGGGGNNN.', - '...NSSSSYN..', - '..NN....NN..', - '............', - '............' - ], {N:'#252836', G:'#6fbf65', Y:'#ffd66e', S:'#8a6748'}, 12, 10); - } - - function drawTeaSparrowRight() { - return artRowsSized([ - '........', - '..NNN...', - '.NYYNN..', - 'NYYYYYN.', - '.NDDYN..', - '..NNN...', - '..N.N...', - '........' - ], {N:'#252836', Y:'#c89a6b', D:'#6b4b38'}, 8, 8); - } - - function drawBubbleDiverRight() { - return artRowsSized([ - '....NNNN....', - '...NCCCCN...', - '..NCYYYYCN..', - '..NCYYYYCN..', - '...NCCCCN...', - '..NNBBBBNN..', - '.NBBBBBBBBN.', - '.NBBYBBYBBN.', - '..NBBBBBBN..', - '...NBBBBN...', - '..NN.BB.NN..', - '.....BB.....', - '....N..N....', - '...NN..NN...', - '............', - '............' - ], {N:'#252836', C:'#dffbff', Y:'#e0b18c', B:'#357bd1'}, 12, 16); - } - - function drawMistEelRight() { - return artRowsSized([ - '..............', - '...NNN........', - '.NNCCCNNNNN...', - 'NCCCCCCCCCCNN.', - '.NNCCNNNNNN...', - '..............' - ], {N:'#252836', C:'#bfefff'}, 14, 6); - } - - function drawRubyBeetleRight() { - return artRowsSized([ - '..........', - '..NNNN....', - '.NRRRRN...', - 'NRYRRYRN..', - '.NRRRRN...', - '..NNNN....', - '.N....N...', - '..........' - ], {N:'#252836', R:'#d4505f', Y:'#ffd66e'}, 10, 8); - } - - function drawSnowyTanukiRight() { - return artRowsSized([ - '............', - '....NNNN....', - '..NNWWWWNN..', - '.NWWDWWDWN..', - '.NWWWWWWWN..', - '..NNWWWWN...', - '...NDDDN....', - '..NNWWWNN...', - '..N.N.N.N...', - '.NN.N.NN....', - '............', - '............' - ], {N:'#252836', W:'#f4f7ff', D:'#6b5d67'}, 12, 12); - } - - function drawLighthouseKeeperRight() { - return artRowsSized([ - '....NNNN....', - '...NYYYYN...', - '..NYNNYYYN..', - '..NYYYYYYN..', - '...NNNNNN...', - '..NNCCCCNN..', - '.NCCCCCCCCN.', - '.NCCYCCYCCN.', - '..NCCCCCCN..', - '...NCCCCN...', - '..NN.DD.NN..', - '.....DD.....', - '....N..N....', - '...NN..NN...', - '............', - '............' - ], {N:'#252836', Y:'#e0b18c', C:'#c97844', D:'#3c3348'}, 12, 16); - } - + // Pixel Island default-art drawing code was moved out of app.js. window.PixelIslandDebug = { ...(window.PixelIslandDebug || {}), applySyncEvent }; bootstrap(); })(); diff --git a/index.html b/index.html index 95cbffb..60c1ad4 100644 --- a/index.html +++ b/index.html @@ -3,38 +3,23 @@ - Pixel Island Summoner - Shared Test + Pixel Island
+ + + 333 + - +
-
A shared rotating island exhibition for tiny pixel works.
-
-
-
- - - -
- - Publish creates an account for shared island publishing. +
A rotating island exhibition for tiny pixel works.
@@ -70,8 +55,8 @@ + + + +
+ + Publish creates a local account for island publishing. + +
-
Settings
+
Display

Turn major visual systems on or off. Server decides the public 250 exhibition slots; this local cap only filters your view.

diff --git a/server/test_publish_api.py b/server/test_publish_api.py new file mode 100644 index 0000000..482bee1 --- /dev/null +++ b/server/test_publish_api.py @@ -0,0 +1,132 @@ +import json +import os +import shutil +import socket +import subprocess +import tempfile +import time +import unittest +import urllib.error +import urllib.request +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def free_port(): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def asset(asset_id, owner="acct-a"): + return { + "id": asset_id, + "name": "Test Block", + "category": "static", + "subtype": "other", + "width": 1, + "height": 1, + "size": 1, + "pixels": "a", + "ownerAccountId": owner, + } + + +def publish_command(command_id, asset_id="asset-a", object_id="object-a"): + return { + "id": command_id, + "type": "publish.asset_object", + "kind": "static", + "asset": asset(asset_id), + "object": { + "id": object_id, + "assetId": asset_id, + "x": 3, + "y": 4, + "publishedAt": 1, + "version": 1, + }, + } + + +@unittest.skipIf(shutil.which("php") is None, "PHP CLI is not installed") +class PublishApiTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.port = free_port() + env = os.environ.copy() + env["PIXEL_ISLAND_DB_PATH"] = str(Path(self.temp.name) / "pixel.sqlite") + self.proc = subprocess.Popen( + ["php", "-S", f"127.0.0.1:{self.port}", "-t", str(ROOT)], + cwd=str(ROOT), + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + self.base = f"http://127.0.0.1:{self.port}/api/index.php" + deadline = time.time() + 5 + while time.time() < deadline: + try: + self.get("health") + return + except Exception: + time.sleep(0.05) + self.fail("PHP test server did not start") + + def tearDown(self): + self.proc.terminate() + self.proc.wait(timeout=5) + self.temp.cleanup() + + def get(self, action): + with urllib.request.urlopen(f"{self.base}?action={action}", timeout=5) as response: + return json.loads(response.read().decode("utf-8")) + + def post_commands(self, commands, account_id="acct-a", password="pw"): + payload = { + "account": {"id": account_id, "name": account_id, "password": password}, + "commands": commands, + } + request = urllib.request.Request( + f"{self.base}?action=commands", + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(request, timeout=5) as response: + return json.loads(response.read().decode("utf-8")) + + def test_publish_asset_object_is_atomic_and_server_timestamped(self): + data = self.post_commands([publish_command("cmd-one")]) + self.assertEqual(data["appliedCommandIds"], ["cmd-one"]) + self.assertEqual(data["rejectedCommands"], []) + placed = data["snapshot"]["placed"] + self.assertEqual(len(placed), 1) + self.assertEqual(placed[0]["assetId"], "asset-a") + self.assertNotEqual(placed[0]["publishedAt"], 1) + self.assertEqual(data["snapshot"]["assets"][0]["ownerAccountId"], "acct-a") + + def test_duplicate_publish_command_replays_without_new_object(self): + self.post_commands([publish_command("cmd-dupe")]) + data = self.post_commands([publish_command("cmd-dupe")]) + self.assertEqual(data["appliedCommandIds"], ["cmd-dupe"]) + self.assertEqual(len(data["snapshot"]["placed"]), 1) + + def test_owner_rejection_is_reported_in_batch(self): + self.post_commands([publish_command("cmd-owner", "shared-asset", "owner-object")]) + stolen = publish_command("cmd-stolen", "shared-asset", "stolen-object") + data = self.post_commands([stolen], account_id="acct-b", password="pw") + self.assertEqual(data["appliedCommandIds"], []) + self.assertEqual(data["rejectedCommands"][0]["error"], "asset_id_already_owned") + + def test_quota_rejection_does_not_abort_response(self): + commands = [publish_command(f"cmd-{i}", f"asset-{i}", f"object-{i}") for i in range(6)] + data = self.post_commands(commands) + self.assertEqual(len(data["appliedCommandIds"]), 5) + self.assertEqual(data["rejectedCommands"][0]["error"], "publish_limit_reached") + + +if __name__ == "__main__": + unittest.main() diff --git a/styles.css b/styles.css index 83e344d..4023006 100644 --- a/styles.css +++ b/styles.css @@ -995,8 +995,8 @@ body, button, input, select, textarea { text-transform: uppercase; } .reportDialog { - position: absolute; - z-index: 20; + position: fixed; + z-index: 9998; inset: 0; display: grid; place-items: center; @@ -1052,8 +1052,9 @@ body, button, input, select, textarea { font-size: 15px; } .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; } -.placementPreviewBar { position:fixed; left:50%; bottom:22px; transform:translateX(-50%); z-index:35; display:flex; align-items:center; gap:10px; max-width:min(760px, calc(100vw - 24px)); padding:12px 14px; border:3px solid #243044; background:#fff7de; box-shadow:5px 5px 0 rgba(36,48,68,.25); font-size:15px; } -.placementPreviewBar[hidden] { display:none; } +.homeLink { position:fixed; left:12px; top:12px; z-index:6; display:inline-flex; align-items:center; gap:6px; padding:7px 9px; border:2px solid #243044; background:rgba(255,247,222,.92); box-shadow:3px 3px 0 rgba(36,48,68,.22); color:#243044; text-decoration:none; font-weight:800; letter-spacing:.02em; } +.homeLinkIcon { display:grid; place-items:center; width:22px; height:22px; border:2px solid currentColor; background:#fffdf5; line-height:1; } +.homeLinkText { font-size:13px; } .likedCodex { margin:8px 0 14px; padding:10px; border:2px dashed rgba(36,48,68,.35); background:rgba(255,255,255,.45); font-size:14px; } .likedCodexTitle { font-weight:700; margin-bottom:6px; } .likedCodexList { display:flex; gap:8px; flex-wrap:wrap; } @@ -1213,6 +1214,25 @@ body, button, input, select, textarea { font-size: 15px; } box-shadow: none !important; } .clockCard.utilityControl { display: none !important; } +.accountSettingsCard .accountCard.utilityControl { + max-width: none !important; + width: 100% !important; + justify-content: space-between; + flex-wrap: wrap; + background: #fffdf5 !important; +} +.accountSettingsCard .accountFields { + grid-template-columns: minmax(90px, 1fr) minmax(120px, 1.2fr) minmax(120px, 1.2fr); + flex: 1 1 360px; +} +.accountSettingsCard .accountCard small { + max-width: none !important; + flex-basis: 100%; +} +@media (max-width: 560px) { + .accountSettingsCard .accountFields { grid-template-columns: 1fr; } +} + .editorToolRow { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } .editorHistoryRow { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } .studioDrawer.advancedTools .editorToolRow { grid-template-columns: repeat(4, minmax(0, 1fr)) !important; } @@ -1266,7 +1286,6 @@ body, button, input, select, textarea { font-size: 15px; } font-size: 11px; } .finishActions { grid-template-columns: 1.4fr 1fr !important; } -.placementPreviewBar span::after { content: " Left-click chooses a tile; right-click cancels selection."; } @media (max-width: 980px) { .islandTopBar { width: calc(100vw - 16px) !important; flex-wrap: wrap; justify-content: center; } .accountCard.utilityControl { order: 3; max-width: 100% !important; } @@ -1462,7 +1481,7 @@ body, button, input, select, textarea { font-size: 15px; } width: 4px; height: 28px; transform-origin: 50% 100%; - transform: translate(-50%, -100%) rotate(var(--clock-rotate)); + transform: translate(-50%, -100%) rotate(var(--clock-rotate, 0deg)); background: #243044; border-radius: 6px 6px 2px 2px; box-shadow: 1px 0 0 rgba(255,255,255,.45); @@ -2895,3 +2914,26 @@ body, button, input, select, textarea { font-size: 15px; } } } + + +/* v3 placement confirmation: explicit button, never the old bottom preview bar. */ +.placeHereButton { + position: fixed; + z-index: 92; + transform: translate(-50%, -100%); + padding: 9px 12px !important; + border: 3px solid var(--line) !important; + border-radius: 13px !important; + background: #fff2a8 !important; + color: #243044 !important; + box-shadow: 0 6px 0 rgba(36,48,68,.22), 0 10px 18px rgba(22,31,46,.18) !important; + font-size: 13px !important; + font-weight: 900 !important; + letter-spacing: .04em; + cursor: pointer; +} +.placeHereButton[hidden] { display: none !important; } +#confirmDialog.reportDialog { position: fixed !important; z-index: 10000 !important; } +.reportDialog { z-index: 9998; } +.assetStatus.validating { background: #fff2a8; } +.assetStatus.failed { background: #ffe2e2; color: #8b2424; }