玄学放置 · 任务掉落系统
起因
项目走到现在,任务完成后的反馈只有经验值增长——经验条往右挪一格,仅此而已。放置游戏的核心循环是"等待-收获-再投入",如果"收获"这一环只有经验,玩家的满足感会很薄。经验是长期积累的隐性奖励,但物品掉落是即时可见的显性奖励——每次任务完成都可能蹦出一个没见过的新材料,这种不确定性的刺激远比经验条填满更抓人。
同时,每个学科目前只有任务卡片的视觉差异(颜色、图标),玩法上没有独特性。掉落物品是让学科产生差异化的天然载体——物理掉电阻元件、历史掉兵法竹简、玄学掉灵感结晶——玩家为了收集某个学科的特色材料,自然会多刷那个学科的任务。
还有一个现实约束:后续会接 Node 服务端,掉落概率的计算逻辑需要能在前后端共用。这意味着概率模型不能依赖前端特有的 API,数据结构要对 JSON 序列化友好。
万分比概率:为服务端预留的整数运算
掉落概率最常见的两种表示方式:
- 百分比:
0.3表示 30%,浮点数,服务端做概率修正时有精度问题 - 千分比:
300表示 30%,整数,但精度只到 0.1%,做 0.05% 这种低概率不够
我选了万分比:rate 取值 0-10000,3000 就是 30%,100 是 1%,10 是 0.1%。
export interface DropEntry {
itemId: string // 'gold' 代表金币,其余为 ITEMS_DB 中的 key
rate: number // 万分比概率
countMin: number // 掉落数量下限
countMax: number // 掉落数量上限
}万分比的好处不只是精度:服务端做 buff/debuff 修正时,rate + 500 比 rate * 1.05 简单得多,且不引入浮点误差。前端显示时 rate / 100 直接得到百分比数字,也不复杂。
countMin / countMax 提供数量区间,让高难度任务掉更多金币和材料成为一种自然的设计手段——简单任务掉 1-2 金币,困难任务掉 3-7 金币,差距一眼可见。
掉落执行:一轮完成一次判定
掉落挂在 tickProgress 里,每当一轮任务完成就执行一次判定:
// 本轮完成 → 发放经验 + 执行掉落
entry.completedCount += 1
subject.completedCount += 1
this._grantExp(subject, task.expReward)
this._processDrops(task.drops)_processDrops 的逻辑很直:遍历 drops 数组,每个条目独立判定。Math.random() * 10000 < rate 就命中,命中后随机 countMin 到 countMax 之间的数量。
金币和物品走不同的入账路径——金币加到 inventoryStore.gold(独立字段),物品推入背包。这样金币和物品在存储层就是分离的,后续服务端做排行榜、交易、商店时,金币的查询和修改不需要遍历背包数组。
if (drop.itemId === 'gold') {
inventory.addGold(count)
} else {
inventory.addItem(drop.itemId, count)
}每个条目独立判定意味着一次任务完成可能同时掉落金币+材料+稀有材料,也可能什么都不掉。这种"可能全空也可能全中"的体验,比"每次必掉一样"更有抽卡的刺激感。
学科特色掉落:让每个学科有自己的材料
9 个学科,每个 2-3 种 material 类物品,共 19 种。设计原则:
- 基础材料(物品 1):rate 3000-5000(30%-50%),较常见,短期刷几次就能攒到
- 进阶材料(物品 2):rate 1500-3000(15%-30%),需要多刷
- 稀有材料(物品 3,仅部分学科有):rate 500-1000(5%-10%),长期目标
物品的命名和描述都贴合学科主题,同时带一点学生时代的吐槽感——这是之前装备系统就确立的文案风格,掉落材料延续它:
- 电阻元件(物理):色环标注的碳膜电阻,阻值一目了然
- 古地图残片(历史):羊皮纸地图的残片,依稀可见古代疆域轮廓
- 灵感结晶(玄学):冥想中凝聚的灵感结晶,散发着紫金色光芒
概率和数量的梯度设计让"高难度任务更值得刷"变成自然结论:比如物理的"冥想量子叠加"掉 3-7 金币 + 8% 光栅片,而"受力分析"只掉 1-3 金币 + 无稀有掉落。玩家用脚投票就会选高难度任务——但如果战力不够打不过高难度,低难度任务也能稳定产出基础材料,不会让新手卡住。
金币独立:从背包物品到 store 字段
之前金币是背包里的一种 currency 类物品(scholarship),存在 bag 数组中。这次把金币独立为 inventoryStore.gold 字段,同时从初始背包和物品 tab 的分类筛选中移除了货币分类。
独立的原因有三:
- 掉落系统需要高频写入金币(每次任务完成可能加),bag 数组的查找和修改比直接字段赋值慢
- 金币的语义和物品不同——物品可装备、可堆叠、有品质,金币只是一个数字
- 后续商店、交易等功能都需要"花金币"这个操作,独立字段让这些 action 更直接
scholarship 的像素图标和 ITEMS_DB 定义保留着,因为金币的 UI 展示仍然用这个图标——金币栏和任务掉落预览里的金币图标都是 PixelIcon name="scholarship",而不是 emoji。保持像素风格的统一性。
任务气泡框的掉落预览
SubjectPanel 的任务气泡框新增了"可能掉落"区块,插在经验信息和次数选择之间。每条掉落显示四列:
- 图标(金币用 scholarship 像素图标,物品用各自的像素图标)
- 名称
- 数量范围(1-3 这种区间,或固定数字)
- 概率(百分比数字,颜色分三档)
概率的颜色分级是关键细节:
- 高概率(>=50%):绿色,一眼就知道"大概率能拿到"
- 中概率(20%-50%):蓝色,"有一定机会"
- 低概率(<20%):紫色,"稀有,但不是没可能"
颜色信号让玩家扫一眼就能判断"这个任务值不值得刷",不需要心算概率。这比单纯列数字更有效——人对颜色的感知速度比数字快一个量级。
收获
这次改动最核心的收获是:掉落系统让任务从"等进度条走完"变成了"等进度条走完看掉什么"。多了一层不确定性,每次完成都有悬念。
从架构角度,万分比 + 独立金币字段 + DropEntry 统一结构的组合,让后续服务端迁移的成本很低——服务端只需要实现同样的 randomInt 和 rate 判定逻辑,数据结构直接 JSON 序列化就能前后端共用。不需要在 API 层做概率格式的转换。
19 个学科特色物品和对应的像素图标也让项目的视觉资产丰富了不少。每个物品的 16x16 像素画虽然小,但足够辨识——玩家扫一眼背包就能认出"这是物理的电阻"和"这是历史的竹简",学科差异在材料层面就有了视觉锚点。
下一篇大概率围绕市场面板展开——有了掉落物品和金币,商店的买入卖出就有了数据基础。