アセットパック(追加アセットの読み込み)
概要
同梱されていない定義YAML・絵・表示文字列を、外部の配布物(アセットパック)から追加で 読み込む仕組みの設計ドキュメントです。配布物の形(ZIP)と、その中身をゲームがどう見つけるかを 決めます。
定義YAMLの文法そのものと、既存定義を差し替える patch 文法は
GameElementDefinition.md(3節)が扱います。表示文字列の対応表の
形式は Localization.md が持ちます。
持ち込めるのはデータだけで、コードは持ち込めません(1節)。ただしこのゲームのデータは
振る舞いを含む——props・interactions・passives・recipes はすべて定義YAMLに書かれるので、
アセットパックは新しい物・新しい操作・新しいレシピを足せます。
WorldCodexの文法は増やしません。増えるのは配布物の中のディレクトリ構成と、絵の在処を実行時に
決める仕組みだけです。同梱ぶんは import.meta.glob がビルド時に一覧を作るので、パックぶんも同じ形の
一覧(本書では在庫表と呼ぶ)を配布物から作れれば、その先の仕組みは共通になります。
確定しているのは3.2節(pack.yaml の名乗り)と6.1.1節(捨てたぶんを画面から読める)と6.3節
(名前空間を設けない)だけで、他の記述は暫定です。【未実装】・【いつか】 の印が付いた節を除いて
実装済みです(src/asset-pack/、読み込みは src/loader/・
src/locale/Localization.ts・src/art/、検証は tests/asset-pack/)。節にならない未実装がもう1つ
あり、絵に対する patch にあたる文法がまだありません(6節)。未決事項は7節に整理しています。
複数のパックを並べて読めますが、取得元は同梱のサンプルパック1つだけです
(src/asset-pack/install.ts)。取得元を選ぶ画面はまだありません。
見本として、薬(毒薬・回復ポーション)のパックが入っています。型を足すだけでなく、同梱の土地の
探索結果へ patch で自分のアイテムを差し込んでいます(sample-pack/world-codex/potions.yaml)。
元のファイルは sample-pack/、配る形は public/sample-pack.zip で、npm run pack:sample が
固め直します(3.1節)。
読むかどうかはスタート画面の設定が決めます(StartScreen.md 画面構成 4、
既定は読まない)。パックは起動時に1回だけ入って以後不変(4節)なので、設定はゲームを始める前に
しか変えられず、変えた値はページを読み込み直して初めて効きます。ビューア(src/codex-viewer/)は
この設定によらず常に読みます——設定があるのはパックの物が世界に混ざるのを避けるためで、
ビューアは世界を始めずに定義を読むだけの道具だからです。
1. 持ち込めるのはデータだけ
コード(JavaScript)を含むパックには対応しません。 「MOD」と呼ばないのはこのためです。
ブラウザでZIP内のスクリプトを動かすこと自体は可能ですが、サンドボックスが無く、読み込んだ時点で ゲームと同じ権限を持ちます。加えて、バンドラの外にあるコードからは内部のクラスやモジュールが 公開契約になり、リファクタのたびに壊れます。データだけに限れば、パックが触れるのは WorldCodexの文法というすでに公開されている契約だけで済みます。
2. パックは1つのZIPで受け取る
パック1つ=ZIPファイル1つとし、個別ファイルをURLで拾いに行く形は採りません。
ZIPは末尾に中央ディレクトリ(全エントリの一覧)を持つため、在庫表が中身と一緒に届きます。
配布物をディレクトリのまま置く形だと、外部サーバではディレクトリ一覧を得る手段が無く、配る側が
一覧ファイルを別途用意して同期させることになります。それは同梱ぶんで手書きの一覧をやめた理由
(src/loader/loadWorldCodex.ts)をそのまま外部へ持ち出すだけです。
取得元がユーザーのローカルでも外部サーバでも、ArrayBuffer を得るところまでが違うだけで
(<input type="file"> か fetch)、その先は同じ経路になります。今あるのは fetch の側だけです。
【いつか: パックの取得元】 ユーザーが選んだパックを入れる口は、同梱の見本1つで形が確かめられて
いるので後から足します(Someday.md)。
取得そのものに失敗したときは、まだゲームを起動しません(src/asset-pack/install.ts)。ここは
「パックを読み込んだうえで一部が行えなかった」場合(6.1節)とは別で、パックを指定したのに何も
届いていない状態なので、設定の誤りとして扱います。
3. ZIPのトップが src/assets/ に相当する
ZIPの中身は src/assets/ 以下と同じ形にし、assets/ のような包む階層は設けません。
パックにはデータしか入らないので(1節)、src/assets/ がまさにその中身と同じ役割です。
| 中身 | ZIP内の位置 | 同梱ぶんの位置 |
|---|---|---|
| 定義YAML | world-codex/**/*.yaml |
src/assets/world-codex/ |
| 型の絵 | objects/<型の絵の名前>.png |
src/assets/objects/ |
| 背景の絵 | backgrounds/<持ち主>_<スロット名>_<用途>.png |
src/assets/backgrounds/ |
| 表示文字列 | locale/<言語>.yaml |
src/assets/locale/ |
名前の規約(型の絵の名前・_multiply の接尾辞・背景の3語構成)は変わりません。読み手にとっては
「同梱の絵をどこに置くか」を知っていれば、パックの作り方も知っていることになります。同梱ぶんを
そのままZIPに固め、pack.yaml(3.2節)を1つ足せば、それが1つのパックとして通ります。
絵は .png と .webp、定義は .yaml/.yml を読みます。これら以外のファイルは、入っていても
無視します。アイコン(icons/)・空(weather/)・シナリオ(scenarios/)はまだ読みません。
【いつか: 絵以外の資産】 音などを同じ在庫表に載せる話は、ゲーム本体が音を持ってからにします
(Someday.md)。
3.1 同梱のサンプルは sample-pack/ を固めたもの
元のファイルはリポジトリに普通のファイルとして置き、ZIPはそれを固めた成果物です
(npm run pack:sample → public/sample-pack.zip)。ZIPだけを置くと中身が差分に出ず、
置き方の規約を変えたときに黙って腐ります。
public/ はビルドが素通しで配る場所で、バンドルへ入る同梱データ(src/assets/)とは役割が
違います(CodeStructure.md 1節)。パックはゲームが取得しに行く配布物なので、
byte単位でそのまま配られる必要があります。
ZIPは誰がどの環境で固めても同じバイト列になります(時刻を書かない・並び順はパス順・テキストの
改行はLFへ揃える)。作業ツリーの改行はgitの取り出し方(core.autocrlf)で変わるので、読んだままを
入れると、中身を変えていないのに82KBが差分に出ます。何をどんなバイト列で入れるかは
scripts/samplePackFiles.mjs が決め、packSample.mjs はZIPの形に組むだけです。
ZIPと sample-pack/ が食い違っていないかは tests/asset-pack/samplePack.test.ts が検査します。
期待するバイト列は同じ samplePackFiles.mjs から引きます——テストが自分でファイルを読み直すと、
中身が合っていても作業ツリーの改行しだいで落ちます。
3.2 パックは pack.yaml で自分の名前と版を名乗る【確定】
ZIPのトップに pack.yaml を1つ置き、パックの識別子と版をそこに書きます。どちらも省略できません。
同梱ぶんに対応するものが無い唯一のファイルで、3節の「ZIPのトップが src/assets/ に相当する」の
例外はこれだけです。
識別子はパックが自分で名乗り、ファイル名からは採りません。 ミラーやリネームでファイル名は変わり、 そのたびに同じパックが別物になります。セーブは記録した識別子でパックを照合する(6.4節)ので、 名前が動くとその瞬間にスロットが開けなくなります。
版は大小の順序を持ちません。 使い道は「同じ版か」の照合(6.4節)だけで、比べる用途が ありません。
出どころ: #656(pack.yaml に識別子と版を書かせ省略は認めない)と、同(版は文字列で順序を持たない)
3.3 pack.yaml の知らない欄はエラーにする
知らないキーはエラーにします(定義YAMLと同じ)。後から欄を足しても、それを書いていないパックは そのまま通ります。
表示用の名前の欄も、今は持ちません——画面に出すのは id と version そのものです。
3.4 同じ名乗りのパックは2つ入れられない
同じ id のパックを2つは入れられません(エラー)。出所の表示が一意でなくなり、どちらのパックの
話なのかが読めなくなります。同じパックの2つの版を並べて入れることもできません。
3.5 欄の名前と、識別子の見せ方
3.2節の識別子と版を、どう書かせてどこに出すかを決めたもので、覆すのに人間の判断は要りません。
欄の名前は id と version で、版はYAMLの文字列として書きます。
id: sample-pack
version: '1'
識別子が出てくるのは、定義の出所の表示(<id>:<ファイル名>)・エラーメッセージ・セーブの記録
(6.4節)です。
4. 絵の有無は在庫表に問い合わせる
「絵があれば使う」の判定は、常に在庫表への問い合わせで行い、読みに行って404かどうかでは 決めません。 この不変条件が、パック対応で最も壊れやすい箇所です。
現在の絵の解決は、import.meta.glob が作った一覧(ART_BY_NAME・BACKGROUND_ART)への
問い合わせだけでできています。
- 段ごとの絵(
art_by_stage)は、宣言された接尾辞のファイルが無ければ型自身の絵へ落とす _multiplyの絵は、あれば乗算で重ねる- レーンの背景・カードの地の絵は、無ければ単色の背景板・紙のままにする
いずれも「無いのが普通」で、しかもカードやレーンの数だけ判定が走ります。ここを「読んでみて 404なら無い」に変えると、1画面あたり大量の404が出ます。
変えたのは在庫表の作り方だけです。ビルド時の定数から、起動時に同梱ぶんとパックぶんを重ねて
1回組み立てる形にしました(組み立て後は不変)。表の値はどちらも画像のURLで、パックぶんは
ZIPのエントリから作るBlobのURLです。Blobは要求された絵の分だけ作り、同じ絵なら1つを使い回します。
どの絵をいつPhaserへ読ませるかは変わりません(起動時に読み切るぶんと、土地ごとの遅延ロード、
src/art/artFiles.ts)。
【いつか: パックの着脱】 遊んでいる途中で入れ替える道は、ページを読み込み直せば同じことができるので
後から足します(Someday.md)。
5. どのパックの絵かは、定義の出所で決まる
object_def の出所(どの配布物のどのファイルから読まれたか)を辿り、そのパックの在庫表を先に
見ます。 ローダーは既に定義の出所を持っています(重複を検出したときのエラーが出所を挙げる)。
出所は型が名乗る絵の名前に添えて運びます(<id>:<絵の名前>。同梱ぶんの名前に : は入らない
ので、外せば同梱ぶんの名前になります)。
これにより、同じ識別子の絵を複数のパックが持っていても、その型を宣言した側の絵が使われます。 パックの中で見つからなければ同梱ぶんへ落ちるので、「定義だけを足して絵は同梱のものを使う」パックも そのまま書けます。
6. 差し替えは patch、絵に後勝ちは無い
絵にも定義と同じ規則を適用します。 定義は「同じ識別子が2箇所に現れたら常にエラー、意図的な
変更は patch 文法」(GameElementDefinition.md 3.4節)です。
絵だけ後勝ちにすると、パックを2つ入れた人の画面が、読み込み順という見えない要因で変わります。
型の絵は、同じ名前でも衝突しません。 どちらが出るかは定義の出所で決まる(5節)ので、 読み込み順によらず一意です。背景の絵は、同じ名前を2つのパックが持てません(エラー)。名前が 持ち主の識別子で始まる(3節)以上、同じ名前を持ち込むのは、宣言していない型の絵を配ったときだけです。
絵にはまだ patch にあたる文法がありません。他のパックや同梱ぶんの絵を、名前で指して差し替える ことはできません。
6.1 読み込みの失敗は、できたところまでで続ける
パックの中身はユーザーが書くもので、競合は起こって当たり前です——「標準の邪魔な機能を外す」
パックが2つ入れば、後の方は既に無いものを外そうとします。そこで止めるとプレイできなくなるため、
行えなかったぶんだけを捨てて先へ進みます。捨てた理由は記録に残します(src/loader/LoadReport.ts。
読める画面はまだ無く、当面はコンソールへ出ます。6.1.1 節)。
捨てる単位は2段です。
- patchの1操作(
GameElementDefinition.md3.4節)。removeの相手が 既に無い、addのキーが既にある、whereが0件または2件以上——いずれも葉の編集なので、その操作 だけを捨てれば「そのパックがそこを頼まなかった」のと同じ状態になります。 - パック全体。ZIPが壊れている・YAMLの構文エラー・識別子が同梱と衝突・参照切れ、といった失敗は
操作単位で捨てられません。定義の一部だけを生かすと参照切れが残り、壊れ方が原因から遠い場所に
出ます。そのパックの定義を全部外して組み直せば、結果は必ず「同梱ぶん+無事なパック」という、
それ自体で筋の通った世界になります(届いた配布物そのものを読めないぶんは
src/asset-pack/install.ts、定義を読んで落ちるぶんはsrc/loader/loadDefinitions.tsが外します)。
同梱ぶんの誤りは、今までどおり止めます。 ゲーム自身のバグで、外して続ける先がありません。 ここを緩めると、パックのせいに見える形で本体のバグが隠れます。
引き換えに、パックを書いた本人が自分の書き間違いに気付きにくくなります。記録を必ずコンソールへ 出すのはそのためです。
ここで守っているのは「パックの中身が期待どおりでなくても遊べる」ことだけです。セーブが指した パックが揃わない場合には及びません(6.4節)——揃わないまま開くと、同じシードから別の島が出ます。
6.1.1 捨てた定義と理由を、画面から読めるようにする【確定】【いつか: 読み込みの記録の画面】
読み込みで捨てた定義と、その理由の一覧を、画面から読めるようにします。 画面が1つ増えます。
今の時点では要らないので、入れるのは Someday.md の側です。
出どころ: #2404(読み込みで捨てた定義と理由を画面から読めるようにする。今は要らない。2026-09-29に回答)
6.2 複数のパックは、入れる人が並べた順に読む
読み込み順は取得元に並べた順で決まり、パックの側からは指定できません。 同梱ぶんが常に先頭で、 その後にパックが並んだ順で載ります。
順序が変えるのは並びだけです。同じ識別子が2度現れれば常にエラーで、後勝ちの上書きが無い(6節)ので、
どの順で読んでも出来上がる世界の中身は同じになります。順で変わるのは宣言順に振られるもの——
プロパティタグの並び、レシピ一覧の棚の並び(src/loader/WorldCodexYamlLoader.ts)といった、画面上の
カテゴリの順だけです。
パックに順序を宣言させません。 2つのパックが互いに「自分が先」と言ったときの解決規則が要り、 その規則自体が新しい未決事項になります。並べるのは入れる人なので、入れる人が決めれば足ります。
patch(GameElementDefinition.md 3.4節)が他のパックの定義を指すときだけ、
指される側を先に置く必要があります。先に無ければ、その操作だけが捨てられます(6.1節)。
6.3 識別子に名前空間を設けない【確定】
パックが増えても、識別子はCodex全体で1つの平らな名前空間のままにします。 パックの id を
接頭辞に付けたり、パックごとに名前空間を分けたりはしません。
後から名前空間を入れると、既に書かれた識別子が全部変わります。patch は他のパックの定義を識別子で
指す(GameElementDefinition.md 3.4節)ので、接頭辞が付けば参照も全部
書き換えになり、しかもパックの id を変えただけで参照が切れるようになります。
衝突は隠さずエラーにします(6節)。同名を持ち込んだパックは丸ごと外れる(6.1節)ので、入れた人には どのパックを外せばよいかが分かります。パックを書く側は、自分の識別子に固有の接頭辞を付ける慣習で 避けられます——規約にはしません。 規約にすると、同梱ぶんの識別子にも接頭辞が要ることになります。
出どころ: #656(名前空間は設けず、ぶつかったらエラー)
6.4 パックの並びが違うセーブは開かない
セーブは作られたときのパックの並び(id と version を順序どおり)を記録し、今入っているものと
食い違うスロットは開きません。
パックは土地の型も生成の重みも足せて、島は定義から作られます(generateIsland)。つまり同じシード
でも、入っているパックが違えば別の島になります。ワールド状態は保存せずシードから作り直す
(SaveDataManagement.md ワールド状態の保存節)ので、食い違ったまま開くと、
続きのつもりで知らない島が出ます。
記録の欄はセーブの形式が持ちます(同 セーブデータのスキーマ節の assetPacks)。配ってしまってからでは
足せない欄です——記録の無いセーブは「同梱ぶんだけで作られた」としか読めず、パックを入れて遊んで
いた人のスロットが、パック無しのセーブとして開かれます。まだ誰にも配っていない今のうちに足したので、
その代償は自分の手元のスロットだけで済みます(形式バージョン3までのセーブが1回そう読まれます)。
version まで含めて一致を見ます。版が上がれば定義が変わりうる以上、id だけの一致では「同じ島が出る」
とは言えません。引き換えに、パックを更新するとそれまでのスロットは開けなくなります。
棚(SaveDataManagement.md 棚節)は別です。パックが外れても、そのパックの
物の識別子を消しません。
7. 未決事項・今後の検討課題
- 絵に対する patch にあたる文法(6節)。同じ名前の絵を持つパックは、その絵を足せないままです。