9.2 KiB
ADDED Requirements
Requirement: 卡池配置
系统 MUST 提供卡池配置 GachaPoolConfig,每个卡池含:id、name、单抽消耗(itemId + count)、十连消耗(itemId + count,通常有折扣)、保底次数(每 N 抽必出某品质)、保底品质等级。所有数值由 Excel 配置,运行时通过 GachaPoolConfigCategory.Instance.Get(poolId) 获取。
Scenario: 加载卡池配置
- WHEN 服务端启动加载 ExcelExporter 生成的配置
- THEN
GachaPoolConfigCategory包含所有配置的卡池 - AND 每个卡池可查询单抽/十连消耗及保底参数
Scenario: 卡池关闭
- WHEN
GachaPoolConfig.openTime > 现在时间或closeTime < 现在时间 - THEN 服务端拒绝该卡池的抽卡请求,返回
ERR_GACHA_POOL_CLOSED
Requirement: 卡池权重表
系统 MUST 为每个卡池配置权重表 GachaWeightConfig,每条记录含:poolId、entryIndex、itemId(奖励道具)、count(数量)、weight(权重)、rare(品质 1-5)。抽卡随机算法 MUST 按权重总和归一化抽取。
Scenario: 权重归一化抽取
- WHEN 卡池 A 有 3 条记录权重分别为 70 / 25 / 5(总和 100)
- AND 服务端生成随机数 r ∈ [0, 100)
- THEN r < 70 → 选第 1 条;70 ≤ r < 95 → 选第 2 条;95 ≤ r < 100 → 选第 3 条
Scenario: 权重表为空
- WHEN 卡池配置中无任何 weight 记录
- THEN 服务端启动时 MUST 报错并拒绝该卡池上线
Requirement: 抽卡组件数据模型
服务端 MUST 为每个玩家维护 GachaComponent,记录:每个卡池的累计抽数(用于保底统计)、当前距离保底的抽数、最后一次抽卡时间。GachaComponent MUST 持久化到 DB。
Scenario: GachaComponent 初始化
- WHEN 新玩家首次登录
- THEN 创建空的
GachaComponent,所有 poolStats 为初始值
Scenario: GachaComponent 同步
- WHEN 玩家登录后
- THEN 服务端 MUST 推送
Gacha_GachaInfo含所有 poolStats - AND 客户端缓存到
GachaComponent
Requirement: 单抽
玩家 MUST 能对指定卡池发起单抽。服务端 MUST 严格按顺序执行:
- 校验玩家拥有
GachaPoolConfig.singleCostItem数量>= singleCostCount - 扣除消耗
- 按权重表随机抽取 1 个奖励项
- 调用
BagComponentSystem.AddItems(rewards)发放奖励 - 更新
GachaComponent.poolStats[poolId](累计 +1、距保底 -1 或重置) - 推送
R2C_GachaDraw { rewards }+Item_ItemInfo*+Gacha_GachaInfo
Scenario: 单抽成功
- WHEN 玩家拥有 1 个钻石(卡池消耗 1 钻石)
- AND 发送
C2G_GachaDraw { poolId, drawType: Single } - THEN 服务端扣 1 钻石、抽取 1 个奖励、发放到背包
- AND 返回
R2C_GachaDraw { error: 0, rewards: [(itemId, count, isNewHero)] } - AND 推送
Item_ItemInfo(钻石变化)+ 奖励道具
Scenario: 消耗不足
- WHEN 玩家消耗道具不足
- THEN 服务端拒绝并返回
ERR_GACHA_COST_NOT_ENOUGH - AND 不扣消耗、不抽奖、不更新状态
Requirement: 十连抽
玩家 MUST 能对指定卡池发起十连抽。服务端 MUST 一次性独立抽取 10 个奖励(每次独立抽奖,互不影响),并应用十连专属消耗(通常优惠 10%-20%)。
Scenario: 十连成功
- WHEN 玩家拥有 10 个钻石(十连消耗 9 钻石优惠)
- AND 发送
C2G_GachaDraw { poolId, drawType: Multi10 } - THEN 服务端扣 9 钻石
- AND 独立抽取 10 个奖励(每次独立随机,可能重复也可能不重复)
- AND 累计抽数 +10,保底计数 -10(或触发保底)
- AND 返回 10 个奖励的列表
Scenario: 十连必出保底品质
- WHEN 卡池配置
multiGuaranteeRare = 4(十连必出至少 1 个 4 星) - AND 玩家发起十连
- AND 自然抽取的 10 个结果中无 rare ≥ 4
- THEN 服务端 MUST 把最低品质的 1 个替换为该卡池权重表中任意 rare ≥ 4 的奖励
- AND 替换在响应给客户端之前完成
Requirement: 保底机制
每个卡池 MUST 支持累计保底:玩家在该卡池累计抽到 GachaPoolConfig.guaranteeCount 次(如 90 次)仍未出 GachaPoolConfig.guaranteeRare(如 5 星)品质,第 N 次 MUST 强制出该品质。一旦出 ≥ 该品质,保底计数器重置。
Scenario: 触发保底
- WHEN 玩家在卡池 A 已抽 89 次未出 5 星
- AND 玩家发起第 90 抽
- AND 自然抽取结果 rare < 5
- THEN 服务端 MUST 强制把奖励替换为权重表中任意 rare ≥ 5 的项
- AND 重置该玩家在卡池 A 的距离保底计数为 0
Scenario: 自然出货重置保底
- WHEN 玩家在卡池 A 累计抽 30 次
- AND 第 31 抽自然抽到 rare = 5 的奖励
- THEN 服务端 MUST 重置该玩家在卡池 A 的"距离保底"计数为 0
- AND 累计抽数继续累加(用于历史统计)
Scenario: 不同卡池独立保底
- WHEN 玩家在卡池 A 累计 50 次、卡池 B 累计 80 次
- THEN 两个卡池的保底进度独立计算
- AND 抽卡池 A 不影响卡池 B 的保底进度
Requirement: 随机算法
服务端随机算法 MUST 使用密码学安全的随机源或种子可记录的伪随机(如 System.Random 配 GUID seed),且 MUST 在服务端独立完成,不接受客户端任何随机性输入。
Scenario: 服务端单边随机
- WHEN 客户端发送
C2G_GachaDraw { poolId, drawType } - THEN 服务端使用自身随机数生成器抽奖
- AND 客户端 payload 中任何 "randSeed" / "luck" 等字段 MUST 被忽略
Scenario: 抽卡日志可审计
- WHEN 任何一次抽卡完成
- THEN 服务端 MUST 记录日志:玩家 Id、卡池 Id、抽取时间、消耗、奖励列表、抽数序号、保底计数
- AND 日志格式便于后续 SQL 分析(防欺诈、概率审计)
Requirement: 抽卡奖励发放
抽卡奖励 MUST 通过统一接口 BagComponentSystem.AddItems(player, rewards) 发放。抽卡系统自身 MUST NOT 直接操作背包。
Scenario: 抽到道具
- WHEN 抽卡结果为 itemId=2001(升星石)count=10
- THEN 服务端调用
BagComponentSystem.AddItem(player, 2001, 10) - AND
Item_ItemInfo自动推送给客户端
Scenario: 抽到英雄碎片
- WHEN 抽卡结果为 itemId=10001(某英雄碎片)count=1
- AND 该英雄碎片足够招募(满足 HeroConfig.getItemNum)
- THEN 服务端发放碎片到背包
- AND 响应中
isCanSummon = true标记,客户端展示"碎片满,可招募"动效
Scenario: 抽到完整英雄(首次)
- WHEN 抽卡奖励是 itemId 对应"直接发英雄"(非碎片)
- AND 玩家尚未拥有该英雄
- THEN 服务端直接创建 Hero Entity(等价 SummonHero 流程)
- AND 响应中
isNewHero = true标记
Scenario: 抽到完整英雄(已拥有)
- WHEN 抽卡奖励是"直接发英雄"但玩家已拥有
- THEN 服务端 MUST 按
ItemConfig.exchangeWhenOwned配置转化为碎片或其他奖励 - AND 响应中标记为"转换奖励"
Requirement: 客户端抽卡 UI
客户端 MUST 提供 GachaPanel,包含:
- 卡池切换栏(横向列表,显示所有 open 状态卡池)
- 当前卡池信息(名称、关闭时间倒计时、单抽/十连消耗)
- 保底进度条(显示"距离保底还需 N 抽")
- 单抽 / 十连按钮(点击发送
C2G_GachaDraw) - 历史奖励预览(最近 N 抽的简略列表)
Scenario: 切换卡池
- WHEN 玩家点击卡池切换按钮
- THEN UI 刷新显示新卡池的所有信息
- AND 保底进度条按新卡池数据更新
Scenario: 抽卡过程动画
- WHEN 玩家点击十连按钮且消耗足够
- THEN UI 立刻禁用按钮防重复点击
- AND 发送
C2G_GachaDraw - AND 收到响应后播放抽卡动画(约 2-3 秒)
- AND 动画完毕弹出
GachaResultPanel展示所有奖励
Requirement: 抽卡结果展示
GachaResultPanel MUST 一次性展示一次抽卡的所有奖励(单抽 1 个、十连 10 个),按品质升序排列展示(高品质放最后突出显示)。新英雄 MUST 有特殊"亮相"动画。
Scenario: 十连结果展示
- WHEN 收到十连响应含 10 个奖励
- THEN UI 按 rare 升序排列展示(低品在前、高品在后)
- AND 含新英雄的项额外播放
NewHeroBanner动画 - AND 玩家点击"再抽十连"按钮 → 直接发起下一轮(不关闭面板)
Scenario: 跳过动画
- WHEN 玩家点击屏幕任意位置
- THEN 直接快进到结果展示完毕状态
- AND 跳过动画不影响奖励发放(奖励已在
R2C_GachaDraw响应到达时入背包)
Requirement: 抽卡红点
系统 MUST 提供以下红点:
- 卡池新开放(首次开启或恢复开启时显示红点,进 GachaPanel 后清除)
- 免费单抽可用(部分卡池每 N 小时提供 1 次免费)
- 接近保底(剩余抽数 ≤ 5 时显示红点提示)
Scenario: 接近保底红点
- WHEN 玩家在卡池 A 累计 85 次未出保底
- AND 卡池 A 保底阈值 90 次
- THEN
GachaPanel上卡池 A 标签显示红点 - AND 进入该卡池后红点不消失(仍持续提示)