背包与装备
一个玩家能打开、能用的背包:故事里拿到的东西会进背包,药水可以喝(生命涨,数量减),武器可以装备上,让 AI 知道玩家手里拿着什么。背包、药水和武器都能不写代码做出来;每件物品自带按钮的完整物品格子,放在最后写代码的做法里。
你要做的东西
- 一个背包,玩家从聊天页打开:捡到的东西都在里面,每样配一句描述,故事里拿到新东西时会弹窗
- 一个喝药水的按钮:生命 +20,药水少一瓶,没有了就提示「药水用完了!」
- 一个装备按钮:武器栏显示「铁剑」,AI 会写玩家握着它
- AI 跟着剧情更新背包:战利品、奖励、丢了或坏了的东西
各部分谁来管
| 什么 | 谁来管 | 为什么 |
|---|---|---|
| 故事里捡到的物品列表 | AI,靠背包变量的行为规则 | 只有 AI 知道故事什么时候把东西交给玩家 |
| 数药水、回血 | 行为 | 算数要准 |
| 当前装备的武器 | 按钮或行为 | 就一个值,玩家选的时候设好 |
| 把这些显示出来 | 玩家界面 | 模板和部件,不用代码 |
一步一步来
第 1 步:加背包模板
点顶栏中间的 玩家界面,再点 模板,加 背包(在 玩的时候 · 从聊天页的「菜单」打开 里):点物品就是拿出来用;故事里拿到新东西会弹窗。
它会加一个背包页、聊天页上的一个弹窗,还有两个变量:
| 变量 | 是什么 |
|---|---|
| 背包(列表/表格) | 一件物品一行,每行有名字和一句备注 |
| 刚得到的东西(文字) | 玩家刚拿到的东西;弹窗显示它,关掉时清空 |
两个变量都自带行为规则,教 AI 怎么用。背包的规则里有这么一段:
玩家拿到东西时 push 一件,同一回合把 new_item 设成「物品名——一句话」;用掉或丢了就 delete 它的下标(从 0 数)。不要每回合都塞东西。
AI 在故事里照着做,所以战利品、礼物、奖励会自己进背包,你什么都不用管。
在背包页上点一件物品,会替玩家说一句「(我拿出旧怀表)」再回到聊天,AI 就会把用它的过程演出来。
开局就有的物品写进背包变量的 初始值,比如:
[{"name": "铁剑", "note": "朴素,但手感很稳。"}]第 2 步:能数清楚的药水
背包是 AI 来写的列表。药水还剩几瓶这种要精确数字的东西,用一个数值变量加一条行为。
变量(还可以加 里的 + 变量):
| 名称 | 类型 | 初始值 | 范围 | 备注 |
|---|---|---|---|---|
| 生命 | 数值 | 80 | 0 到 100 | 行为规则:玩家受伤时减少;休息时慢慢恢复。 |
| 药水 | 数值 | 2 | 0 到(不填) | 行为规则:玩家身上有几瓶治疗药水。找到或买到时增加。 |
生命一开始不满,玩家才有理由试试喝药水。
行为(+ 行为):「喝药水」
| 部分 | 设置 |
|---|---|
| 什么时候触发 | 玩家点了某个按钮,按钮 drink-potion |
| 如果…… | 药水 大于 0 |
| 效果 | 修改变量:药水 减去 1 |
修改变量:生命 加上 20 | |
显示通知:你喝下一瓶药水。生命 +20,样式 成功 | |
告诉 AI:玩家刚喝了一瓶治疗药水。 | |
| 条件不满足时告诉玩家 | 药水用完了! |
生命不会超过 100:变量的范围会卡住它。
按钮:在 玩家界面 里,对话页或者背包页上加一个 按钮,文字写「喝药水(还剩 {药水} 瓶)」(数量用 插入变量的值… 插进去)。在 按下时,依次做 里加 让一条行为生效 → 喝药水。
第 3 步:装备武器
变量:当前武器,文字,初始为空。行为规则:
玩家手里拿着的武器;空着就是赤手空拳。打斗时要写到它。如果故事里它坏了或被夺走,就把它设回空。按钮:「装备铁剑」,两个步骤:
- 改变量:当前武器,设为,
铁剑 - 弹出提示:
装备了铁剑
给它设 什么时候显示 → 变量满足条件时:当前武器 不等于 铁剑,装备上以后按钮就消失。旁边放一个 文字 部件写 武器:{当前武器},用来显示武器栏。
AI 每回合的提示里都看得到当前武器,所以它的下一次回复里玩家就握着这把剑。想让它马上就有反应,就让按钮 让一条行为生效,由那条行为去改变量,再加一个 告诉 AI:玩家刚装备了铁剑。
第 4 步:试玩
点顶栏的 试玩。
- 生命显示 80,药水 2,武器栏是空的
- 点 喝药水:生命 100,药水 1,弹出提示
- 再点两次:第二次弹出「药水用完了!」,什么都没变
- 点 装备铁剑:武器栏显示铁剑,按钮消失。打一架,AI 会把剑写进去
- 一直玩到故事给你一样东西:弹窗显示它,背包里也有了(菜单 → 背包)
如果出了问题:
| 现象 | 可能的原因 | 怎么改 |
|---|---|---|
| 药水按钮没反应 | 按钮叫的行为和行为里的按钮名对不上 | 让一条行为生效 要指向「喝药水」 |
| 药水变成了负数 | 少了 如果…… 条件 | 加上 药水 大于 0 |
| 生命超过了 100 | 生命没设上限 | 把它的 范围 设成 0 到 100 |
| 东西一直没进背包 | AI 没在写背包变量 | 检查背包变量的行为规则还在,AI 权限 是 AI 可读可写 |
| 弹窗一直不出来 | AI 改了背包,但没改刚得到的东西 | 保留刚得到的东西的行为规则,它要求同一回合两个都改 |
为什么这里光靠行为做不完
行为的效果能改数字和文字(设为、加上、减去、乘以、切换、追加文字)。它没法往列表里加一行,没法在列表里找到「药水」那一行,也没法把它删掉。条件能检查列表是不是 包含 某个简单的值,但看不到行里面的内容。所以这个食谱把物品列表交给 AI,要精确数字的东西各自用一个数值变量来数。
行为的 代码 效果可以往列表里加东西,前提是你写一行 JavaScript,比如 ctx.push("背包", { name: "药水", note: "能治一点伤。" })。见行为 · 代码行为。
想两样都要(一个真正的物品格子,每件物品有数量,还有自己的使用或装备按钮),就在界面代码里写。
写代码的做法:能使用、能装备的物品格子
这个版本把整个背包放在一个叫 inventory 的列表/表格变量里,每件物品带数量,使用和装备都在界面代码里处理。它还用到数值变量 hp(0 到 100,初始 80)和文字变量 equipped_weapon。
inventory 的初始值:
[{"name":"药水","icon":"🧪","count":2},{"name":"铁剑","icon":"⚔️","count":1}]它的行为规则:
玩家的物品。每件是 {"name", "icon", "count"}。玩家找到东西时 push 一件新物品。数量交给背包按钮去管。面板 → 前端代码 → 打开 index.tsx,把默认的 return <Chat /> 换掉:
export default function MyWorld() {
const api = useYumina();
const msgs = api.messages || [];
const hp = Number(api.variables.hp ?? 80);
const equippedWeapon = String(api.variables.equipped_weapon || "");
const inventory = Array.isArray(api.variables.inventory) ? api.variables.inventory : [];
// 用掉一件:数量减一,减到零就删掉,再执行它的效果
function useItem(itemName) {
const inv = Array.isArray(api.variables.inventory) ? api.variables.inventory : [];
const idx = inv.findIndex((it) => it && it.name === itemName);
if (idx === -1) {
api.showToast(itemName + "用完了!", "error");
return;
}
const item = inv[idx];
const next = inv.slice();
if (Number(item.count) <= 1) next.splice(idx, 1);
else next[idx] = { ...item, count: Number(item.count) - 1 };
api.setVariable("inventory", next);
if (itemName === "药水") {
api.setVariable("hp", Math.min(Number(api.variables.hp ?? 0) + 20, 100));
api.showToast("喝下一瓶药水!HP +20", "success");
api.injectContext("玩家刚喝了一瓶治疗药水。");
}
}
function equipItem(itemName) {
if (equippedWeapon === itemName) {
api.showToast(itemName + "已经装备上了!", "info");
return;
}
api.setVariable("equipped_weapon", itemName);
api.showToast("装备了" + itemName + "!", "success");
api.injectContext("玩家刚装备了" + itemName + "。");
}
// 每件物品配什么按钮
const itemActions = {
"药水": { type: "consumable", label: "使用", run: () => useItem("药水") },
"铁剑": { type: "equipment", label: "装备", run: () => equipItem("铁剑") },
};
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",
}}>
{/* 生命条 */}
<div style={{ display: "flex", alignItems: "center", gap: "10px", marginBottom: "14px" }}>
<span style={{ fontSize: "16px" }}>❤️</span>
<div style={{ flex: 1 }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "4px" }}>
<span style={{ color: "#94a3b8", fontSize: "12px" }}>HP</span>
<span style={{ color: "#e2e8f0", fontSize: "12px", fontWeight: "bold" }}>{hp} / 100</span>
</div>
<div style={{ height: "8px", background: "#1e293b", borderRadius: "4px", overflow: "hidden" }}>
<div style={{
height: "100%",
width: Math.min(hp, 100) + "%",
background: hp > 50
? "linear-gradient(90deg, #22c55e, #4ade80)"
: hp > 20
? "linear-gradient(90deg, #eab308, #facc15)"
: "linear-gradient(90deg, #ef4444, #f87171)",
borderRadius: "4px",
transition: "width 0.3s ease",
}} />
</div>
</div>
</div>
{/* 武器栏 */}
<div style={{
display: "flex", alignItems: "center", gap: "8px", marginBottom: "14px",
padding: "10px 14px", background: "rgba(30, 41, 59, 0.8)",
borderRadius: "8px", border: "1px solid #475569",
}}>
<span style={{ fontSize: "16px" }}>⚔️</span>
<span style={{ color: "#94a3b8", fontSize: "13px" }}>武器:</span>
<span style={{
color: equippedWeapon ? "#e2e8f0" : "#475569",
fontSize: "13px",
fontWeight: equippedWeapon ? "600" : "normal",
fontStyle: equippedWeapon ? "normal" : "italic",
}}>
{equippedWeapon || "无"}
</span>
</div>
{/* 物品格子 */}
{inventory.length === 0 ? (
<div style={{
padding: "24px", textAlign: "center", color: "#475569", fontSize: "13px",
background: "rgba(30, 41, 59, 0.4)", borderRadius: "8px", border: "1px dashed #334155",
}}>
背包是空的
</div>
) : (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))", gap: "8px" }}>
{inventory.map((item, idx) => {
const name = String(item?.name || item);
const action = itemActions[name];
const isEquipped = equippedWeapon === name;
return (
<div key={idx} style={{
display: "flex", flexDirection: "column", alignItems: "center", gap: "6px",
padding: "12px 8px 8px", background: "rgba(30, 41, 59, 0.8)", borderRadius: "8px",
border: isEquipped ? "1px solid #22d3ee" : "1px solid #475569",
}}>
<span style={{ fontSize: "28px" }}>{String(item?.icon || "📦")}</span>
<span style={{ color: "#e2e8f0", fontSize: "12px", fontWeight: "600", textAlign: "center" }}>{name}</span>
<span style={{ color: "#64748b", fontSize: "11px" }}>x{Number(item?.count ?? 1)}</span>
{action && (
<button
onClick={action.run}
style={{
marginTop: "4px", padding: "4px 14px", width: "100%",
borderRadius: "6px", fontSize: "12px", fontWeight: "600", cursor: "pointer",
background: action.type === "consumable" ? "#047857" : isEquipped ? "#4b5563" : "#1e40af",
border: "1px solid " + (action.type === "consumable" ? "#10b981" : isEquipped ? "#6b7280" : "#3b82f6"),
color: action.type === "consumable" ? "#a7f3d0" : isEquipped ? "#9ca3af" : "#bfdbfe",
}}
>
{isEquipped ? "已装备" : action.label}
</button>
)}
</div>
);
})}
</div>
)}
</div>
)}
</div>
);
}} />
);
}代码做了什么:
<Chat renderBubble={...} />保留平台自带的消息列表、输入框和滚动;你负责画每个气泡,面板放在最后一个下面api.variables.inventory、.hp和.equipped_weapon按名字读变量useItem按名字找到物品,数量减一(减到零就删掉),再用api.setVariable("inventory", next)把整个列表写回去。药水还会让hp涨上去equipItem设好equipped_weapon,再告诉玩家api.injectContext(text)给 AI 塞一句话,只管它的下一次回复,和行为里的 告诉 AI 一样api.showToast(text, type)弹一个提示;类型是"success"、"error"或"info"itemActions决定哪些物品有按钮。每多一种能用的物品就加一行
面板 → 前端代码 里文件旁边的状态应该是绿色的 正常。
不想自己写代码?
点画布右上角的 创作助手,描述一下就行,比如「一个物品格子,带生命条、武器栏,还有使用和装备按钮」。
AI 怎么改这个列表
AI 在回复末尾用指令写列表的变化,格式引擎已经教给它了。往里加东西很靠谱:
你搜了搜哥布林,找到一个小瓶子。[inventory: push {"name":"药水","icon":"🧪","count":1}]AI 也能按位置删([inventory: delete 0] 删掉第一件),但它没法按名字找到「药水」,也没法稳定地改某一件的数量。数量交给按钮去管,在行为规则里告诉 AI 只负责加东西。
速查
| 你想要 | 怎么做 |
|---|---|
| 一个 AI 跟着剧情往里放东西的背包 | 玩家界面 → 背包 模板 |
| 开局就有的物品 | 背包变量的 初始值 |
| 精确的数量(药水、箭) | 每种东西一个数值变量 |
| 用一件东西,先检查够不够 | 一条行为:玩家点了某个按钮,如果…… 数量大于 0,条件不满足时告诉玩家 …… |
| 一个能触发它的按钮 | 按钮步骤 让一条行为生效 |
| 装备一样东西 | 按钮步骤 改变量,改一个文字变量 |
| 做完后藏起按钮 | 什么时候显示 → 变量满足条件时 |
| 马上告诉 AI | 行为里的 告诉 AI,或者代码里的 api.injectContext() |
| 每件物品带按钮和数量的格子 | 界面代码(见上面写代码的做法) |
自己试试:可导入的演示
下载这个 JSON 导入进去,就能看到能跑的版本:
怎么导入:
- 点左边的 创建,选 空白项目,再选 画布
- 在顶栏点 ⋮ → 导入文件,选下载好的
.json文件。它会替换掉这张新卡里的所有内容 - 点 保存,再点 试玩
演示用的是写代码的版本:一个 inventory 列表、hp、equipped_weapon、一条装备行为,物品格子写在界面代码里。
