# 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 颗时，优先新增统计型规则，不新增固定位置分支。
