# FTUE / 新手引导系统架构设计
# Tutorial & Onboarding System Architecture

> **范围**：本文档涵盖游戏的首次用户体验（FTUE）、上下文提示、引导状态机，以及  
> `TutorialManager` Autoload 的完整设计规范。  
> 本文档以 `architecture_design.md` 为基础，所有运行时接口遵循 §6.1 语言分区原则。

---

> **⚠️ 实现现状 (2026-07-20 审计)**：本文档描述的整套 FTUE 架构——`TutorialManager` Autoload、9 步状态机、气泡提示 `tutorial_hint_bubble.tscn`、`hint_*.tres`、情境提示、渐进解锁（局 1-5）、靶场慢放——**均未实现**。现状"新手引导" = 仅**首次启动的难度三选一弹窗**（`main_menu.gd` 的 FTUE 面板 + `SettingsManager.ftue_done`）。下文保留作目标设计。详见 [审计报告](../../docs_dev/doc_code_audit_2026-07-20.md)。

## 一、设计目标

| 目标 | 度量标准 |
| :--- | :--- |
| 前 3 分钟玩家留存 | 90%+ 玩家完成首波（Wave 1）且尝试第一次施法 |
| 零文字强制阅读 | 教学信息通过上下文提示传达，不弹出强制阅读对话框 |
| 可跳过原则 | 所有引导步骤可在设置中永久关闭，或在进行中跳过 |
| 渐进式复杂度 | W1–W3 仅展示核心移动+射击；W4+ 逐步解锁进阶提示 |

---

## 二、TutorialManager Autoload

### 2.1 职责边界

- **GDScript 层**（`tutorial_manager.gd`）：状态读写、EventBus 订阅、提示节点显示/隐藏。  
- **不含热路径**：提示触发频率极低（每场景级别），全部保留在 GDScript。

```gdscript
# scripts/autoloads/tutorial_manager.gd
extends Node

## 教学步骤枚举（顺序即解锁顺序）
enum TutorialStep {
    MOVE            = 0,   # WASD 移动
    CAST            = 1,   # 首次自动施法（Wand 冷却后自动提示）
    PICK_SPELL      = 2,   # 首次拾取法术卡
    OPEN_SHOP       = 3,   # 首次进入商店
    EQUIP_CORE      = 4,   # 首次装备 Core
    KILL_10         = 5,   # 击杀 10 个敌人
    WAVE_CLEARED    = 6,   # 通过 Wave 1
    RESONANCE_HINT  = 7,   # 首次触发共鸣（S3 解锁）
    COMPLETED       = 99,  # 引导全部完成
}

## 已完成步骤的持久化集合（存入 ProfileManager）
var _completed_steps: PackedInt32Array = PackedInt32Array()

## 当前激活提示步骤（-1 = 无）
var _active_step: int = -1

func _ready() -> void:
    # 从 ProfileManager 恢复已完成状态
    _completed_steps = ProfileManager.get_int_array(
        "tutorial_completed", PackedInt32Array())
    # 订阅进度事件
    EventBus.subscribe(EventID.PLAYER_SPAWNED,  _on_player_spawned)
    EventBus.subscribe(EventID.SPELL_CAST,      _on_spell_cast)
    EventBus.subscribe(EventID.SHOP_OPENED,     _on_shop_opened)
    EventBus.subscribe(EventID.ENEMY_KILLED,    _on_enemy_killed)
    EventBus.subscribe(EventID.WAVE_CLEARED,    _on_wave_cleared)
    EventBus.subscribe(EventID.RESONANCE_TRIGGERED, _on_resonance)
```

### 2.2 状态机定义

```
[初始状态]
     │
     ▼
MOVE ──────────────── 检测：玩家首帧输入 WASD 后标记完成
     │
     ▼
CAST ──────────────── 检测：EventID.SPELL_CAST 首次触发
     │
     ▼
PICK_SPELL ─────────── 检测：拾取 loot 掉落的法术卡
     │
     ▼
OPEN_SHOP ──────────── 检测：Wave 1 通关后自动弹出商店提示
     │
     ▼
EQUIP_CORE ─────────── 检测：首次从商店装备任意 Core
     │
     ▼
KILL_10 ────────────── 检测：ENEMY_KILLED 事件累计 10 次
     │
     ▼
WAVE_CLEARED ───────── 检测：WAVE_CLEARED（wave_num=1）
     │
     ▼
RESONANCE_HINT ─────── 检测：RESONANCE_TRIGGERED（S3 起激活）
     │
     ▼
COMPLETED ──────────── 持久化到 ProfileManager，停止所有提示
```

