MomijiUI 部品化(ビルド時展開)設計 — E2
Generated by Claude Opus 4.8 — 2026年6月2日
05-uikit-cross-media-next-steps の E2(頻出構成の部品化) を、A案=ビルド時展開で実装するための設計。メメントモリ風モックの作り込みで顕在化した「ヘッダー/タブバー/ステータス行/カードが画面間でコピペ複製」を、レンダラ(native C++ / web JS)を一切変えずに解消する。
1. 目的・非目的
目的
- 頻出構成を再利用部品(component)として1箇所で定義し、画面JSONから参照で展開する。
- native(
Momiji.cpp:nodeFromJson)と web(web-momiji)の両パーサを無改修に保つ。 momiji-validateを展開後ツリーに掛けて従来どおり検証できる。
非目的
- ランタイム動的UI(実行中に部品を差し替える等)は対象外。
{var}補間(gamehost.js)は既存のまま。 - スキーマ(
momiji-schema.json)の widget enum 拡張はしない。部品=既存12 widget の構成物。
2. 設計原則:展開はビルド時、レンダラは素のJSONだけ食う
画面JSONを読む経路が2つある(Momiji.cpp:47 の C++ 再帰パーサ / web-momiji/src/momiji.js の JS)。ここに参照解決を入れると二重実装=drift(05 のリスク欄・F2 の懸念)になる。
→ 参照解決を前処理ステップに寄せ、素の UiNode ツリーへ平坦化してから配る。 両レンダラは平坦化済みJSONを今までどおり読むだけ。scaffold-game.mjs(Node・依存ゼロ)の兄弟ツールとして実装でき、CI/AI からも呼べる。
components/*.json ─┐
├─[expand-components.mjs]→ 平坦な screens/*.json ─┬→ native nodeFromJson(無改修)
画面JSON(use参照) ─┘ │ ├→ web momiji.js(無改修)
└→ momiji-validate(無改修・展開後に検証)
3. データ形式
3.1 部品定義 components/<name>.json
部品はパラメータ(props)とスロット(子ツリー差し込み口)を持つ1つの UiNode サブツリー。
{
"name": "stat-row",
"props": { "label": "HP", "value": 0.9, "color": "#b1894e" },
"node": {
"type": "HorizontalLayout", "gap": 8,
"children": [
{ "type": "Label", "text": "{{label}}", "w": 60 },
{ "type": "Slider", "value": "{{value}}", "color": "{{color}}", "flex": 1 }
]
}
}
props: 既定値つきパラメータ。型は string / number / color(string)。node: 部品本体。文字列値の中の{{propName}}を props で置換。- スロット(子差し込み)は本体に
{ "slot": "body" }プレースホルダを置く。
3.2 画面JSONからの参照
参照ノードは type を持たず use を持つ(後述のとおり展開前JSONは momiji-schema 非適合で構わない — 検証は展開後)。
{ "type": "VerticalLayout", "children": [
{ "use": "header", "props": { "title": "キャラクター" } },
{ "use": "stat-row", "props": { "label": "ATK", "value": 0.78, "color": "#5aa05a" } },
{ "use": "card", "props": { "name": "リエラ" }, "slots": {
"body": [ { "type": "Label", "text": "Lv60" } ] } }
] }
props: 部品の既定値を上書き(指定キーのみ)。slots: スロット名 → 差し込む子ノード配列。
3.3 デリミタ規約(最重要)
| 記法 | 解決タイミング | 解決者 | 例 |
|---|---|---|---|
{{prop}} | ビルド時(展開器) | expand-components.mjs | {{title}} |
{var} | ランタイム | gamehost.js / native | {coins} |
二重ブレースと単一ブレースで層を分離。部品の中にランタイム {var} を残したい場合(例:通貨ピル部品が画面ごとに違う変数を見る)は、props で変数名を渡し {{ }} → {coins} のような単一ブレース文字列を生成できる。展開器は {{ }} のみ処理し、{ } は素通しする。
3.4 反復と選択(repeat / selection)— 計算を伴う構成
下部ナビ(全画面に同型8セル)やカードグリッド(同型カード×N)は「同じ構造の反復+選択セルの強調」で、文字列置換だけでは畳めない。これを算術なしで表現するため、子配列に反復ディレクティブを置ける。
{ "repeat": "tabs", "as": "t", "indexAs": "i", "selected": "active",
"node": { "type": "VerticalLayout", "action": "{{t.action}}", "children": [
{ "$whenSelected": { "type": "Image", "h": 4, "color": "{{navOn}}" } },
{ "type": "Label", "text": "{{t.name}}",
"textColor": { "$selected": "{{navOn}}", "$else": "{{navOff}}" } }
] } }
repeat: 配列 prop 名("tabs"/"{{tabs}}")。要素ごとにnodeを1つ生成し親の子として並べる。as/indexAs: 各要素・添字のバインド名。テンプレ内で{{t.field}}(ドット参照)・{{i}}。selected: 選択添字を持つ prop 名。反復中、その添字の要素だけ「選択」文脈になる。{"$selected":A,"$else":B}: 値の選択切替(選択セルは A、他は B)。{"$whenSelected": node}: 選択セルにのみ挿入されるノード(インジケータ等)。
インジケータは active*160 の絶対座標でなく選択セル先頭のアクセントバーで等価表現するため、算術ロジックは不要。レンダラは無改修のまま。
3.5 算術式 {{ expr }} — 重なり型カードの部品化
ソシャゲ/放置ゲームの中心は、枠・ポートレート・バッジ・星・ゲージを重ね合わせた絶対座標カード。レンダラはプレーンコンテナ(Image/Panel)の子を親原点でオフセットしない(Momiji.cpp:369/378・web 同様)ため、自己完結カードの内部 offset を相対化できない。これを算術なしでは畳めないので、{{ }} の中身を式として評価できるようにした。
{ "type": "Image", "x": "{{cx + 4}}", "y": "{{cy + ch - 56}}", "w": "{{cw - 8}}" }
- 中身が単一識別子(
{{cx}}/{{c.frame}})なら props の素の型(数値/色文字列/配列)をそのまま返す(従来どおり)。 - 演算子(
+ - * / %)・括弧を含めば数値式として評価(再帰下降・eval不使用の小型評価器)。識別子は数値に解決される必要があり、未解決・非数値は ERROR。 - データ側がカードの
cx/cyを持ち、内部 offset は式で表現 → 重なり型カードも repeat で量産でき、見た目は不変。条件付き要素(NEW バッジ)は「非該当時は透明色+空文字で常時同梱」で分岐を回避。
4. 展開アルゴリズム
expandNode(node, registry):
if node.use:
comp = registry[node.use] # 無ければ ERROR で停止
props = { ...comp.props, ...node.props } # 既定値に上書きをマージ
body = deepClone(comp.node)
substituteProps(body, props) # 文字列内 {{key}} を置換(数値prop→数値化)
fillSlots(body, node.slots ?? {}) # {slot:name} を子配列で置換、未指定スロットは除去
return expandChildren(body, registry) # 部品が部品を使う場合に再帰展開
else:
node.children = node.children.map(expandNode) # 通常ノードは子だけ展開
return node
- ネスト: 部品が別部品を
useしてよい(expandChildrenで再帰)。 - 再帰検出: 展開スタックに同名が再出現したら循環として ERROR(無限展開防止)。
- 数値prop:
"value": "{{value}}"で値全体が単一の{{key}}のとき、props が number なら文字列でなく数値として埋める(Slider.valueは number 必須)。 - 未解決
{{key}}: props に無いキーが残ったら ERROR(タイポ検出)。
5. ツール構成
packages/native-engine/tools/
scaffold-game.mjs # 既存
expand-components.mjs # 新規: <game>/ を読み、components 参照を平坦化して出力
- 入力:
<game>/screens/*.json+<game>/components/*.json - 出力: 既定で
<game>/.expanded/screens/*.json(元を破壊しない)。--in-placeで上書きも可。 - 依存ゼロ(標準 Node のみ)。
scaffold-game.mjsのcopyTree/parseArgsを踏襲。 - CLI:
node tools/expand-components.mjs --game assets/games/mementomori-mock node tools/expand-components.mjs --game <dir> --out /tmp/expanded
パイプライン上の位置
[author/AI が screens + components を書く]
→ expand-components.mjs # 平坦化
→ momiji-validate <expanded>/screens/* # 検証(既存・無改修)
→ kaedevn_game / web-momiji が <expanded> を読む
native のアセット配置(assets/games/<id>/screens/)や web の fetch(main.js)は展開後ディレクトリを指すよう配線するだけ。ビルド時に .expanded を実体の screens/ に置く運用でも可(要・配置方針の決定 → §7)。
6. テスト方針
| 層 | 内容 |
|---|---|
| 展開器ユニット | props 上書き / 数値prop / slot 差し込み / ネスト / 循環検出 / 未解決キー ERROR を Node test で網羅 |
| 検証連携 | 展開後ツリーが momiji-validate を通る(既存 CLI を呼ぶ) |
| 不変性(回帰) | メメントモリ既存画面を「部品化前の手書きJSON」と「部品化→展開した結果」でバイト一致 or ノード等価を照合。見た目を変えずにリファクタした保証 |
| 媒体一致 | 展開後JSONで native collectedHits と web 幾何を照合(05 F2 と同じ仕組み・展開後に掛ける) |
7. 方針決定(2026-06-02 確定)
前提(運用方針): 宣言的UI(MomijiUI) は生成AIが参照画像を再現して画面JSONを作る。ブロックエディタはノベルゲーム専用で当面機能追加なく、宣言的UIをエディタで開くパスは無く、作らない。確認はプレイ画面の実描画(kaedevn_game --capture 等)で行う。
- 配置方針 → 決定: 展開はパイプラインの1ステップ。AIが
use入り画面を生成 → 展開器で素のツリーへ平坦化 → レンダラは展開後を読み、プレイ画面で描画確認。エディタ経路が無いため.expanded/別置きの凝った規約は不要。現状の build-mm-screens は in-process 展開(生成時に平坦化してscreens/に直接書く)でこの方針に合致。AI生成ゲーム(game-produce)では生成後にexpand-componentsを1ステップ挟む。 - エディタ schema 連携 → 決定: 不要(やらない)。
useノードのエディタ対応・GET /api/editor-schemaへの部品掲載はしない。宣言的UIの編集はAI生成で完結し、人手のエディタ編集対象にしない。 - 部品の所在 → 決定: 横断共有
_components/(_templates/と同階層)。ゲーム個別<game>/components/も展開器はフォールバック対応済み(game-local 優先)。 - 未指定スロット → 決定: 除去(任意スロット扱い)。実装・テスト済み。
→ 4点とも確定。残るは応用(カード系の部品化・web-momiji 表示確認・CI 配線)のみ。
8. 段階導入
expand-components.mjs+ ユニットテスト(部品ゼロでも素通しする no-op から)。- メメントモリの header / tab-bar を部品化 → 全画面置換 → 不変性テストで見た目不変を保証。
- stat-row / card / currency-pill を順次部品化。
momiji-validateをパイプラインの展開後に固定(CI 配線)。- 配置方針(§7-1)を確定し native/web の参照先を展開後へ。
scaffold-game.mjsのテンプレを部品参照ベースに更新(E3 と合流)。
規模感は 05 の E2「M」のまま。レンダラ無改修なので native/web 双方が同時に恩恵を受ける。
9. 既存 build-mm-screens.mjs との関係
メメントモリ画面は tools/build-mm-screens.mjs(JS ビルダー)が生成しており、そこでは既に nav(active)(下部8タブ)・topTabs(...)・カード factory として JS 関数で部品化されている。つまり「複製」は生成された出力JSONに出ていて、ソース側は JS で factoring 済み。ただしこの方式は native モック専用・一回限りで、web-momiji / momiji-validate / game-produce(AI) のどれにも乗らない。
本設計の宣言的 components/*.json +展開器は、その JS factory を移植可能・検証可能・AI/web 共有可能にした版。topTabs() は top-tabs.json、nav() は bottom-nav.json に1対1で写せる。移行は build-mm-screens の各 factory を宣言的部品へ置き換える形で段階実施できる。
10. 実装状況(2026-06-02)
- ✅
tools/expand-components.mjs実装(props{{ }}置換 / ドット参照 / 数値prop / slot / repeat / selection($selected・$whenSelected) / ネスト / 循環検出 / 未解決ERROR /{var}素通し / game-local + 共有_componentsフォールバック /.expanded出力)。 - ✅
tools/expand-components.test.mjs— ユニット20件すべて通過(node --test)。 - ✅ 共有部品:
_components/top-tabs.json(topTabs()相当)、_components/bottom-nav.json(nav(active)相当・8セル+選択ハイライト)。 - ✅ E2E 実証:
top-tabs: 展開結果が実characters.jsonの ToggleGroup とノード一致。bottom-nav:{"use":"bottom-nav","props":{"active":N}}の1行 → 8セル+選択セルのアクセント/色分けを算術なしで生成。CLI 展開 →momiji-validate通過。全9画面の約11ノード複製を1参照に集約可能。
- ✅ build-mm-screens.mjs を部品参照へ移行:
nav()→{use:bottom-nav}、topTabs()→{use:top-tabs, slots}。write()が書き出し時にexpandScreenで平坦化するので生成 screens にuseは残らない(native/web 無改修)。全9画面 regenerate →momiji-validate通過、use残存ゼロ。- 視覚検証: 旧インライン nav と 部品 bottom-nav を
kaedevn_game --captureで実描画して等価確認(暗色バー/8セル/選択セルの金アクセント・金文字が一致)。characters(top-tabs+nav)・quest(nav active=3) の実画面も正しく描画。 - 構造は変化(nav が flat 3兄弟 →
Image>HorizontalLayoutの入れ子、インジケータは選択セル先頭アクセントに移動)だが表示は不変。
- 視覚検証: 旧インライン nav と 部品 bottom-nav を
- ✅ §7 方針4点を確定(エディタ連携=不要、配置=パイプライン1ステップ展開、部品所在=共有
_components/、未指定slot=除去)。前提: 宣言的UIは生成AIが画像を再現して作り、確認はプレイ画面の実描画。 - ✅ web-momiji マルチプラットフォーム確認: 静的配信+Playwright で web-momiji(Canvas2D) に同一 screens を描画。
characters(top-tabs+nav active=1) とquest(nav active=3) が native(kaedevn_game --capture) と同一表示。部品(top-tabs/bottom-nav)・selection($selected/$whenSelected) が web でも正しく動作=同じ画面JSONが native/web で一致。 - ✅ 算術式
{{ expr }}(§3.5)を展開器に追加(小型評価器・eval不使用・テスト計30件通過)。今後ソシャゲ/放置ゲームの重なり型カードを部品化するための土台として A 案(展開器に算術)を採用。 - ✅ 重なり型カードの部品化: 共有部品
_components/char-card.json(枠/ポートレート/属性バッジ/星/Lvバー/NEW を算術 offset で内部配置)。build-mm-screens のcharCard()を use 参照へ移行。characters 全21枚を部品化し、カード移行前(HEAD) と移行後で PNG バイト完全一致(cmp確認)=見た目不変を厳密検証。 - ✅ パイプライン組み込み:
scaffold-game.mjsに展開ステップ(expandScreensInPlace)を追加。テンプレがuse/repeat/算術を含んでも scaffold 時に平坦化され、native/web はそのまま読める。 - ✅ グリッド部品
_components/card-grid.json:repeat+ 算術(列i % cols/ 行(i - i % cols) / colsの整数除算)で char-card をグリッド配置。データ配列を渡すだけでカード盤面を生成。 - ✅ 新ジャンルテンプレ
_templates/gacha/: bottom-nav + card-grid + char-card のみで構成。scaffold-game --genre gachaで新規ガチャゲームが共有部品から1本立ち上がることを実証(ヘッダー+7列14枚グリッド+10連ボタン+ナビ、momiji-validate通過・描画確認)。=メメントモリ以外の新規ゲームでも本機構で量産可能。 - ✅
game-produceskill に部品システムを明記: パイプライン図に展開ステップ、共有部品カタログ(top-tabs/bottom-nav/char-card/card-grid)、use/repeat/selection/算術の記法、scaffold 自動展開を追記。AI 生成経路が部品を使う前提に。 - ✅ 放置ゲーテンプレ
_templates/idle/: bottom-nav + 放置報酬パネル + ステージ + ボタンで構成。scaffold --genre idleで放置RPGメイン画面が立ち上がり描画確認({gold}/{stage}補間も動作)。gacha と合わせ ソシャゲ/放置の2ジャンルが部品ベースで量産可能。 - ✅ CI 配線:
tools/validate-screens.mjs(実ゲームは screens を直接、_templates は展開してからmomiji-validate)。ci.ymlの native-tests に検証ステップ追加(momiji-validate はそこで既にビルド済み)、pre-pushにもバイナリがあれば走る形で追加。不正画面で exit 1 を確認。 - ✅ SSIM 回帰:
tools/screen-regression.mjs— 画面を撮り直しコミット済みベースライン PNG と SSIM 比較(ImageMagickcompare・CPU・GPU不要)。閾値 0.99 未満で exit 1、--updateで更新。無変更→全画面 1.0000、パネル色変更→0.9292 で検知。設計 03 の回帰自動化の実装。 - ⬜ 残(任意・設計判断なし): 実データ(実画像)でのカード/立ち絵差し込み(
sprite/fit:coverは実装済、素材を入れるだけ)、放置/別ソシャゲの本実装、game-produceのジャンルテンプレ拡充。
bottom-nav の設計判断(採用: 展開器に repeat/selection を追加)
nav(active) は「アクティブ色分け+インジケータ位置 active*160」という計算を持ち、ToggleGroup は各セルを1行テキストでしか描かない(native Momiji.cpp:428 / web momiji.js:210 同一挙動)ため2段セルを畳めない。選択肢は (1) 展開器に repeat/selection 追加、(2) nav を生成据え置き、(3) TabBar ウィジェット新設(レンダラ改修=Cゾーン)。(1) を採用 — レンダラ無改修を保ち、nav と同型反復(カードグリッド等)の双方に効くため。
この文書は Claude Opus 4.8 が生成しました。内容の正確性は人間のレビューを経て確認してください。
Claude Opus 4.8 — Anthropic