# 02 内容创作指南 (Content Authoring)

> 策划和程序共同维护的内容文件。所有数值修改无需改架构代码，只改指定位置。

> **⚠️ 实现现状 (2026-07-20 审计)**：本文多个「改硬编码」的步骤已**过期作废**——项目内容早已全面 JSON 数据驱动，法术/波次/敌人/共鸣/Core 均从 `data/*.json` 加载，相关硬编码工厂函数与常量表已从代码中删除。**当前唯一权威的内容创作工作流是 [07 游戏设计器](07_game_designer.md)**（编辑器内插件面板，全部经 `data/*.json` + 编辑器，零代码）。本文中标注 ❌**OBSOLETE** 的小节仅保留作历史参考，请勿据其修改代码。详见审计报告 ../../docs_dev/doc_code_audit_2026-07-20.md。

> ⭐ **策划/美术优先用可视化工具**：[07 游戏设计器](07_game_designer.md) 提供编辑器内一站式面板（法术/敌人/波次/Core/平衡），表单填数值即可，**无需写代码**。本文的代码方式已大部分作废（见上方现状说明）。

---

## A. 添加/修改法术

> **⚠️ 实现现状 (2026-07-20 审计)**：法术定义唯一来源是 `res://data/spells.json`，由 `SpellRegistry` 启动时加载。`wand_preset.gd` **已不再定义任何法术**（文件头注：「法术已迁至 SpellRegistry」），下文 `make_frost_bolt` / `make_spell_by_id` / `get_all_spell_ids` 等工厂函数**在代码中已不存在**，请勿添加。详见审计报告 ../../docs_dev/doc_code_audit_2026-07-20.md。

> 💡 **策划优先用可视化工具**：在 [07 游戏设计器](07_game_designer.md) 的「法术」页签（字段化表单，用法详见 [06 法术编辑器](06_spell_editor.md)）新增/编辑法术，保存到 `data/spells.json`，**无需写代码**。

### 法术定义位置

- **唯一来源**：`res://data/spells.json`（由游戏设计器/法术编辑器维护，启动时由 `SpellRegistry` 加载）。
- 手工编辑时，每条法术是一个对象，键与旧工厂函数字段对应：`id` / `type` / `display_name`（tr() 键）/ `description` / `element_tags`（如 `["tag:ice"]`）/ `meta`（`base_damage` / `speed` / `lifetime` / `radius` / `damage_type` / `apply_status_id` / `shop_cost` 等）。推荐用编辑器面板生成，避免手写 JSON 出错。

### 步骤：添加一个新 ACTION 法术（当前正确流程）

1. 打开游戏设计器「法术」页签，新增一条法术，填写上述字段（`data/spells.json` 会自动写入）。
2. 在 `translations/*.po` 四个文件中添加对应翻译键（见本地化指南）。
3. 无需改动任何 `.gd` 代码；`SpellRegistry` 启动即加载，商店会自动抽取新法术。

> ❌ **OBSOLETE（历史参考，代码已删除）**：早期版本需在 `wand_preset.gd` 添加 `make_frost_bolt()` 工厂函数并注册到 `make_spell_by_id` / `get_all_spell_ids`。这些函数已随 SpellRegistry 迁移删除，请勿参照。

---

### 支持的法术类型

| `SpellNode.SpellType` | 说明 | meta 必填键 |
|:---|:---|:---|
| `ACTION` | 发射弹道 | `base_damage`, `speed`, `lifetime`, `radius` |
| `ACTION`(zone) | 地面效果 | `action_kind:"zone"`, `radius`, `status_id`, `duration`, `tick_interval` |
| `ACTION`(summon) | 召唤物 | `action_kind:"summon"`, `lifetime`, `range`, `fire_interval`, `base_damage` |
| `MODIFIER` | 修改施法参数 | 至少一个：`damage_add`, `damage_mult`, `spread_add`, `speed_mult`, `multicast` |
| `TRIGGER` | 命中触发子弹 | `base_damage`, `speed`, `lifetime`, `radius`（触发物自身弹道参数） |
| `LOGIC` | 条件/循环 | `logic_op`（见下表）|

### LOGIC meta 参数

| `logic_op` | 额外 meta | 效果 |
|:---|:---|:---|
| `"every_n_shots"` | `n:int`, `reg:0-3` | 每 N 次施法触发 |
| `"if_hp_below"` | `threshold:float(0-1)` | HP < 阈值时触发 |
| `"if_enemy_nearby"` | `range:float` | 范围内有敌时触发 |
| `"loop"` | `count:int(1-16)` | 后续法术重复 N 次 |

---

## B. 修改波次配置

### 波次配置位置

`scripts/autoloads/wave_manager.gd` → `WAVE_CONFIG: Array`（数组索引 0 = Wave 1）

### 配置字段说明

```gdscript
{"count": 20, "hp": 15.0, "type": 0, "elite": 0, "gold": 10, "xp": 20, "boss": -1}
#  count  : 本波生成的杂兵数量
#  hp     : 杂兵基础血量（受难度乘子影响）
#  type   : 敌人原型（见下方原型表）
#  elite  : W15+ 带 NavigationAgent2D 寻路的精英数量
#  gold   : 波次结算奖励金币
#  xp     : 波次结算奖励 XP
#  boss   : -1=无；4=Mini Boss；5=Final Boss
```

### 敌人原型（`EnemyManager.Type`）