- **前向跳跃**：若玩家自行完成某步（无提示出现），步骤自动标记完成，无需显示提示。
- **后向保护**：已完成步骤不会因新 Run 而重置（存 ProfileManager 而非 RunData）。

### 2.3 核心接口

```gdscript
# 查询某步骤是否已完成
func is_completed(step: TutorialStep) -> bool:
    return step in _completed_steps

# 手动标记完成（商店系统 / 法杖编辑器调用）
func mark_complete(step: TutorialStep) -> void:
    if is_completed(step): return
    _completed_steps.append(int(step))
    ProfileManager.set_int_array("tutorial_completed", _completed_steps)
    _hide_hint()
    EventBus.emit(EventID.TUTORIAL_STEP_COMPLETED, step)

# 显示上下文提示（锚定到 UI 节点旁）
func show_hint(step: TutorialStep, anchor: Control,
               offset: Vector2 = Vector2.ZERO) -> void:
    if is_completed(step): return
    _active_step = step
    # 委托给 UIManager 渲染气泡提示
    UIManager.show_tutorial_hint(
        TutorialHintData.build(step), anchor, offset)

func _hide_hint() -> void:
    if _active_step == -1: return
    UIManager.hide_tutorial_hint()
    _active_step = -1
```

---

## 三、上下文提示系统（Contextual Hints）

### 3.1 提示数据结构

```gdscript
# resources/tutorial/tutorial_hint_data.gd
class_name TutorialHintData
extends Resource

@export var step:       int     # TutorialStep 枚举值
@export var icon_key:   String  # 对应 UIAtlas 中的图标键（如 "wasd_icon"）
@export var text_key:   String  # i18n 键（tr(text_key) 使用）
@export var arrow_dir:  int     # 0=上, 1=右, 2=下, 3=左（箭头指向目标）
@export var duration:   float = 0.0  # 0 = 手动关闭，>0 = 自动消失秒数

static func build(step: TutorialManager.TutorialStep) -> TutorialHintData:
    return _HINT_TABLE[step]

# 提示内容表（运行时常量，避免 JSON 热加载）
const _HINT_TABLE: Dictionary = {
    0: preload("res://resources/tutorial/hint_move.tres"),
    1: preload("res://resources/tutorial/hint_cast.tres"),
    2: preload("res://resources/tutorial/hint_pick_spell.tres"),
    3: preload("res://resources/tutorial/hint_shop.tres"),
    4: preload("res://resources/tutorial/hint_equip_core.tres"),
    5: preload("res://resources/tutorial/hint_kill10.tres"),
    6: preload("res://resources/tutorial/hint_wave_cleared.tres"),
    7: preload("res://resources/tutorial/hint_resonance.tres"),
}
```

### 3.2 提示触发时机

| 步骤 | 触发时机 | 自动消失 | 跳过方式 |
| :--- | :--- | :--- | :--- |
| MOVE | 游戏开始后 1s（玩家还未移动） | 首次移动后 | 任意移动 |
| CAST | 法杖冷却恢复后 2s（玩家未施法） | 首次施法后 | 施法 |
| PICK_SPELL | 首个法术 loot 落地后 3s | 拾取后 | 走到 loot 上 |
| OPEN_SHOP | Wave 1 通关时自动进入商店流程 | 商店打开后 | 自动 |
| EQUIP_CORE | 商店内有 Core 可购买时 | 购买后 | 购买 |
| KILL_10 | 击杀计数达 5 时预显提示 | 计数达 10 | 击杀 |
| WAVE_CLEARED | Wave 1 通关瞬间 | 5s 后 | 点击 |
| RESONANCE_HINT | 首次编辑法杖时检测邻接 | 触发共鸣后 | 触发共鸣 |

### 3.3 提示 UI 节点结构

```
CombatScene
└── UILayer (CanvasLayer z_index=100)
    └── TutorialHintContainer  # UIManager 管理的专用节点
        ├── HintBubble (Panel)
        │   ├── Icon (TextureRect)
        │   └── Label (text = tr(hint.text_key))
        └── Arrow (TextureRect, 根据 arrow_dir 旋转)
```

- `TutorialHintContainer` 随 `anchor` 节点世界坐标定位（`force_update_transform` 每帧同步）。
- 气泡使用 `AnimationPlayer` 播放 0.2s 淡入动画，消失时 0.15s 淡出。
- 同一时刻最多显示 1 个提示（显示新提示时先隐藏旧提示）。

---

## 四、首 3 分钟玩家流（First 3 Minutes Flow）

