v
Skip to content

ショップと売買 ​

プレイヤーが買い物できるお店を作ります。所持金と売り物が見えていて、「購入」を押すと金貨が減り、何を買ったかが知らされます。金貨が足りなければ、そう知らされます。お店全体を動かすのはビヘイビア 1 つだけで、各ボタンはどのアイテムをいくらで買うかを伝えるだけです。


完成するもの ​

  • 所持金を画面に表示
  • ショップのページ:アイテムごとに購入ボタン。ポーション(20 金貨)、Iron Sword(50 金貨)
  • 購入:値段の分だけ金貨が減り、何を買ったかの通知が出て、AI にも伝わる
  • 金貨が足りないとき:「金貨が足りない。Iron Sword は 50 枚必要」。何も引かれません
  • 買ったものはプレイヤーの持ち物に入る

仕組み ​

プレイヤーが「Iron Sword を買う」をタップ
  → ボタンがビヘイビア「購入」を動かし、item = Iron Sword、price = 50 を渡す
  → ビヘイビアが確認:Gold ≥ {param.price}?
    → はい:Gold から {param.price} を減算、通知「Iron Sword を買った」、AI に伝える
    → いいえ:「条件が合わないときに伝える」の通知が出て、ほかには何も起きない

{param.price} と {param.item} には、ボタンから渡された値が入ります。だから、アイテムがいくつあってもビヘイビアは 1 つで足ります。


作り方 ​

ステップ 1:変数を作る ​

カードの下の 追加できるもの から + 変数 を押します。

Gold ​

項目値
名前Gold
型数値
初期値100
範囲0 〜(空欄)
ビヘイビアルールプレイヤーの所持金。ショップでの支払いは自動で引かれる。物語の中で変えてもよい:クエストの報酬、盗難、宝箱など。

最小値の 0 は安全網です。買えるかどうかはビヘイビアがもう確認していますが、範囲を設定しておけば、金貨がマイナスの数字で表示されることはありません。

プレイヤー画面に表示 を押すと、チャット中も Gold がずっと見えます。

持ち物 ​

いちばん手軽な持ち物は、プレイヤー画面 → テンプレート → 持ち物 です。かばんのページ、何か新しく手に入ったときのポップアップ、そして 持ち物 というリストの変数が付いてきます。この変数のビヘイビアルールは、物語で何かを渡したらアイテムを追加するよう AI に指示しています。持ち物と装備のレシピを参照してください。


ステップ 2:「購入」のビヘイビアを 1 つ作る ​

追加できるもの から + ビヘイビア を押します。

部分設定
発動するときプレイヤーがボタンを押した、ボタン buy
次の場合のみGold ≥ {param.price}
効果変数を変更:Gold 減算 {param.price}
通知を表示:購入:{param.item}、スタイル 成功
AIに伝える:プレイヤーはたったいま {param.item} を {param.price} 金貨で買った。持ち物に追加すること。
条件が合わないときに伝える金貨が足りない。{param.item} は {param.price} 枚必要。

最後の行がないと、お金が足りないプレイヤーがボタンを押しても、何も起きないように見えてしまいます。

AIに伝える は、次の返信に向けて AI にひとこと渡します。持ち物テンプレートのビヘイビアルールがあるので、AI はアイテムをかばんに入れ、「手に入れた」の通知を出します。

アイテムを確実に持ち物へ入れるには

ビヘイビアの効果で変えられるのは数値と文字で、リストに行を足すことはできません。ここでは AIに伝える を受けて AI がアイテムを追加しています。正確さが必要なら:

  • 数えるアイテムは数値で:ポーションに専用の数値の変数を用意し、別に「ポーションを買う」ビヘイビアを作って、変数を変更 でポーション 加算 1 も行う
  • コードの効果:ビヘイビアの効果に コード を追加し、ctx.push("Inventory", { name: "Potion", note: "Heals a little." }) のような 1 行を書く。ビヘイビアの「コードのビヘイビア」を参照
  • インターフェースのコード:下のカスタムコードの方法では、コードが自分でリストに書き込みます

ステップ 3:ショップのページを作る ​

上部バー中央の プレイヤー画面 を押します。ページを追加 でページを足し(白紙のページ)、名前を「ショップ」にします。

  1. 所持金:{Gold} と書いた 文字 の部品を置く(変数の値を挿入… を使います)
  2. アイテムごとに、名前と説明の 文字 の部品と、購入 · 20 金貨 と書いた ボタン を置く
  3. ボタンの 押したとき、順番に に ビヘイビアを動かす → 購入 を追加し、+ 値を渡す を 2 回押して、item = Potion、price = 20 とする。剣なら Iron Sword と 50
  4. 会話のページへ戻る ページへ移動 のボタンを置き、会話のページにはこのページへ行く「ショップ」ボタンを置く

買えないアイテムをわかるようにしたいときは、購入ボタンを複製してコピーのほうをグレーの見た目にし、表示するタイミング → 変数が条件を満たすとき を使います。グレーのほうは Gold より小さい 20、通常のほうは Gold 以上 20 です。グレーのボタンからもビヘイビアは動くので、押せば「金貨が足りない」の通知が出ます。

