ショップと売買
プレイヤーが買い物できるお店を作ります。所持金と売り物が見えていて、「購入」を押すと金貨が減り、何を買ったかが知らされます。金貨が足りなければ、そう知らされます。お店全体を動かすのはビヘイビア 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:ショップのページを作る
上部バー中央の プレイヤー画面 を押します。ページを追加 でページを足し(白紙のページ)、名前を「ショップ」にします。
所持金:{Gold}と書いた 文字 の部品を置く(変数の値を挿入… を使います)- アイテムごとに、名前と説明の 文字 の部品と、
購入 · 20 金貨と書いた ボタン を置く - ボタンの 押したとき、順番に に ビヘイビアを動かす → 購入 を追加し、+ 値を渡す を 2 回押して、
item=Potion、price=20とする。剣ならIron Swordと50 - 会話のページへ戻る ページへ移動 のボタンを置き、会話のページにはこのページへ行く「ショップ」ボタンを置く
買えないアイテムをわかるようにしたいときは、購入ボタンを複製してコピーのほうをグレーの見た目にし、表示するタイミング → 変数が条件を満たすとき を使います。グレーのほうは Gold より小さい 20、通常のほうは Gold 以上 20 です。グレーのボタンからもビヘイビアは動くので、押せば「金貨が足りない」の通知が出ます。
編集 をオフにすると、ページを実際に押して試せます。プレビューでは AI は呼ばれませんが、金貨が変わるのは確認できます。
値段を変えたいとき
値段は変数から取ることもできます。+ 値を渡す で 20 の代わりに {{ポーションの値段}} と書きます。その変数を変えれば(たとえばお祭りのあいだだけビヘイビアで変えるなど)、すべてのボタンがそれに合わせて変わります。
ステップ 4:プレイテスト
上部バーの プレイ を押します。
- Gold は 100。ショップ を開く
- ポーションを買う:Gold 80、「購入:Potion」。メッセージを送ると、AI がそれに触れ、ポーションがかばんに入る
- もう 1 つ買う:Gold 60
- 剣を買う:Gold 10
- 何かを買おうとする:「金貨が足りない。Potion は 20 枚必要。」Gold は 10 のまま
プレイテスト右側の このターン で、購入のビヘイビアが動いたかどうか、動かなかったならその理由がわかります。
うまくいかないときは:
| 症状 | よくある原因 | 直し方 |
|---|---|---|
| 購入を押しても何も起きない | ボタンの ビヘイビアを動かす が別のものを指している、またはビヘイビアが 有効 になっていない | 両方を確認する |
| 金貨が変わらない | 渡した値の名前が、ビヘイビアの {param.…} と違う | ボタン側は price、ビヘイビア側は {param.price}。綴りをそろえる |
| 金貨が足りないのに何も出ない | 条件が合わないときに伝える が空 | 一文を書く |
| 金貨がマイナスになる | 条件がない、または Gold に最小値がない | 次の場合のみ を残し、範囲 を 0 からにする |
| アイテムがかばんに入らない | AI が追加しなかった | ビヘイビアの AIに伝える と持ち物の変数のビヘイビアルールを残すか、上の確実な方法のどれかを使う |
カスタムコードの方法:チャットの下のショップパネル
この方法では、ショップをインターフェースのコードで描きます。金貨の確認には引き続き 購入 のビヘイビアを使うので、値段と「金貨が足りない」のメッセージはキャンバスに置いたまま、購入が通ったらコードが inventory のリストにアイテムを自分で追加します。inventory という名前で、初期値 [] のリスト/表の変数があるものとします。
パネル → フロントエンドコード で index.tsx を開き、デフォルトの return <Chat /> を置き換えます。
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 つに値を渡すやり方は、値段が付くものやメニューから選ぶものなら何にでも使えます。クラフト、訓練、雇用など。ビヘイビアの「ボタンから値を渡す」を参照してください。
