RPG 设计复盘 · 第 1 篇
在线试玩:/rpg · 玩法地图见分类 博客系统 下自文档《RPG 玩法全览》《RPG 技术实现》
写在前面
博客里做 RPG,容易写成「功能清单」:签到、任务、抽奖、排行榜……读者看完只知道有什么,不知道数字怎么定、状态怎么重置、随机怎么不翻车。
自文档 09/10 已经覆盖玩法全景与模块链路。本文只深挖三条数值主干,全部对照 blog-server 现网代码:
- EXP:阈值公式、来源上限、加成链
- 任务引擎:四类任务 + 懒重置(不是 Cron 清零)
- 抽卡:加权随机 + 史诗/传说双保底独立计数
试玩入口:Web /rpg;规则细则仓库内见 src/modules/rpg/RPG-GAMEPLAY.md。
一、EXP:一条二次曲线撑起整站成长感
设计目标
- 前期升级快,让新用户第一天就能摸到 LV2~LV3
- 后期曲线拉开,避免「刷两周满级」
- 防刷只卡易刷行为(点赞/收藏),创作与签到不设日盖帽,避免挫伤内容生产
阈值公式
实现在 LevelService.getLevelThreshold:
typescript
// Lv.N 所需最低累计经验
// LV1=0, LV2=100, LV3=300, LV4=600, LV5=1000 …
return level <= 1 ? 0 : level * (level - 1) * 50;
| 等级 | 累计经验阈值 | 升到下级所需 |
|---|---|---|
| LV1 | 0 | 100 |
| LV2 | 100 | 200 |
| LV5 | 1000 | 500 |
| LV10 | 4500 | 1000 |
| LV15 | 10500 | 1500 |
这是 累计阈值,不是「每级固定包」。升级检测是 while (exp >= threshold(level+1)) level++,一次加经验可以连升多级。
为何用 N(N-1)\times50 而不是指数爆表?博客站 DAU 有限,指数曲线会让中期玩家「永远差一截」;二次增长够用,心智也好算。
经验从哪来
| 来源 | 单次 | 每日上限 |
|---|---|---|
| 签到 | 10 + 连签阶梯 | 无 |
| 评论 / 回复 / 留言 | 5 | 无 |
| 发文 | 20 | 无 |
| 点赞 | 2 | 10 EXP/天 |
| 收藏 | 3 | 15 EXP/天 |
| 任务 / 成就 / 抽奖 | 配置值 | 无(抽奖奖品另计) |
上限键:rpg:exp:{uid}:{reason}:{YYYY-MM-DD},只在 addExp(..., dailyLimit) 传入时生效(见 RPG-TECH.md §4.5)。
加成链(乘法)
addExp 顺序大致是:
- Buff
exp_boost(需激活) - 活动
expBuffRate - 出战宠物被动
- 天气临时加成
- 日上限截断 → 写
rpg.exp→ 升级 → WSlevelUp/expGain
升级奖励不在 LevelService 里硬编码发装扮,而是走 trackEvent: level_up 成就,展示名一律来自 rpg_item_config——避免前后端各维护一份「LV5 叫什么称号」。
二、任务引擎:懒重置比定时任务更省心
四类任务,不是「每日/每周/成就」三分类
成就是独立子系统(UserAchievement)。任务侧按 questSubtype / type 分成:
| 类型 | 进度日期键 | 含义 |
|---|---|---|
| daily | 当天零点 | 每日任务 |
| bounty | 当天零点 | 悬赏(当日可重复追踪) |
| weekly | 当周周一零点 | 周常 |
| special | 固定 2000-01-01 |
一次性(打赏/孵宠/入会等) |
预定义种子在 quest.service.ts 的 PREDEFINED_QUESTS,启动 upsert 进 rpg_quest。
为什么不用 Cron「每天 0 点清空进度」?
清空表有两个麻烦:多实例要分布式锁、用户跨时区/宕机窗口会脏。
我们用的是 懒重置:
- 进度行带
questDate - 查今日任务时,只认「今天的键」;没有行 → 进度视为 0
- 昨天的行自然过期,无需 DELETE
typescript
private getQuestDateKey(quest: Quest): Date {
if (quest.type === 'special' || quest.questSubtype === 'special') {
return SPECIAL_QUEST_DATE; // 2000-01-01
}
if (quest.type === 'weekly' || quest.questSubtype === 'weekly') {
return this.getWeekQuestDate(); // 周一零点
}
return this.getTodayQuestDate();
}
进度从哪来
各领域行为(签到、评论、抽奖……)调用 QuestService.trackProgress(uid, action, increment?)。
连抽时 increment = count,一次推进 lottery_draw 类任务,避免循环 10 次打点。
完成时推 WS「任务完成」;奖励不自动发,需 POST /rpg/quest/claim:
- 校验
completed && !claimed - 先标
claimed = true再发经验/钻石/券/HP - 已领取再点 → 业务错误,充当幂等
全勤:当日全部 daily 领完,额外 +2 抽奖券(配置在领取逻辑里)。
和事件总线的关系
评论/发文等不一定同步打 RPG:很多路径进 Redis Stream blog:events,由 rpg-event.consumer 再加经验、推成就/任务。任务引擎本身不管 MQ,只认 trackProgress——边界干净,自文档 11 有 Stream vs WS 分工说明。
三、抽卡:加权随机 + 两条独立保底
产品约束
- 1~10 连抽;货币
ticket(1 张/次)或currency(钻石,ECONOMY.LOTTERY_CURRENCY_COST = 10/次) - 奖池在
rpg_lottery_pool,展示字段 JOINrpg_item_config(C 端不硬编码稀有度色) - 概率要对玩家可解释,又不能「非洲脸永久无史诗」
加权随机
typescript
private weightedRandom(items: EnrichedLotteryPool[]) {
const totalWeight = items.reduce((sum, i) => sum + i.probability, 0);
let random = Math.random() * totalWeight;
for (const item of items) {
random -= item.probability;
if (random <= 0) return item;
}
return items[items.length - 1];
}
稀有度区间(种子奖池量级,运营可改 DB):
| 稀有度 | 大约占比 | 例子 |
|---|---|---|
| 普通 | ~60% | 小额经验、券碎片 |
| 稀有 | ~25% | 中额经验、微增卷轴 |
| 史诗 | ~12% | 大额经验、头像框、宠物蛋 |
| 传说 | ~3% | 经验宝珠、券箱 |
双保底(独立计数)
存在 rpg 表两列:
| 字段 | 阈值 | 行为 |
|---|---|---|
lotteryPityCounter |
90 | 强制从 史诗 池抽一件并清零 |
lotteryLegendaryPityCounter |
180 | 强制从 传说 池抽一件并清零 |
每次抽奖两计数都 +1;判定顺序 传说保底优先于史诗保底。自然出货时:
- 出传说 → 只清传说计数
- 出史诗 → 只清史诗计数
也就是说:刚出一件史诗,传说计数继续走——避免「出史诗把传说进度一起抹掉」的虐非洲设计。常量见 ECONOMY.LOTTERY_EPIC_PITY_THRESHOLD / LOTTERY_LEGENDARY_PITY_THRESHOLD。
连抽结束后只回写保底字段,避免覆盖同请求里 grantReward 已改过的 exp/券。
踩坑与取舍
-
配置三处漂移
数值可能在constants、种子数组、DBrpg_item_config/rpg_lottery_pool同时存在。改概率或奖品名,要对着 TECH 文档三处核对,否则出现「后台改了、C 端还显示旧名」。 -
保底文档也曾写岔
早期笔记写过「出传说也清史诗计数」。以lottery.service.ts的draw为准:两档独立。写外发文、写注释都别靠记忆。 -
抽奖中途改 RPG 行要小心
连抽里会grantReward(加经验、加券)。若最后saveRpg用抽奖开始时的脏对象整行覆盖,会吃掉中间写入。现实现是结束时重新getOrCreateRpg再只改 pity 字段。 -
任务懒重置会留下历史进度行
表会变大,但省掉了定时任务与锁。若以后要归档,按questDate < 某日冷删即可,不影响在线逻辑。 -
前端进度条公式必须与后端一致
Home 的getLevelThreshold与 Nest 同构; equestion 一处改公式、一处忘改,会出现「显示 100% 却没升级」。
小结
| 子系统 | 一句话设计 |
|---|---|
| EXP | 累计二次阈值 + 易刷行为 Redis 日盖帽 + 乘法加成链 |
| 任务 | 日期键懒重置 + 行为 trackProgress + 手动 claim 幂等 |
| 抽卡 | 权重随机 + 史诗 90 / 传说 180 独立保底 |
整站玩法地图、充值与 WS 推送,请读自文档系列《RPG 玩法全览》《RPG 技术实现》;事件总线见《事件驱动与模块解耦》。下一例外发深挖计划写 RAG:分块、混合检索与 Tool 编排(自文档里几乎还是空白)。
关键路径
blog-server/src/modules/rpg/level/level.service.tsblog-server/src/modules/rpg/quest/quest.service.tsblog-server/src/modules/rpg/lottery/lottery.service.tsblog-server/src/modules/rpg/constants/economy.tsblog-server/src/modules/rpg/RPG-GAMEPLAY.md/RPG-TECH.md

全部评论(0)