```
T=0s   游戏载入 → CombatScene 初始化
T=1s   [提示] WASD 移动（气泡锚定玩家位置右侧，arrow→左）
T=~5s  玩家完成移动 → MOVE 标记完成 → 提示消失
T=~8s  法杖冷却结束 → [提示] 观察法术已自动施发（施法为自动）
T=~15s 首个敌人被击杀 → loot 掉落 → [提示] 拾取法术卡
T=~30s 玩家拾取法术卡 → PICK_SPELL 标记完成
T=~60s Wave 1 清场 → 自动进入商店
T=商店  [提示] 尝试装备 Core（若商店有 Core，arrow→Core 格）
T=~90s 购买/关闭商店 → Wave 2 开始
T=~120s 已掌握基本循环，FTUE 完成度 ~70%
T=Wave 4+ 触发共鸣时 → 最终提示显示
```

---

## 五、关键 EventID 扩展

> 以下 EventID 需追加到 `event_id.gd`（当前最大 ID = 17）：

| ID | 常量名 | 负载 | 触发方 | 监听方 |
| :--- | :--- | :--- | :--- | :--- |
| 18 | `TUTORIAL_STEP_COMPLETED` | `step: int` | TutorialManager | UIManager（显示解锁提示）|
| 19 | `SHOP_OPENED` | — | ShopManager | TutorialManager |
| 20 | `RESONANCE_TRIGGERED` | `recipe_id: String` | SpellEvaluator | TutorialManager, VFXManager |

> **⚠️ 实现现状 (2026-07-20 审计)**：上表 EventID 与实际 `event_ids.gd` **不符**——`TUTORIAL_STEP_COMPLETED` 与 `RESONANCE_TRIGGERED` 常量**根本不存在**；ID 18/19/20/21 实为 `ACHIEVEMENT_UNLOCKED`/`BOSS_SPAWNED`/`GAME_STATE_CHANGED`/`SPELL_DROP_PICKUP`，`SHOP_OPENED` 实为 17。详见 [审计报告](../../docs_dev/doc_code_audit_2026-07-20.md)。
| 21 | `ACHIEVEMENT_UNLOCKED` | `achievement_id: String` | AchievementManager | Steam 插件层 |

> **注意**：ID 0 为 `CRASH_DETECTED`（CrashReporter 专用，已定义）。

---

## 六、可跳过 & 关闭机制

```gdscript
# settings_manager.gd 新增项
const KEY_TUTORIAL_DISABLED: String = "tutorial_disabled"

# TutorialManager._ready() 中检查
func _ready() -> void:
    if ProfileManager.get_bool(SettingsManager.KEY_TUTORIAL_DISABLED, false):
        # 将所有步骤标记为已完成，彻底关闭引导
        for s in TutorialStep.values():
            _completed_steps.append(s)
        return
    # 正常初始化...
```

设置菜单提供「关闭新手提示」开关，写入 `ProfileManager`（持久化跨 Run）。  
**重置入口**：`SettingsManager.reset_tutorial()` → 清空 `tutorial_completed` 并重置标志，  
下次启动重新触发完整 FTUE 流程（适用于练习账号 / QA 测试）。

---

## 七、文件清单

```
scripts/autoloads/
  tutorial_manager.gd          # 核心 Autoload（本文档 §2 实现）

resources/tutorial/
  hint_move.tres                # TutorialHintData（MOVE 步骤）
  hint_cast.tres
  hint_pick_spell.tres
  hint_shop.tres
  hint_equip_core.tres
  hint_kill10.tres
  hint_wave_cleared.tres
  hint_resonance.tres

scenes/ui/
  tutorial_hint_bubble.tscn     # HintBubble + Arrow 节点树
```

**Autoload 注册顺序**（`project.godot`）：

> `TutorialManager` 在 `EventBus` 和 `ProfileManager` 之后注册，  
> 确保 `_ready()` 时可正常读取持久化数据并订阅事件。

---

## 八、与其他系统的接口约定

| 系统 | 接口 | 说明 |
| :--- | :--- | :--- |
| `UIManager` | `show_tutorial_hint(data, anchor, offset)` / `hide_tutorial_hint()` | 渲染和动画委托给 UIManager |
| `ProfileManager` | `get_int_array / set_int_array("tutorial_completed", ...)` | 跨 Run 持久化已完成步骤 |
| `EventBus` | 订阅 ID 1/6/11/18~21 等 | 只读订阅，不 emit 游戏逻辑事件 |
| `SpellEvaluator` | 无直接依赖 | 共鸣触发通过 `RESONANCE_TRIGGERED` 事件解耦 |
| `SettingsManager` | `KEY_TUTORIAL_DISABLED` 常量 | 关闭引导的持久化开关 |
