feat(lucky-dice): 提交 demo 前核心流程

This commit is contained in:
JSD\13999
2026-06-23 14:13:08 +08:00
commit 855659bf78
140 changed files with 9267 additions and 0 deletions

View File

@@ -0,0 +1,765 @@
# Lucky Dice 核心流程规格
## 文档信息
- 来源需求:[lucky-dice-core-flow-requirements.md](../Requirements/lucky-dice-core-flow-requirements.md)
- 适用范围FishDice 第一版普通骰子主循环与 Lucky Dice 特殊分支
- 文档目标:把玩法需求拆成可实现、可测试、可扩展的系统规格
## 设计目标
Lucky Dice 核心流程需要支持两层玩法:
1. 普通骰子主循环:玩家按骰子集合配置 Roll 普通骰子,根据组合事实匹配规则并执行奖励或特殊触发行为。
2. Lucky Dice 特殊流程:当普通骰子出现双四叶草或配置声明的触发条件时,进入独立的特殊结果池,经过筛选、权重随机和模式解析后启动目标玩法。
第一版只做核心闭环,不实现复杂动画、远端热更、完整短玩法内容和复杂运营权重策略。系统边界必须保留扩展点,避免后续新增组合或特殊结果时不断堆硬编码分支。
第一版默认值:
- 普通骰子数量默认 2 颗,但必须来自 DiceSet 配置。
- Lucky Dice 结果槽数量默认 3 格,但必须来自 Lucky Dice 结果或目标玩法配置。
- 规则系统、组合事实和展示流程不得把 2 颗骰子或 3 格结果槽写死为不可扩展假设。
## 总体架构
核心模块按职责拆分为以下几层:
```text
Roll 输入层
→ 骰子结果生成层
→ 组合事实提取层
→ 组合规则匹配层
→ 行为执行层
→ Lucky Dice 流程层
→ 目标玩法模式解析层
→ 目标玩法启动层
```
各层只依赖前一层产出的结构化数据,不直接读取 UI 状态或动画状态。
### 模块职责
| 模块 | 职责 | 不负责 |
| --- | --- | --- |
| DiceSetConfigProvider | 提供骰子集合、骰子数量、结果槽数量等配置 | 执行 Roll 或业务行为 |
| DiceRollService | 按骰子集合配置生成普通骰子或 Lucky Dice 结果 | 决定奖励、玩法入口 |
| ComboFactBuilder | 把普通 Roll 结果转换为结构化组合事实 | 执行业务行为 |
| ComboRuleMatcher | 按优先级匹配组合规则 | 直接发奖或进玩法 |
| RollActionDispatcher | 按行为类型分发到通用执行器 | 按组合 key 维护巨大方法 map |
| LuckyDiceFlowService | 构建候选池、筛选、随机、生成可展示结果槽 | 直接决定目标模式细节 |
| TargetModeResolver | 把 ResultKey/TargetKey 解析为 ModeKey | 执行目标玩法内容 |
| TargetModeLauncher | 统一启动目标玩法 | 参与 Lucky Dice 随机 |
| RollTraceLogger | 记录一次 Roll 的关键决策链路 | 影响玩法决策 |
## 核心流程
### 普通 Roll 流程
```text
PlayerRollRequest
→ DiceSetConfigProvider.GetNormalDiceSet(request)
→ DiceRollService.Roll(NormalDiceSet)
→ ComboFactBuilder.Build(result)
→ ComboRuleMatcher.Match(facts, ruleSet)
→ RollActionDispatcher.Execute(matchedRule.Actions)
→ grant_reward发放普通奖励并结束
→ grant_reward + add_multiplier发奖并应用 Clover bonus
→ trigger_lucky_dice进入 LuckyDiceFlowService
```
普通 Roll 第一版默认两颗骰子,骰子面为:
```text
2 / 3 / 4 / 5 / 6 / Clover
```
骰子数量必须从 `DiceSetConfig.DiceCount` 读取。第一版配置值可以是 2`DiceRollService``ComboFactBuilder` 和 matcher 都必须按 `Faces.Count``DiceCount` 处理,不读取固定的左骰、右骰两个位置。
### Lucky Dice 流程
```text
TriggerLuckyDiceAction
→ LuckyDiceFlowService.BuildCandidatePool(context)
→ LuckyDiceFlowService.ApplyFilters(pool, context)
→ LuckyDiceFlowService.ResolveEmptyPoolFallback(filteredPool, context)
→ LuckyDiceFlowService.WeightedPick(filteredPool, context)
→ LuckyDiceFlowService.BuildResultSlots(result, slotConfig)
→ LuckyDicePresentation.ShowResultSlots(resultSlots)
→ TargetModeResolver.Resolve(result, context)
→ TargetModeLauncher.Launch(targetEntry)
→ TargetModeCompletion 回到主循环
```
第一版 Lucky Dice 默认展示 3 格相同特殊图标,不要求实现托盘、飞入、粒子爆发、白烟转场等表现。结果槽数量必须从 Lucky Dice 结果或目标玩法配置读取,后续可扩展为 2 格、4 格或按玩法定义不同槽数。
## 数据模型
### DiceSetConfig
骰子集合配置定义一次 Roll 使用哪套骰子、投几颗骰子,以及该集合是否拥有展示槽概念。
```csharp
public sealed class DiceSetConfig
{
public string DiceSetId { get; init; }
public DiceSetType SetType { get; init; }
public int DiceCount { get; init; }
public IReadOnlyList<DiceFaceDefinition> Faces { get; init; }
public int? DefaultResultSlotCount { get; init; }
}
```
约束:
- `DiceCount` 必须大于 0。
- 第一版普通骰子 `DiceCount = 2`,但核心逻辑不得依赖该常量。
- Lucky Dice 如果不真实 Roll 多颗特殊骰,可以将 `DiceCount` 理解为结果选择次数;第一版只随机一次,再由展示槽配置生成多个相同槽。
- `DefaultResultSlotCount` 用于展示层默认槽数,第一版 Lucky Dice 为 3。
### DiceFace
普通骰子面与 Lucky Dice 特殊结果必须区分集合类型,不能混用同一个裸字符串列表。
```csharp
public enum DiceSetType
{
Normal,
Lucky
}
public readonly struct DiceFace
{
public DiceSetType SetType { get; }
public string Key { get; }
public int? NumberValue { get; }
}
```
约束:
- 普通数字面使用 `Key = "2" ... "6"``NumberValue` 为对应数字。
- 普通四叶草使用 `Key = "clover"``NumberValue = null`
- Lucky Dice 特殊结果使用 `DiceSetType.Lucky`,例如 `rocket``thief`
- 业务代码不得到处裸写这些 key必须集中在定义表或常量入口。
### DiceRollResult
```csharp
public sealed class DiceRollResult
{
public string RollSessionId { get; init; }
public string DiceSetId { get; init; }
public DiceSetType SetType { get; init; }
public int DiceCount { get; init; }
public IReadOnlyList<DiceFace> Faces { get; init; }
public int Multiplier { get; init; }
public string TriggerSource { get; init; }
}
```
约束:
- `DiceCount` 必须等于本次配置要求的投掷数量。
- `Faces.Count` 必须等于本次实际产出的骰子面数量。
- 普通 Roll 中 `DiceCount``Faces.Count` 不一致时,应拒绝进入规则匹配并记录错误。
### ComboFacts
`ComboFacts` 是普通组合匹配的唯一输入。
```csharp
public sealed class ComboFacts
{
public DiceSetType DiceSetType { get; init; }
public int DiceCount { get; init; }
public IReadOnlyList<DiceFace> Faces { get; init; }
public IReadOnlyDictionary<string, int> FaceCounts { get; init; }
public int NumberSum { get; init; }
public IReadOnlyList<int> NumberValues { get; init; }
public int CloverCount { get; init; }
public bool HasClover { get; init; }
public string NormalizedKey { get; init; }
public string OrderedKey { get; init; }
}
```
归一化规则:
- 默认按无序组合处理。
- `2 + clover``clover + 2` 都归一为 `2_clover`
- `clover + clover` 归一为 `clover_clover`
- N 颗骰子的 `NormalizedKey` 由所有面 key 排序后拼接,例如 `2_2_clover``3_3_3`
- 后续若需要有序语义,由规则显式声明使用 `OrderedKey`
约束:
- `ComboFacts` 不能假设只有 2 颗骰子。
- matcher 必须基于 `DiceCount``Faces``FaceCounts``NumberValues` 等统计信息判断。
- 只有有序规则才能读取 `OrderedKey`,普通规则默认使用无序事实。
### ComboRule
```csharp
public sealed class ComboRule
{
public string RuleId { get; init; }
public int Priority { get; init; }
public ComboMatcherSpec Matcher { get; init; }
public IReadOnlyList<RollActionSpec> Actions { get; init; }
public bool StopAfterMatched { get; init; } = true;
}
```
第一版规则:
| Priority | Matcher | Actions |
| --- | --- | --- |
| 100 | ExactCombo `clover_clover` | `trigger_lucky_dice` |
| 50 | ContainsFace `clover` count 1 | `grant_reward`, `add_multiplier` |
| 10 | AllNumbers | `grant_reward` |
匹配策略:
-`Priority` 从高到低匹配。
- 同优先级按配置顺序匹配。
- 第一版默认命中第一条 `StopAfterMatched = true` 的规则后停止。
- 未命中任何规则时走可观测兜底,默认发普通奖励或返回明确错误,由产品配置决定。
### ComboMatcherSpec
```csharp
public sealed class ComboMatcherSpec
{
public string MatcherType { get; init; }
public string ComboKey { get; init; }
public string FaceKey { get; init; }
public int? RequiredCount { get; init; }
public int? MinCount { get; init; }
public int? MaxCount { get; init; }
public int? DiceCount { get; init; }
public int? OfAKindCount { get; init; }
public int? MinSum { get; init; }
public int? MaxSum { get; init; }
public bool UseOrderedKey { get; init; }
}
```
首批 matcher
- `ExactCombo`
- `AllNumbers`
- `ContainsFace`
- `FaceCount`
- `FaceCountRange`
- `DiceCount`
- `NumberPair`
- `NumberOfAKind`
- `NumberSumRange`
新增组合语义优先新增规则配置;只有出现新的通用匹配能力时才新增 matcher。
N 颗骰子扩展示例:
| Matcher | 例子 | 含义 |
| --- | --- | --- |
| `FaceCountRange` | `faceKey = clover`, `minCount = 2` | 至少 2 个 Clover |
| `DiceCount` | `diceCount = 3` | 只匹配 3 骰规则 |
| `NumberOfAKind` | `ofAKindCount = 3` | 任意数字三连 |
| `NumberSumRange` | `minSum = 18`, `maxSum = 24` | 点数和落在 18 到 24 |
### RollActionSpec
```csharp
public sealed class RollActionSpec
{
public string ActionType { get; init; }
public IReadOnlyDictionary<string, string> Params { get; init; }
}
```
首批 action
- `grant_reward`
- `add_multiplier`
- `trigger_lucky_dice`
- `enter_mode`
- `show_popup`
执行层只按 `ActionType` 找执行器,不按组合 key 找方法。
### LuckyDiceCandidate
```csharp
public sealed class LuckyDiceCandidate
{
public string ResultKey { get; init; }
public string TargetKey { get; init; }
public int? ResultSlotCount { get; init; }
public int Weight { get; init; }
public bool Enabled { get; init; }
public int MinLevel { get; init; }
public IReadOnlySet<string> SourceFilter { get; init; }
public CooldownRule CooldownRule { get; init; }
public bool IsDefault { get; init; }
}
```
第一版候选配置:
| ResultKey | TargetKey | ResultSlotCount | Weight | Enabled | Mode 默认结果 |
| --- | --- | --- | --- | --- | --- |
| `rocket` | `slap_down` | 3 | 1 | true | `slap_down_normal` |
| `thief` | `treasure_heist` | 3 | 1 | true | `treasure_heist_normal` |
`ResultSlotCount` 允许为空。为空时使用 Lucky Dice 集合配置或目标玩法配置中的默认槽数。
### LuckyDiceResultSlots
Lucky Dice 随机只选中一个特殊结果;展示层按槽配置生成实际槽位。
```csharp
public sealed class LuckyDiceResultSlots
{
public string ResultKey { get; init; }
public string TargetKey { get; init; }
public int SlotCount { get; init; }
public IReadOnlyList<string> SlotResultKeys { get; init; }
}
```
第一版要求:
- `SlotCount` 默认 3。
- `SlotResultKeys` 第一版全部填入同一个 `ResultKey`
- 后续如某玩法需要不同槽位结果,可通过配置扩展 `SlotResultKeys` 的生成策略。
### LuckyDiceContext
```csharp
public sealed class LuckyDiceContext
{
public string RollSessionId { get; init; }
public string TriggerSource { get; init; }
public int NormalDiceCount { get; init; }
public int LuckyResultSlotCount { get; init; }
public int PlayerLevel { get; init; }
public int Multiplier { get; init; }
public bool IsTutorial { get; init; }
public IReadOnlyDictionary<string, object> RuntimeTags { get; init; }
}
```
### TargetModeEntry
```csharp
public sealed class TargetModeEntry
{
public string TargetKey { get; init; }
public string ModeKey { get; init; }
public string ResultKey { get; init; }
public int Multiplier { get; init; }
public string TriggerSource { get; init; }
public string RollSessionId { get; init; }
}
```
## 接口边界
### 组合事实构建
```csharp
public interface IComboFactBuilder
{
ComboFacts Build(DiceRollResult result);
}
```
要求:
- 只接受普通骰子结果构建普通组合事实。
- 如果传入 Lucky Dice 结果,应返回错误或拒绝处理,避免两套骰子语义混用。
- 输出必须保留 DiceCount、原始 Faces、FaceCounts、NumberValues、NormalizedKey、OrderedKey。
- 不允许只读取固定两个骰子位置;必须遍历 `Faces`
### 组合规则匹配
```csharp
public interface IComboRuleMatcher
{
ComboRuleMatchResult Match(ComboFacts facts, IReadOnlyList<ComboRule> rules);
}
```
要求:
- matcher 由 `MatcherType` 注册,不由组合 key 注册。
- 匹配结果需要包含命中的 `RuleId``Priority``MatcherType` 和关键输入 facts供日志追踪。
### 行为执行
```csharp
public interface IRollActionExecutor
{
string ActionType { get; }
RollActionResult Execute(RollActionContext context, RollActionSpec action);
}
```
要求:
- `RollActionDispatcher` 只维护 `ActionType -> IRollActionExecutor` 注册表。
- 不允许维护 `clover_clover -> SomeMethod` 这类随组合数量增长的 map。
- 多个 action 按规则配置顺序执行。
- action 失败时需要返回结构化失败结果,并进入统一日志。
### Lucky Dice 筛选
```csharp
public interface ILuckyDiceCandidateFilter
{
string FilterType { get; }
IReadOnlyList<LuckyDiceCandidate> Apply(
IReadOnlyList<LuckyDiceCandidate> candidates,
LuckyDiceContext context);
}
```
首批 filter
- `EnabledFilter`
- `ProgressFilter`
- `SourceFilter`
- `CooldownFilter`
- `TutorialFilter`
筛选器按固定顺序执行,顺序建议为:
```text
Enabled → Progress → Source → Cooldown → Tutorial
```
### 权重随机
```csharp
public interface IWeightedPicker
{
LuckyDiceCandidate Pick(
IReadOnlyList<LuckyDiceCandidate> candidates,
LuckyDiceContext context,
IRandomSource random);
}
```
要求:
- 权重小于等于 0 的候选不参与普通随机。
- 随机源可注入,便于测试和回放。
- 选中结果必须记录候选池快照、权重、随机值和最终 ResultKey。
### Lucky Dice 结果槽构建
```csharp
public interface ILuckyDiceResultSlotBuilder
{
LuckyDiceResultSlots BuildSlots(
LuckyDiceCandidate selected,
LuckyDiceContext context,
DiceSetConfig luckyDiceSetConfig);
}
```
要求:
- 槽数优先级:候选结果配置 `ResultSlotCount` → 目标玩法配置 → Lucky Dice 集合配置 `DefaultResultSlotCount`
- 第一版每个槽都展示同一个 `ResultKey`
- 槽数小于等于 0 时返回结构化错误并进入 Lucky Dice 兜底。
### 模式解析
```csharp
public interface ITargetModeResolver
{
TargetModeEntry Resolve(LuckyDiceCandidate selected, LuckyDiceContext context);
}
```
解析优先级:
1. `TutorialModeRule`
2. `BonusModeRule`
3. `DefaultModeRule`
第一版映射:
| ResultKey | TargetKey | Default ModeKey |
| --- | --- | --- |
| `rocket` | `slap_down` | `slap_down_normal` |
| `thief` | `treasure_heist` | `treasure_heist_normal` |
## 兜底与错误处理
### 普通组合未命中
处理策略:
1. 记录 `RollSessionId`、原始 Faces、NormalizedKey。
2. 使用配置的普通奖励兜底,或返回可见错误给调试面板。
3. 不进入 Lucky Dice。
第一版建议默认发普通奖励兜底,避免玩家流程中断。
### Lucky Dice 候选池为空
处理策略:
1. 尝试使用配置的 default result。
2. 如果 default result 不可用,执行普通奖励兜底。
3. 记录空池原因:初始候选数量、每个 filter 过滤前后数量、最终兜底类型。
### 目标模式解析失败
处理策略:
1. 记录 `ResultKey``TargetKey`、上下文标签。
2. 如果存在 TargetKey 默认模式,降级到默认模式。
3. 如果不存在默认模式,返回 Lucky Dice 失败并执行普通奖励兜底。
## 可观测性
每次 Roll 必须拥有同一个 `RollSessionId`,贯穿普通 Roll、规则匹配、Lucky Dice、模式解析和目标玩法启动。
最小日志字段:
```text
RollSessionId
TriggerSource
NormalFaces
NormalDiceCount
ComboNormalizedKey
MatchedRuleId
MatchedRulePriority
ExecutedActions
LuckyDiceInitialCandidates
LuckyDiceFilteredCandidates
LuckyDiceFallbackType
LuckyDiceSelectedResultKey
LuckyDiceSelectedTargetKey
LuckyDiceResultSlotCount
LuckyDiceSlotResultKeys
ResolvedModeKey
FinalOutcome
```
日志目标:
- 复盘一次 Roll 为什么发奖、为什么进 Lucky Dice、为什么进某个玩法。
- 发现配置问题时能看出是规则未命中、候选池为空、权重异常还是模式解析失败。
## 配置示例
第一版可以使用本地静态配置或 ScriptableObject远端配置热更新不在范围内。
### 骰子集合配置示例
```json
{
"diceSetId": "normal_main",
"setType": "Normal",
"diceCount": 2,
"faces": ["2", "3", "4", "5", "6", "clover"]
}
```
```json
{
"diceSetId": "lucky_dice",
"setType": "Lucky",
"diceCount": 1,
"defaultResultSlotCount": 3,
"faces": ["rocket", "thief", "chest", "bomb", "key"]
}
```
### 普通组合规则示例
```json
[
{
"ruleId": "normal_clover_clover_lucky_dice",
"priority": 100,
"matcher": {
"matcherType": "ExactCombo",
"comboKey": "clover_clover"
},
"actions": [
{ "actionType": "trigger_lucky_dice", "params": {} }
],
"stopAfterMatched": true
},
{
"ruleId": "normal_number_clover_bonus",
"priority": 50,
"matcher": {
"matcherType": "ContainsFace",
"faceKey": "clover",
"requiredCount": 1
},
"actions": [
{ "actionType": "grant_reward", "params": {} },
{ "actionType": "add_multiplier", "params": { "reason": "clover_bonus" } }
],
"stopAfterMatched": true
},
{
"ruleId": "normal_all_numbers_reward",
"priority": 10,
"matcher": {
"matcherType": "AllNumbers"
},
"actions": [
{ "actionType": "grant_reward", "params": {} }
],
"stopAfterMatched": true
}
]
```
后续多骰规则示例:
```json
[
{
"ruleId": "normal_at_least_two_clovers_lucky_dice",
"priority": 100,
"matcher": {
"matcherType": "FaceCountRange",
"faceKey": "clover",
"minCount": 2
},
"actions": [
{ "actionType": "trigger_lucky_dice", "params": {} }
],
"stopAfterMatched": true
},
{
"ruleId": "normal_three_of_a_kind_bonus",
"priority": 60,
"matcher": {
"matcherType": "NumberOfAKind",
"ofAKindCount": 3
},
"actions": [
{ "actionType": "grant_reward", "params": {} },
{ "actionType": "add_multiplier", "params": { "reason": "three_of_a_kind" } }
],
"stopAfterMatched": true
}
]
```
### Lucky Dice 候选示例
```json
[
{
"resultKey": "rocket",
"targetKey": "slap_down",
"resultSlotCount": 3,
"weight": 1,
"enabled": true,
"minLevel": 0,
"sourceFilter": ["normal_roll"],
"isDefault": true
},
{
"resultKey": "thief",
"targetKey": "treasure_heist",
"resultSlotCount": 3,
"weight": 1,
"enabled": true,
"minLevel": 0,
"sourceFilter": ["normal_roll"],
"isDefault": false
}
]
```
## 测试规格
### 组合事实测试
- `2 + 3` 生成 `NumberSum = 5``CloverCount = 0``NormalizedKey = "2_3"`
- `2 + clover``clover + 2` 都生成 `NormalizedKey = "2_clover"`
- `clover + clover` 生成 `CloverCount = 2``NormalizedKey = "clover_clover"`
- `2 + 2 + clover` 生成 `DiceCount = 3``NumberValues = [2, 2]``NormalizedKey = "2_2_clover"`
- Lucky Dice 特殊结果不能被普通 `ComboFactBuilder` 当成普通组合处理。
### 规则匹配测试
- `clover_clover` 命中优先级 100 的 Lucky Dice 规则。
- `2_clover` 命中优先级 50 的 Clover bonus 规则。
- `2_6` 命中优先级 10 的 AllNumbers 规则。
- 当多条规则同时可命中时,高优先级先执行。
- 3 骰配置下,`FaceCountRange clover min 2` 能命中任意至少 2 个 Clover 的组合。
- `NumberOfAKind count 3` 能命中三个相同数字,不依赖具体数字 key。
### 行为执行测试
- `grant_reward` 调用奖励执行器。
- `trigger_lucky_dice` 调用 Lucky Dice 流程入口。
- dispatcher 只按 `ActionType` 分发,不依赖组合 key。
- 未注册的 action 返回结构化错误并记录日志。
### Lucky Dice 测试
- Enabled=false 的候选被过滤。
- 玩家等级不足时候选被 `ProgressFilter` 过滤。
- 候选池为空时使用 default result。
- default result 不可用时走普通奖励兜底并记录日志。
- 权重随机可以通过固定随机源得到可预测结果。
- Lucky Dice 结果槽数量来自候选结果、目标玩法或 DiceSet 默认配置。
- 第一版 `rocket` 生成 3 个 `rocket` 展示槽。
- `rocket` 解析到 `slap_down_normal`
- `thief` 解析到 `treasure_heist_normal`
### 链路测试
- 普通 Roll 出 `clover + clover` 后,最终进入 Lucky Dice而不是发普通奖励。
- Lucky Dice 选中 `rocket` 后,启动 Slap Down 普通模式。
- Lucky Dice 选中 `thief` 后,启动 Treasure Heist 普通模式。
- 同一个 `RollSessionId` 能串起普通结果、命中规则、Lucky Dice 候选、随机结果和最终 ModeKey。
- 普通骰子数量从 2 改为 3 时,组合事实与 matcher 仍按统计结果工作,不出现固定左右骰逻辑。
## 验收映射
| 需求验收点 | 规格落点 |
| --- | --- |
| 普通骰子结果能转换为结构化组合事实 | `ComboFacts`、组合事实测试 |
| 普通骰子数量和 Lucky Dice 结果槽数量来自配置 | `DiceSetConfig``LuckyDiceResultSlots`、槽构建测试 |
| 少量通用匹配器命中普通组合 | `ComboMatcherSpec`、规则匹配测试 |
| 双四叶草触发 Lucky Dice | 普通规则优先级 100、链路测试 |
| 执行层不维护大型组合 map | `IRollActionExecutor`、行为执行测试 |
| Lucky Dice 先筛选再权重随机 | Lucky Dice 筛选、权重随机接口 |
| Lucky Dice 结果解析到目标玩法和模式 | `TargetModeResolver`、模式映射 |
| Rocket 进入 Slap Down 普通模式 | 第一版模式映射、Lucky Dice 测试 |
| Thief 进入 Treasure Heist 普通模式 | 第一版模式映射、Lucky Dice 测试 |
| 候选池为空时有可观测兜底 | 兜底与错误处理、日志字段 |
| 日志能追踪一次 Roll 的关键决策 | 可观测性字段、链路测试 |
## 第一版不做
- 复杂动画完整复刻。
- 所有 Lucky Dice 特殊结果类型。
- 复杂运营活动权重策略。
- 完整短玩法内容实现。
- 远端配置热更新。
- 按组合 key 直接绑定业务方法。
## 后续扩展建议
- 当组合规则增多后,把本地配置迁移到 ScriptableObject 或 JSON 配置表。
- 当 Lucky Dice 特殊结果超过第一版范围后,补充结果图标、目标玩法、默认模式和冷却规则的配置校验。
- 当接入埋点时,直接复用 `RollSessionId` 和可观测性字段,避免另起一套追踪链路。
- 当新增有序组合时,只新增规则声明与 matcher 参数,不修改 `ComboFacts` 的原始结果保存方式。
- 当普通骰子扩展到 3 颗或 4 颗时,优先新增统计型规则,不新增固定位置分支。