編集 をオフにすると、ページを実際に押して試せます。プレビューでは AI は呼ばれませんが、金貨が変わるのは確認できます。

値段を変えたいとき

値段は変数から取ることもできます。+ 値を渡す で 20 の代わりに {{ポーションの値段}} と書きます。その変数を変えれば(たとえばお祭りのあいだだけビヘイビアで変えるなど)、すべてのボタンがそれに合わせて変わります。


ステップ 4:プレイテスト ​

上部バーの プレイ を押します。

  1. Gold は 100。ショップ を開く
  2. ポーションを買う:Gold 80、「購入:Potion」。メッセージを送ると、AI がそれに触れ、ポーションがかばんに入る
  3. もう 1 つ買う:Gold 60
  4. 剣を買う:Gold 10
  5. 何かを買おうとする:「金貨が足りない。Potion は 20 枚必要。」Gold は 10 のまま

プレイテスト右側の このターン で、購入のビヘイビアが動いたかどうか、動かなかったならその理由がわかります。

うまくいかないときは:

症状よくある原因直し方
購入を押しても何も起きないボタンの ビヘイビアを動かす が別のものを指している、またはビヘイビアが 有効 になっていない両方を確認する
金貨が変わらない渡した値の名前が、ビヘイビアの {param.…} と違うボタン側は price、ビヘイビア側は {param.price}。綴りをそろえる
金貨が足りないのに何も出ない条件が合わないときに伝える が空一文を書く
金貨がマイナスになる条件がない、または Gold に最小値がない次の場合のみ を残し、範囲 を 0 からにする
アイテムがかばんに入らないAI が追加しなかったビヘイビアの AIに伝える と持ち物の変数のビヘイビアルールを残すか、上の確実な方法のどれかを使う

カスタムコードの方法:チャットの下のショップパネル ​

この方法では、ショップをインターフェースのコードで描きます。金貨の確認には引き続き 購入 のビヘイビアを使うので、値段と「金貨が足りない」のメッセージはキャンバスに置いたまま、購入が通ったらコードが inventory のリストにアイテムを自分で追加します。inventory という名前で、初期値 [] のリスト/表の変数があるものとします。

パネル → フロントエンドコード で index.tsx を開き、デフォルトの return <Chat /> を置き換えます。

