# 03 资源替换指南 (Asset Replacement Guide)

> 当前项目使用**程序化占位**（几何体 + 蜂鸣音），所有接口已就绪。本文说明如何无缝替换为正式美术资源，**不需要修改任何业务逻辑代码**。

---

## A. 音效替换

### 当前状态

`AudioManager` 在 `res://audio/sfx/<id>.ogg`（或 `.wav` / `.mp3`）**不存在**时自动回退到程序化蜂鸣。文件存在即优先加载真实音效。

### 替换步骤

**1. 创建目录并放入文件**

```
res://audio/sfx/
  hit.ogg           ← 子弹命中音效
  kill.ogg          ← 敌人死亡
  hurt.ogg          ← 玩家受击
  wave_complete.ogg ← 波次结算
  boss.ogg          ← Boss 入场
  shop.ogg          ← 商店开启
  buy.ogg           ← 购买法术
  level_up.ogg      ← 升级
```

**2. 确认格式要求**

| 属性 | 推荐值 |
|:---|:---|
| 格式 | OGG Vorbis（体积小）/ WAV PCM（低延迟） |
| 采样率 | 44100 Hz 或 22050 Hz |
| 声道 | 单声道（Mono）即可，立体声也支持 |
| 时长 | 音效 < 3s；BGM 循环 |
| 响度 | 建议 -14 LUFS 标准化后导入 |

**3. Godot 导入设置**（在编辑器 FileSystem 面板右键文件 → Import）

| 设置 | 推荐值 |
|:---|:---|
| Loop | 音效：`Off`；BGM：`On` |
| Compression Mode | `quality` OGG / `PCM` WAV |
| Max Texture Size | 不适用（音频） |

**4. 添加新音效 ID**

在 `AudioManager._SFX_DEFS` 中追加一行（定义占位蜂鸣，有真实文件时忽略）：

```gdscript
# audio_manager.gd
const _SFX_DEFS: Dictionary = {
    # ... 已有 ...
    "spell_cast": [600.0, 80.0],   # 新增：频率 600Hz，80ms
}
```

然后在需要播放的地方调用：

```gdscript
AudioManager.play("spell_cast", spawn_pos)
```

### BGM 系统（预留接口）

当前无 BGM 系统，添加时建议在 `AudioManager` 中扩展：

```gdscript
# 建议扩展方向（未实现，供参考）
func play_bgm(track_id: String, fade_sec: float = 1.0) -> void:
    var path = "res://audio/bgm/%s.ogg" % track_id
    # 跨淡 + loop...
```

---

## B. 敌人 / 玩家 精灵替换

### 当前状态

- **玩家**：`combat_s2.gd` 中程序化 `Polygon2D`（青色方块）
- **敌人**：`EnemyManager.sync_multimesh()` 驱动 `MultiMeshInstance2D`，使用单位 `QuadMesh` + 颜色覆盖

### 替换玩家精灵

`combat_s2.gd` → `_setup_world_view()`，找到玩家标记部分替换：

```gdscript
# 当前（几何占位）
var marker := Polygon2D.new()
marker.polygon = PackedVector2Array([...])
marker.color = Color(0.35, 0.9, 1.0)
_player_node.add_child(marker)

# 替换为精灵（修改为）
var marker := Sprite2D.new()
marker.texture = load("res://assets/sprites/player.png")
marker.scale   = Vector2(0.5, 0.5)  # 按实际尺寸调整
_player_node.add_child(marker)
```

### 替换敌人渲染（MultiMesh + Atlas）

敌人使用 `MultiMeshInstance2D`，单次 Draw Call 渲染所有实例。替换步骤：

**步骤 1：准备 Atlas 贴图**

将所有敌人原型排列在一张贴图上（建议 512×512）：

```
atlas_enemies.png
┌────┬────┬────┐
│BASIC│FAST│ARM.│  每格 64×64 px
├────┼────┼────┤
│ELITE│MIN.│BOSS│
└────┴────┴────┘
```

**步骤 2：修改 MultiMesh 使用 Atlas**

`combat_s2.gd` → `_setup_world_view()` 中的敌人 MMI 部分：

```gdscript
# 原：使用 QuadMesh 纯色
var quad := QuadMesh.new()
quad.size = Vector2(1.0, 1.0)
_enemy_mm.mesh = quad

# 替换：使用 PlaneMesh + Atlas 材质
var mesh := PlaneMesh.new()
mesh.size = Vector2(1.0, 1.0)
var mat := CanvasItemMaterial.new()
# 或使用 ShaderMaterial + 自定义 Shader 实现 UV 偏移（按 enemy_type 选择 Atlas 区域）
_enemy_mm.mesh = mesh
var mmi := MultiMeshInstance2D.new()
mmi.multimesh  = _enemy_mm
mmi.texture    = load("res://assets/sprites/atlas_enemies.png")
add_child(mmi)
```

**步骤 3（进阶）：Shader 按 instance_custom_data 选 UV**

