9.2 KiB
Raw Blame History

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 严格按顺序执行:

  1. 校验玩家拥有 GachaPoolConfig.singleCostItem 数量 >= singleCostCount
  2. 扣除消耗
  3. 按权重表随机抽取 1 个奖励项
  4. 调用 BagComponentSystem.AddItems(rewards) 发放奖励
  5. 更新 GachaComponent.poolStats[poolId](累计 +1、距保底 -1 或重置)
  6. 推送 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 进入该卡池后红点不消失(仍持续提示)