tsx
export default function MyWorld() {
  const api = useYumina();
  const msgs = api.messages || [];

  const gold = Number(api.variables["Gold"] ?? 0);
  const inventory = Array.isArray(api.variables.inventory) ? api.variables.inventory : [];

  const shopItems = [
    { name: "Potion",     price: 20, icon: "🧪", desc: "Restores a little health" },
    { name: "Iron Sword", price: 50, icon: "⚔️", desc: "A plain iron sword" },
  ];

  const buy = async (item) => {
    // Run the "buy" behavior and wait for it; firedIds is empty if its
    // condition failed (it shows its own "not enough gold" notice)
    const result = await api.executeActionAndWait("buy", { item: item.name, price: item.price });
    if (result.firedIds.length > 0) {
      const current = Array.isArray(api.variables.inventory) ? api.variables.inventory : [];
      api.setVariable("inventory", [...current, item.name]);
    }
  };

  return (
    <Chat renderBubble={(msg) => {
      const isLastMsg = msg.messageIndex === msgs.length - 1;
      return (
        <div>
          <div
            style={{ color: "#e2e8f0", lineHeight: 1.7 }}
            dangerouslySetInnerHTML={{ __html: msg.contentHtml }}
          />

          {isLastMsg && (
            <div style={{
              marginTop: "16px",
              padding: "16px",
              background: "rgba(15, 23, 42, 0.6)",
              borderRadius: "12px",
              border: "1px solid #334155",
            }}>
              {/* Gold */}
              <div style={{
                display: "flex", alignItems: "center", gap: "8px", marginBottom: "16px",
                padding: "10px 14px", background: "linear-gradient(135deg, #78350f, #92400e)",
                borderRadius: "8px", border: "1px solid #b45309",
              }}>
                <span style={{ fontSize: "20px" }}>💰</span>
                <span style={{ color: "#fde68a", fontSize: "16px", fontWeight: "bold" }}>{gold} Gold</span>
              </div>

              {/* Items for sale */}
              <div style={{ display: "flex", flexDirection: "column", gap: "8px", marginBottom: "16px" }}>
                {shopItems.map((item) => {
                  const affordable = gold >= item.price;
                  return (
                    <div key={item.name} style={{
                      display: "flex", alignItems: "center", justifyContent: "space-between",
                      padding: "10px 14px", background: "rgba(30, 41, 59, 0.8)",
                      borderRadius: "8px", border: "1px solid #475569",
                    }}>
                      <div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
                        <span style={{ fontSize: "22px" }}>{item.icon}</span>
                        <div>
                          <div style={{ color: "#e2e8f0", fontSize: "14px", fontWeight: "600" }}>{item.name}</div>
                          <div style={{ color: "#64748b", fontSize: "12px" }}>{item.desc}</div>
                        </div>
                      </div>
                      <button
                        onClick={() => buy(item)}
                        style={{
                          padding: "6px 16px", borderRadius: "6px", fontSize: "13px", fontWeight: "600", whiteSpace: "nowrap",
                          background: affordable ? "#047857" : "#4b5563",
                          border: "1px solid " + (affordable ? "#10b981" : "#6b7280"),
                          color: affordable ? "#a7f3d0" : "#9ca3af",
                          cursor: "pointer",
                          opacity: affordable ? 1 : 0.6,
                        }}
                      >
                        {item.price} Gold
                      </button>
                    </div>
                  );
                })}
              </div>

              {/* Inventory */}
              {inventory.length === 0 ? (
                <div style={{
                  padding: "20px", textAlign: "center", color: "#475569", fontSize: "13px",
                  background: "rgba(30, 41, 59, 0.4)", borderRadius: "8px", border: "1px dashed #334155",
                }}>
                  Inventory is empty
                </div>
              ) : (
                <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))", gap: "8px" }}>
                  {inventory.map((name, idx) => {
                    const def = shopItems.find((s) => s.name === String(name));
                    return (
                      <div key={idx} style={{
                        display: "flex", flexDirection: "column", alignItems: "center", gap: "4px",
                        padding: "10px 6px", background: "rgba(30, 41, 59, 0.8)",
                        borderRadius: "8px", border: "1px solid #475569",
                      }}>
                        <span style={{ fontSize: "24px" }}>{def ? def.icon : "📦"}</span>
                        <span style={{ color: "#cbd5e1", fontSize: "11px", textAlign: "center" }}>{String(name)}</span>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>
          )}
        </div>
      );
    }} />
  );
}

コードがしていること:

  • api.executeActionAndWait("buy", { item, price }) は、プレイヤー画面のボタンが渡すのと同じ値で購入のビヘイビアを動かし、終わるのを待ちます
  • result.firedIds には動いたビヘイビアが入ります。空なら条件を満たさなかったということで、ビヘイビアがもう「金貨が足りない」の通知を出しているので、何も追加しません
  • api.setVariable("inventory", [...current, item.name]) でアイテムをリストに追加します
  • 買えないアイテムはグレーになりますが、押すことはできるので、プレイヤーには通知が出ます
  • repeat(auto-fill, minmax(80px, 1fr)) で、幅に収まるだけ持ち物のマスを 1 行に並べます

この方法を使うときは、ビヘイビアの AIに伝える を プレイヤーはたったいま {param.item} を {param.price} 金貨で買った。 に変えて、AI がアイテムを二重に追加しないようにします。

パネル → フロントエンドコード で、ファイルの横の状態が緑の OK になっていれば大丈夫です。

自分でコードを書きたくないときは

キャンバス右上の AIアシスタント を押して、ほしいショップを伝えます。


さらに発展させる ​

アイテムを増やす ​

アイテムと値段を指定したボタンを 1 つ足します(コードなら shopItems に 1 行)。ビヘイビアは変えなくて大丈夫です。

売る ​

2 つ目のビヘイビア「売却」を作ります。ボタン sell、Gold 加算 {param.price}、AIに伝える は プレイヤーは {param.item} を売った。持ち物から取り除くこと。

金貨を稼ぐ ​

  • Nターンごと(たとえば 3):変数を変更 で Gold 加算 10
  • AIがキーワードを発言 戦いに勝った, 報酬を手に入れた:Gold 加算 25(キーワードのどれか 1 つで発動します)
  • AI 自身が、Gold のビヘイビアルールに従って、物語の中の戦利品や報酬を反映する
  • クエストトラッカーのクエスト報酬

値切れる店主 ​

キャラクターとして答え、ときどき値引きもしてくれる商人を作るなら、UI 型の AI を追加して値段の欄を持つ 回答形式 を設定し、ボタンの AI を呼ぶ から呼び出します。


早見表 ​

やりたいことやり方
金貨を管理する最小値 0 の数値の変数
ショップ全体をビヘイビア 1 つでプレイヤーがボタンを押した、値は {param.item} / {param.price} で読む
アイテムと値段を渡すボタンの手順 ビヘイビアを動かす → + 値を渡す
買えるか確認する次の場合のみ Gold ≥ {param.price}
買えないときに知らせる条件が合わないときに伝える
購入を知らせる通知を表示、スタイル 成功
アイテムを持ち物に入れるAIに伝える + 持ち物テンプレート、アイテムごとの数値の変数、コード の効果、またはインターフェースのコード
買えないものをグレーにする表示するタイミング を設定したボタンを 2 種類用意する
コードから同じビヘイビアを動かすapi.executeActionAndWait("buy", { item, price })

これはレシピ #3 です

ボタンからビヘイビア 1 つに値を渡すやり方は、値段が付くものやメニューから選ぶものなら何にでも使えます。クラフト、訓練、雇用など。ビヘイビアの「ボタンから値を渡す」を参照してください。