| 值 | 名称 | 速度 | 护甲 | 颜色 | 大小 |
|:---|:---|:---|:---|:---|:---|
| 0 | BASIC | 80 | 0 | 红 | 14px |
| 1 | FAST | 140 | 0 | 黄 | 11px |
| 2 | ARMORED | 60 | 5 | 灰 | 16px |
| 3 | ELITE | 90 | 3 | 橙 | 22px |
| 4 | MINIBOSS | 55 | 8 | 品红 | 48px |
| 5 | BOSS | 45 | 14 | 深红 | 76px |

### 示例：修改 W8 Mini Boss 血量

```gdscript
# wave_manager.gd line ~50
{"count": 12, "hp": 30.0, "type": 0, "elite": 0, "gold": 80, "xp": 120, "boss": 4},
# ↑ boss:4 → _BOSS_HP[4] = 450.0 控制 Boss 血量

# 修改 Boss 血量，找到下面这行：
const _BOSS_HP: Dictionary = {4: 450.0, 5: 1800.0}
# 改成你想要的数值
```

---

## C. 修改敌人原型数值

`scripts/autoloads/enemy_manager.gd` 顶部常量：

```gdscript
const _SPEED: Dictionary = {0: 80.0, 1: 140.0, 2: 60.0, 3: 90.0, 4: 55.0, 5: 45.0}
const _ARMOR: Dictionary = {0: 0.0,  1: 0.0,   2: 5.0,  3: 3.0,  4: 8.0,  5: 14.0}
```

直接修改对应值即可，无需改其他代码。

---

## D. 修改难度乘子

`scripts/autoloads/settings_manager.gd`：

```gdscript
func enemy_hp_mult()        -> float: return [0.7, 1.0, 1.0][difficulty]
func player_dmg_taken_mult()-> float: return [0.7, 1.0, 1.0][difficulty]
func wave_count_mult()      -> float: return [1.0, 1.0, 1.2][difficulty]
func boss_hp_mult()         -> float: return [1.0, 1.0, 1.3][difficulty]
# 数组格式：[初学者, 标准, 挑战]
```

---

## E. 添加共鸣配方

`scripts/domain/spell_system/spell_evaluator.gd` → `_ready()` → `_resonance_recipes`：

```gdscript
_resonance_recipes = [
    {
        "id":             "plasma_storm",          # 配方 ID
        "pattern":        ["tag:water", "tag:lightning"],  # 元素标签组合
        "match":          "adjacent",              # "adjacent"=相邻；"anywhere_in_deck"=任意位置
        "result_spell_id":"action_plasma_storm",   # 产物法术 id（须在 WandPreset 中注册）
        "consume_inputs": true,                    # true=消耗原始法术插槽
    },
    # 新增配方直接追加到这里
    {
        "id":             "ice_nova",
        "pattern":        ["tag:ice", "tag:water"],
        "match":          "adjacent",
        "result_spell_id":"action_ice_nova",
        "consume_inputs": false,                   # false=原法术保留，额外注入产物
    },
]
```

> **注意**：产物法术必须同时在 `WandPreset.make_spell_by_id()` 中注册，才能被 `SpellRegistry` 识别。

---

## F. 添加/修改状态效果

### 现有状态 ID（`scripts/autoloads/status_id.gd`）

```gdscript
const BURN = 1    # 燃烧 DoT（火系）
const FREEZE = 2  # 冻结（硬控）
const POISON = 3  # 中毒（可叠层 DoT）
const WET = 4     # 潮湿
const OILY = 5    # 油腻
const STUN = 6    # 麻痹
const COMBO_MARK = 7
const VULNERABILITY = 8
# 新增从 9 开始递增
```

### 状态数值定义（`.tres` 资源，目前占位代码生成）

当前 `StatusRegistry` 在 `_ready()` 扫描 `res://resources/status_types/` 目录。若不存在资源文件，会尝试从 `WandPreset` 或内置回退。添加新状态的完整流程：

1. 在 `status_id.gd` 追加常量（从 9 起）
2. 在 `StatusTypeDef` 对象中配置 tick_interval / stack_mode / max_stacks
3. 在状态数值生效的 `can_catalyze` 字段填写对应状态 ID 列表
4. 在四个 `.po` 文件中添加 `STATUS_新状态_NAME` 等翻译键

---

## G. 添加 Core（法杖类型）

```gdscript
# wand_preset.gd 中添加：
func make_staff_long() -> CoreDefinition:
    var c := CoreDefinition.new()
    c.id            = "staff_long"
    c.display_name  = "CORE_STAFF_LONG_NAME"   # tr() 键
    c.slot_count    = 10                        # 插槽数
    c.topology      = CoreDefinition.Topology.LINEAR
    c.cpu_limit     = 10
    c.cast_interval = 0.7
    return c
```

然后在 `CombatManager._CORE_ROSTER` 中追加 `"staff_long"`，以及在 `_make_core_by_id()` 和 `_default_deck_for()` 中添加对应分支。

---

## H. 调试技巧

### 在运行中检查任意系统状态

利用 MCP 游戏脚本执行器（或 Godot 调试器表达式）：

```gdscript
# 查看当前法术 VM 编译结果
var ids = []
for nd in SpellEvaluator._equipped_compiled.nodes: ids.append(nd.id)
print(ids)

# 立即杀死所有敌人（测试波次结算）
for i in range(EnemyManager.get_active_count()-1, -1, -1):
    var eid = EnemyManager._slot_entity_map.get(i, -1)
    if eid >= 0: EnemyManager.apply_damage(eid, 99999.0, -1, false)

# 给玩家大量金币
PlayerStats.gain_gold(9999)
```
