← MomijiUI ドキュメント

MomijiUI 部品化(ビルド時展開)設計 — E2

Generated by Claude Opus 4.8 — 2026年6月2日

05-uikit-cross-media-next-stepsE2(頻出構成の部品化) を、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.mjscopyTree/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. 配置方針 → 決定: 展開はパイプラインの1ステップ。AIが use 入り画面を生成 → 展開器で素のツリーへ平坦化 → レンダラは展開後を読み、プレイ画面で描画確認。エディタ経路が無いため .expanded/ 別置きの凝った規約は不要。現状の build-mm-screens は in-process 展開(生成時に平坦化して screens/ に直接書く)でこの方針に合致。AI生成ゲーム(game-produce)では生成後に expand-components を1ステップ挟む。
  2. エディタ schema 連携 → 決定: 不要(やらない)use ノードのエディタ対応・GET /api/editor-schema への部品掲載はしない。宣言的UIの編集はAI生成で完結し、人手のエディタ編集対象にしない。
  3. 部品の所在 → 決定: 横断共有 _components/_templates/ と同階層)。ゲーム個別 <game>/components/ も展開器はフォールバック対応済み(game-local 優先)。
  4. 未指定スロット → 決定: 除去(任意スロット扱い)。実装・テスト済み。

→ 4点とも確定。残るは応用(カード系の部品化・web-momiji 表示確認・CI 配線)のみ。

8. 段階導入

  1. expand-components.mjs + ユニットテスト(部品ゼロでも素通しする no-op から)。
  2. メメントモリの header / tab-bar を部品化 → 全画面置換 → 不変性テストで見た目不変を保証。
  3. stat-row / card / currency-pill を順次部品化。
  4. momiji-validate をパイプラインの展開後に固定(CI 配線)。
  5. 配置方針(§7-1)を確定し native/web の参照先を展開後へ。
  6. 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.jsonnav()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.jsontopTabs() 相当)、_components/bottom-nav.jsonnav(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 の入れ子、インジケータは選択セル先頭アクセントに移動)だが表示は不変
  • ✅ §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-produce skill に部品システムを明記: パイプライン図に展開ステップ、共有部品カタログ(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 比較(ImageMagick compare・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

Ad: inContent (336x280)
Ad: stickyBottom (728x90)
kaedevn - ノベルゲームを作れるプラットフォーム