```glsl
// res://shaders/enemy_atlas.gdshader
shader_type canvas_item;
uniform sampler2D atlas_tex;
uniform int cols = 3;   // atlas 列数

void fragment() {
    // INSTANCE_CUSTOM.x 存储 enemy_type (0-5)
    float etype = INSTANCE_CUSTOM.x;
    float row   = floor(etype / float(cols));
    float col   = mod(etype, float(cols));
    vec2 uv = (UV + vec2(col, row)) / vec2(float(cols), 2.0);
    COLOR = texture(atlas_tex, uv) * COLOR;
}
```

在 `EnemyManager.sync_multimesh()` 中写入 custom data：

```gdscript
# enemy_manager.gd sync_multimesh() 内，在 set_instance_transform_2d 之后
mm.set_instance_custom_data(i, Color(float(etype) / 255.0, 0, 0, 0))
```

---

## C. 子弹精灵替换

子弹同样使用 `MultiMeshInstance2D`（`_bullet_mm`）。替换为 Atlas 方式与敌人相同，但子弹通常使用 `damage_type` 区分外观。

`BulletManager.sync_multimesh()` 中写入 custom data（damage_type）：

```gdscript
# bullet_manager.gd sync_multimesh() 内追加
mm.set_instance_custom_data(i, Color(float(_data[base + 8]) / 255.0, 0, 0, 0))
```

---

## D. VFX 特效替换

### 当前状态

`VFXManager` 使用程序化彩色矩形作为特效占位（`ColorRect`/`Sprite2D`）。

### 替换为粒子特效

`scripts/autoloads/vfx_manager.gd` → `_instantiate_vfx()` 中修改 `_vfx_scenes` 映射：

**步骤 1：制作粒子场景**

为每个特效 ID 建一个场景文件（`GPUParticles2D` 为根节点，`one_shot = true`）：

```
res://scenes/vfx/
  hit_spark.tscn      ← 命中火花
  status_burn.tscn    ← 燃烧特效
  status_poison.tscn  ← 中毒特效
  cast_flash.tscn     ← 施法闪光
  death_burst.tscn    ← 敌人死亡
  zone_expire.tscn    ← 地面效果消散
```

每个场景设置：
- `GPUParticles2D.one_shot = true`（**必须**，P6-N72）
- `GPUParticles2D.emitting = false`（代码控制时机）
- `lifetime` = 特效持续时长

**步骤 2：修改 `_vfx_scenes` 预加载表**

```gdscript
# vfx_manager.gd
func _ready() -> void:
    _vfx_scenes = {
        "hit_spark":   preload("res://scenes/vfx/hit_spark.tscn"),
        "status_burn": preload("res://scenes/vfx/status_burn.tscn"),
        # ... 其他 ...
    }
```

**步骤 3：让 VFX 播放时自动启动粒子**

> **重要**：`one_shot = true` 和 `emitting = false` 须在**场景文件内**就设好（Godot 编辑器中设置属性），这是 P6-N72 强制要求。运行时 `VFXManager.play()` 只负责把节点拖出池并触发 `emitting = true`。

```gdscript
# vfx_manager.gd → play() 内，instance 创建后追加：
if node is GPUParticles2D:
    node.emitting = true   # 启动粒子（one_shot 已在场景中设置，这里不必覆盖）
```

### 特效 ID 参考

| ID | 触发时机 |
|:---|:---|
| `"hit_spark"` | 子弹命中敌人 |
| `"status_burn"` | 施加 BURN 状态 |
| `"status_poison"` | 施加 POISON 状态 |
| `"cast_flash"` | 施法时玩家周围 |
| `"death_burst"` | 敌人死亡 |
| `"zone_expire"` | 地面效果消失 |

---

## E. UI 界面图片替换

当前 UI 全为程序化构建（`ColorRect` + `Label` + `Button`），无独立 `.tscn` 场景。替换为美术 UI 的推荐路径：

**选项 A：保留程序化，只替换 Theme**

在 `combat_s2.gd` 的各 `_setup_*` 函数中，为 `Button`/`Label` 应用自定义 Theme：

```gdscript
var theme := load("res://assets/ui/main_theme.tres")  # 你制作的 Theme 资源
btn.theme = theme
```

**选项 B：迁移到独立 .tscn 场景**（推荐，长期）

1. 为商店、背包、结算屏分别建 `.tscn`
2. 在 `_setup_shop_ui()` 等函数中替换为 `load("res://scenes/ui/shop.tscn").instantiate()`
3. 通过信号 / Callable 将原有事件（买法术、刷新等）连接到新场景节点

> **注意**：迁移 UI 时须保留对 `_shop_btns`、`_reroll_btn` 等引用的赋值，否则商店逻辑失效。

---

## F. 资源路径约定

| 类型 | 约定路径 |
|:---|:---|
| 音效 | `res://audio/sfx/<id>.ogg` |
| BGM | `res://audio/bgm/<track>.ogg` |
| 玩家精灵 | `res://assets/sprites/player.png` |
| 敌人 Atlas | `res://assets/sprites/atlas_enemies.png` |
| VFX 场景 | `res://scenes/vfx/<id>.tscn` |
| UI Theme | `res://assets/ui/main_theme.tres` |
| UI 场景 | `res://scenes/ui/<name>.tscn` |
| 字体 | `res://assets/fonts/<name>.ttf` |
</content>
