初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
# 01 快速上手 (Quickstart)
## 1. 环境要求
| 工具 | 版本 | 下载 |
|:---|:---|:---|
| Godot Engine | **4.6 Mono**(带 .NET 支持) | godotengine.org |
| .NET SDK | 8.0+ | dotnet.microsoft.com |
| Git | 任意 | git-scm.com |
> **重要**:必须下载 **Mono/C# 版本** 的 Godot(文件名含 `mono`),否则 C# 文件无法编译。
---
## 2. 首次运行
```bash
# 1. 克隆项目
git clone <repo-url>
# 2. 用 Godot 4.6 Mono 打开 project.godot
# 菜单: Project → Open / Import
# 3. 等待导入完成(首次约 10-30 秒)
# 4. 直接按 F5 运行,或菜单: Debug → Run Project
```
运行后你会看到:战斗场景直接开始,玩家(青色方块)自动发弹,敌人(红色方块)向玩家靠近。
---
## 3. 主场景说明
唯一可玩主场景:`scenes/main/combat_s2.tscn`
```
project.godot
└── run/main_scene = "res://scenes/main/combat_s2.tscn"
```
### 游戏循环
```
战斗 (BATTLE)
↓ 全部敌人/Boss 死亡
波次结算 → 商店 (SHOP)
↓ 关闭商店
战斗 (下一波)
↓ 玩家死亡
结算屏 (GAME_OVER) → 再来一局
```
### 商店内操作
| 按钮 | 功能 |
|:---|:---|
| 法术卡 | 购买并装入当前法杖 |
| 法杖 ⟳ | 切换 Core(法杖类型) |
| 🎒 背包 | 打开插槽编辑器,自由排布法术 |
| 刷新 | 重新随机商店(花费金币) |
| ⚙ 设置 | 难度 / 语言 / 音量 / 清除数据 |
| 出发下一波 | 进入下一波战斗 |
---
## 4. 调试工具
### 编辑器内测试
在任何场景运行时,可以通过 MCP 脚本执行器(godot-mcp 插件)向游戏注入 GDScript
```gdscript
# 示例:快速生成一个 Boss 进行测试
EnemyManager.reset()
WaveManager.start_wave(20) # 直接启动 W20 Boss 战
```
### HUD 调试信息
运行时左上角 HUD 显示:
- 当前波次 / HP / 金币 / XP / 击杀数
- DPS / 蓄力百分比
- `[Core名] [法术id列表]`(当前装备)
- 状态实例数 / VFX 数
### 快速切波次
```gdscript
# 在游戏内执行(须在商店状态下)
# 方法:打开商店后跳到指定波次再出发
WaveManager.current_wave = 14 # 设置当前波次基准
# 然后在商店中点「出发下一波」,即进入 W15 精英战
# 或直接重置重开:get_tree().current_scene._cm.start_game()
```
---
## 5. 项目配置文件
| 文件 | 说明 |
|:---|:---|
| `project.godot` | 引擎配置、Autoload 列表、主场景 |
| `user://save_data.json` | 设置(难度/语言/音量),运行时生成 |
| `user://run_a.json` `run_b.json` | 局内 A/B 双槽存档 |
| `user://endless_records.json` | 本地排行榜(HMAC 签名) |
> `user://` 路径在 Windows 上位于 `%APPDATA%\Godot\app_userdata\Rogue\`
---
## 6. 常见问题
**Q: F5 运行后没有画面(黑屏)**
A: 确认下载的是 **Mono 版**;等待 .NET 首次编译完成(约 30s)。
**Q: 敌人/子弹消失了但没有波次结算**
A: 可能生命时间到期。重启当前波次:`WaveManager.start_wave(WaveManager.current_wave)`
**Q: 更改了 `wand_preset.gd` 的法术数值,但游戏里没有变化**
A: `SpellRegistry``_ready()` 时缓存法术对象;需要重启游戏(F5 重新运行)使改动生效。
+248
View File
@@ -0,0 +1,248 @@
# 02 内容创作指南 (Content Authoring)
> 策划和程序共同维护的内容文件。所有数值修改无需改架构代码,只改指定位置。
> ⭐ **策划/美术优先用可视化工具**:[07 游戏设计器](07_game_designer.md) 提供编辑器内一站式面板(法术/敌人/波次/Core/平衡),表单填数值即可,**无需写代码**。本文的代码方式适合需要复杂自定义逻辑的程序员,或了解底层数据结构。
---
## A. 添加/修改法术
> 💡 **策划优先用可视化工具**:[06 法术编辑器](06_spell_editor.md) 提供编辑器内表单,无需写代码即可新增/编辑法术(保存到 `data/spells.json`)。下面的代码方式适合需要复杂逻辑的程序员。
### 法术定义位置
- **数据驱动(推荐)**`res://data/spells.json`(由法术编辑器维护,启动时由 `SpellRegistry` 加载,优先级最高)
- **硬编码(程序)**`scripts/autoloads/wand_preset.gd`(向后兼容回退)
### 步骤:添加一个新 ACTION 法术
**1. 在 `wand_preset.gd` 添加工厂函数**
```gdscript
func make_frost_bolt() -> SpellNode:
var n := SpellNode.new()
n.id = "action_frost_bolt" # 全局唯一 ID
n.type = SpellNode.SpellType.ACTION
n.display_name = "SPELL_FROST_BOLT_NAME" # tr() 键(见本地化指南)
n.description = "SPELL_FROST_BOLT_DESC"
n.element_tags = ["tag:ice"] # 共鸣系统用的元素标签
n.meta = {
"base_damage": 5.0, # 基础伤害
"speed": 380.0, # 弹速 (px/s)
"lifetime": 4.0, # 存活时间 (s)
"radius": 7.0, # 弹体半径 (px)
"damage_type": 0, # 0=通用 (DamageType.NORMAL)
"apply_status_id": StatusID.FREEZE, # 命中施加状态(可选)
"shop_cost": 22, # 商店价格
}
return n
```
**2. 注册到 `make_spell_by_id` 和 `get_all_spell_ids`**
```gdscript
func make_spell_by_id(spell_id: String) -> SpellNode:
match spell_id:
# ... 已有法术 ...
"action_frost_bolt": return make_frost_bolt() # 新增
_: push_warning(...)
func get_all_spell_ids() -> Array:
return [
# ... 已有 ...
"action_frost_bolt", # 新增到列表末尾(商店会自动抽取)
]
```
**3. 在 `translations/*.po` 四个文件中添加翻译键**(见本地化指南)
---
### 支持的法术类型
| `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 Boss5=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)
```
+281
View File
@@ -0,0 +1,281 @@
# 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)即可,立体声也支持 |
| 时长 | 音效 < 3sBGM 循环 |
| 响度 | 建议 -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], # 新增:频率 600Hz80ms
}
```
然后在需要播放的地方调用:
```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 datadamage_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>
+197
View File
@@ -0,0 +1,197 @@
# 04 本地化工作流 (Localization Workflow)
> 项目遵循 **ADR-A1**:所有玩家可见字符串必须通过 `tr("KEY")` 包裹,禁止业务代码中出现裸字符串。
---
## 1. 文件位置
```
translations/
zh_CN.po ← 简体中文(权威主档)
zh_TW.po ← 繁体中文
en.po ← 英文
ja.po ← 日文
```
---
## 2. `.po` 文件格式
每条翻译的格式为:
```po
msgid "UI_WAVE"
msgstr "第 %d 波"
```
- `msgid`:代码中 `tr("KEY")` 使用的键
- `msgstr`:该语言显示的文字
- `%d``%s``%.1f` 等格式占位符必须与代码中 `tr("KEY") % value` 完全对应
---
## 3. 添加新翻译字符串(标准流程)
### Step 1 — 在代码中使用 tr() 键
```gdscript
# ❌ 错误:裸字符串
label.text = "商店已关闭"
# ✅ 正确:通过键引用
label.text = tr("SHOP_CLOSED_MSG")
# 带参数
label.text = tr("UI_WAVE") % current_wave # → "第 3 波"
label.text = tr("UI_KILLS") % [kills, alive] # → "击杀: 7 存活: 3"
```
### Step 2 — 在全部四个 `.po` 文件中添加对应条目
**zh_CN.po**(权威档,写游戏实际中文):
```po
msgid "SHOP_CLOSED_MSG"
msgstr "商店已关闭"
```
**zh_TW.po**(繁体,改繁体字):
```po
msgid "SHOP_CLOSED_MSG"
msgstr "商店已關閉"
```
**en.po**(英文):
```po
msgid "SHOP_CLOSED_MSG"
msgstr "Shop Closed"
```
**ja.po**(日文):
```po
msgid "SHOP_CLOSED_MSG"
msgstr "ショップが閉店しました"
```
### Step 3 — 验证(运行时检查)
在游戏内执行脚本验证无缺键:
```gdscript
var keys = ["SHOP_CLOSED_MSG"] # 你新加的键
for loc in ["zh_CN","zh_TW","en","ja"]:
Locale.set_locale(loc)
for k in keys:
if tr(k) == k:
print("缺键!语言: %s 键: %s" % [loc, k])
```
---
## 4. 键名规范
**格式**`<模块>_<实体>_<含义>`,全大写 + 下划线
| 前缀 | 用途 | 示例 |
|:---|:---|:---|
| `UI_` | 通用 HUD 标签 | `UI_WAVE`, `UI_HP` |
| `SHOP_` | 商店界面 | `SHOP_TITLE`, `SHOP_REROLL` |
| `INV_` | 背包界面 | `INV_TITLE`, `INV_BENCH` |
| `SETTLE_` | 结算屏 | `SETTLE_TITLE`, `SETTLE_STATS` |
| `SETTINGS_` | 设置面板 | `SETTINGS_DIFFICULTY` |
| `STATUS_` | 状态消息 | `STATUS_PURCHASED` |
| `DIFF_` | 难度名称 | `DIFF_BEGINNER`, `DIFF_CHALLENGE` |
| `SPELL_` | 法术名/描述 | `SPELL_SPARK_BOLT_NAME`, `SPELL_SPARK_BOLT_DESC` |
| `CORE_` | Core 名/描述 | `CORE_WAND_BASIC_NAME` |
| `STATUS_EFFECT_` | 状态效果名 | `STATUS_EFFECT_BURN_NAME` |
| `BOSS_` | Boss 名称 | `BOSS_FINAL_NAME` |
---
## 5. 法术 / Core 名称本地化(待完成)
当前法术 `display_name` 字段存储的是裸英/中文字符串(如 `"Spark Bolt"`)。正式发布前需键化。
> **注意**:修改后须同步更新商店按钮渲染(`combat_s2.gd → _refresh_shop_ui`),将 `spell.display_name` 改为 `tr(spell.display_name)`。
**Step 1 — 修改 `wand_preset.gd`**
```gdscript
# 当前(裸字符串)
n.display_name = "Spark Bolt"
n.description = "发射一颗期限 4s 的电光飞弹,造成 3 伤害。"
# 修改为(键)
n.display_name = "SPELL_SPARK_BOLT_NAME"
n.description = "SPELL_SPARK_BOLT_DESC"
```
**Step 2 — 在四语 `.po` 中添加**
```po
# zh_CN.po
msgid "SPELL_SPARK_BOLT_NAME"
msgstr "电花弹"
msgid "SPELL_SPARK_BOLT_DESC"
msgstr "发射一颗快速电花飞弹,造成 3 点闪电伤害。"
```
**Step 3 — 显示时包裹 tr()**
```gdscript
# UI 显示法术名时
btn.text = tr(spell.display_name)
# 法术描述
desc_label.text = tr(spell.description)
```
---
## 6. 添加新语言
**Step 1 — 创建新 `.po` 文件**(以韩语为例)
```
translations/ko.po
```
内容:复制 `en.po`,将 `Language: en` 改为 `Language: ko`,翻译所有 `msgstr`
**Step 2 — 在 `locale_manager.gd` 中注册**
```gdscript
const LOCALES: Array = ["zh_CN", "zh_TW", "en", "ja", "ko"] # 追加 "ko"
```
新语言将自动在商店语言切换按钮中循环出现。
---
## 7. 在游戏内切换语言(测试用)
```gdscript
# 切换到英文
Locale.set_locale("en")
# 循环切换(等同商店 🌐 按钮)
Locale.cycle_locale()
# 查看当前语言
print(Locale.get_locale())
```
---
## 8. 提交规范
每次提交涉及玩家可见字符串的改动时,须**同时更新四个 `.po` 文件**。允许以英文或空字符串占位(`msgstr ""`),但 PR 审查时须标注"待翻译"。
---
## 9. 工具推荐
| 工具 | 用途 |
|:---|:---|
| [Poedit](https://poedit.net) | `.po` 文件 GUI 编辑器,支持模糊匹配和自动翻译接口 |
| VSCode + gettext 插件 | 轻量化文本编辑 |
| Godot 内置本地化 | `Project → Project Settings → Localization → Translations` 可预览各语言 |
+170
View File
@@ -0,0 +1,170 @@
# 05 发布标准清单 (Release Guide)
> 正式发布前所有 **P0 必须项** 须全部打勾。参考:`docs/plan/certification_checklist.md`(权威完整版)。
---
## 一、P0 技术验收(S6 P0,均已通过)
| 编号 | 项目 | 状态 | 说明 |
|:---|:---|:---:|:---|
| P-S6-01 | Wave 1~20 无崩溃通关;W8/W20 Boss 正常 | ✅ | 系统已实现并实测 |
| P-S6-02 | 500 敌人 + 1500 子弹稳定 60fps | ✅ | 实测 180fps / 0.6ms |
| P-S6-03 | Endless 排行榜 + HMAC 签名 | ✅ | user://endless_records.json |
| P-S6-04 | 死亡时截图保存非黑屏 | ✅ | user://runs/run_*.png |
| P-S6-05 | 所有 P6-N 规范条目无违反 | ✅ | 1 修复 / 11 PASS |
| P-S6-06 | 四语本地化(zh_CN/zh_TW/en/ja | 🟡 | 骨架完成;法术/Core 名待键化 |
| P-S6-07 | 内存峰值 RSS < 512MB | ✅ | 实测 ~176MB |
---
## 二、内容完成清单(发布前必做)
### 2.1 法术内容(当前:14 张,目标:20+ 张)
| 类型 | 已有 | 缺失(示例) |
|:---|:---|:---|
| ACTION | spark_bolt, energy_orb, fire_bolt, poison_dart, laser_beam, water_wave, chain_bolt, plasma_storm (产物), poison_pool, summon_turret | ice_nova, nuke, homing, chain_bolt(独立弹道版)|
| MODIFIER | damage_plus, spread_mod, double_cast | heavy_cost, speed_mult_mod |
| TRIGGER | trigger_on_hit | trigger_on_killTimer 触发型) |
| LOGIC | every_n_shots, if_hp_below, if_enemy_nearby, loop | — |
**添加法术流程** → 见 [02 内容创作指南 §A](02_content_authoring.md)
### 2.2 Core 种类(当前:5 种,目标:7 种)
| Core | 状态 |
|:---|:---|
| wand_basic | ✅ |
| wand_fast | ✅ |
| wand_memory (PERSISTENT_MEMORY) | ✅ |
| matrix_board (MATRIX 2×4) | ✅ |
| circuit_fork (CIRCUIT) | ✅ |
| staff_long (10 槽) | ⬜ 待加 |
| wand_eternal (INFINITE_SPELLS) | ⬜ 待加 |
### 2.3 美术资源(当前:全程序化占位)
| 资源 | 规格 | 优先级 |
|:---|:---|:---|
| 玩家精灵 | 64×64 PNG4 方向或单帧 | P0 |
| 敌人 Atlas6 原型) | 512×512,每格 64×64 | P0 |
| 子弹 Atlas(按 damage_type | 256×64,每格 32×32 | P0 |
| VFX 粒子场景(8 种特效) | GPUParticles2Done_shot=true | P0 |
| UI Theme / 背景 | 商店/结算/设置背景图 | P1 |
| App 图标 | 512×512 PNG | P0 发布前 |
**替换方法** → 见 [03 资源替换指南](03_asset_replacement.md)
### 2.4 音效资源(当前:程序化蜂鸣)
| 文件 | 内容 |
|:---|:---|
| `audio/sfx/hit.ogg` | 子弹命中 |
| `audio/sfx/kill.ogg` | 敌人死亡 |
| `audio/sfx/hurt.ogg` | 玩家受击 |
| `audio/sfx/wave_complete.ogg` | 波次结算 |
| `audio/sfx/boss.ogg` | Boss 入场 |
| `audio/sfx/shop.ogg` | 商店开启 |
| `audio/sfx/buy.ogg` | 购买法术 |
| `audio/sfx/level_up.ogg` | 升级 |
| `audio/bgm/battle_*.ogg` | 战斗 BGM(循环)|
**替换方法** → 见 [03 资源替换指南 §A](03_asset_replacement.md)
### 2.5 本地化补全
- [ ] 所有法术 `display_name` / `description` 改为 `tr()`
- [ ] 所有 Core `display_name` 改为 `tr()`
- [ ] 所有状态效果名改为 `tr()`
- [ ] Boss 名称键化
- [ ] 商店页文案(Steam 商店描述,日/英/繁中)
---
## 三、Steam PC 发布前必须项
### 3.1 Steamworks 配置(代码端)
1. **安装 GodotSteam 插件**godotsteam.com
2. **初始化**:在 `CrashReporter._ready()` 前调用 `Steam.init()`
3. **成就钩子**ST-11):`AchievementManager` 订阅 `EventID.ACHIEVEMENT_UNLOCKED`ID=18):
```gdscript
func _on_achievement_unlocked(p: Dictionary) -> void:
Steam.set_achievement(p.get("achievement_id",""))
Steam.store_stats()
```
4. **云存档**ST-50):`ProfileManager.save_run()` 包裹 `Steam.beginFileWriteBatch/endFileWriteBatch`
5. **排行榜提交**ST-42):`EndlessRecords.add_record()` 后尝试 `Steam.uploadLeaderboardScore()`,网络失败静默缓存
### 3.2 认证检查项速查
| 编号 | 项目 | 实现位置 |
|:---|:---|:---|
| ST-01 | 不收集 PII | 无账号系统 ✅ |
| ST-03 | 清除所有本地数据 | `SettingsManager.clear_all_local_data()` ✅ |
| ST-10 | Steam.init() 成功 | 待实现 |
| ST-20 | 手柄基本可用 | 待实现(InputMap 映射) |
| ST-30 | Steam Deck 1280×800 | 待测试 |
| ST-40/41 | HMAC 签名排行榜 | `endless_records.gd` ✅ |
| ST-50 | Steam Cloud 存档 | 待实现(profile_manager.gd 已有位置) |
完整清单见 `docs/plan/certification_checklist.md`
---
## 四、构建与发布流程
### 4.1 导出配置(Godot Editor
```
Project → Export
```
| 平台 | 模板 | 注意 |
|:---|:---|:---|
| Windows x64 | Export with Mono runtime | 勾选"Embed PCK"打包为单文件 |
| Steam Deck (Linux) | Linux x86_64 Mono | 须在 SteamOS/Proton 下测试(ST-33|
### 4.2 版本号管理
在 `project.godot` 设置:
```ini
[application]
config/version="1.0.0"
```
发布时打 git tag
```bash
git tag v1.0.0
git push origin v1.0.0
```
并在 `CHANGELOG.md` 记录构建哈希(PRE-02 要求)。
### 4.3 发布前最终清单
```
[ ] 所有 P0 认证项 ✅(见 certification_checklist.md
[ ] Steam 商店页:截图×5、宣传片、描述文字审核通过
[ ] IARC 自评系统完成(Steam 自动申报)
[ ] Wave 1~20 完整通关测试(无崩溃)
[ ] crash_reporter.gd 已为首位 Autoloadproject.godot 验证)
[ ] version = "1.0.0" 已设置,git tag 已推送
```
---
## 五、QA 测试要点
| 场景 | 验证内容 |
|:---|:---|
| 全 20 波通关 | 每波正常生成;结算金币/XP 正确;W8/W20 Boss 阶段触发 |
| 法术排列测试 | MATRIX 邻接加成;CIRCUIT 双路;共鸣触发;LOGIC 门控 |
| 存档恢复 | 关闭游戏→重开→`resume_game()` 恢复法杖+波次 |
| 排行榜防篡改 | 手动修改 endless_records.json → 重启应拒绝载入 |
| 难度三档 | 初学者:受伤乘数×0.7(即减少30%);挑战:W1 生成24敌(20×1.2),Boss HP×1.3 |
| 清除数据 | 设置→清除→确认→所有 user:// 文件消失 |
| 语言切换 | 商店中切语言→HUD/商店/背包/结算全部切换 |
| 性能压测 | 500 敌 + 1500 弹 → 帧率 ≥ 60fps |
| 内存 | Wave 20 Boss 战 → RSS < 512MB |
+134
View File
@@ -0,0 +1,134 @@
# 06 法术编辑器(可视化工具)
> **给策划/开发**:无需写一行代码,在 Godot 编辑器里用可视化表单新增、编辑、删除法术。这是 Godot 的 **EditorPlugin**(等同 Unity 的自定义 EditorWindow)。
---
## 1. 启用插件
插件位于 `res://addons/spell_editor/`,默认已在 `project.godot` 启用。若未显示:
```
项目 → 项目设置 → 插件 → 勾选 "Spell Editor (法术编辑器)"
```
启用后,编辑器**左下停靠区**会出现「法术编辑器」标签页。
> 首次启用或修改插件后,可能需要**重启 Godot 编辑器**让停靠面板出现。
---
## 2. 界面说明
```
✨ 法术编辑器
┌────────────────────────┐
│ 法术列表(ItemList │ ← 所有 data/spells.json 中的法术
│ [ACTION] action_frost… │
│ [MODIFIER] modifier_pi… │
└────────────────────────┘
[➕新增] [⧉复制] [🗑删除]
────────────────────────
ID(唯一) : [______]
类型 : [ACTION ▾]
显示名(tr 键) : [______]
描述(tr 键) : [______]
元素标签(逗号) : [tag:ice]
metaJSON : [______]
[______]
[插入该类型 meta 模板]
────────────────────────
[✓应用到列表] [💾保存文件] [↻重载]
状态:……
```
---
## 3. 新增一个法术(操作流程)
1.**➕新增** → 列表出现 `new_spell`,表单自动填入 ACTION 模板
2.**ID**(如 `action_lightning`,必须全局唯一)
3.**类型**ACTION / MODIFIER / TRIGGER / LOGIC
4.**显示名 / 描述**(建议填 `tr()` 键,如 `SPELL_LIGHTNING_NAME`,见 [04 本地化](04_localization.md)
5.**元素标签**(共鸣用,如 `tag:lightning`;多个用逗号)
6. 编辑 **meta**(JSON)——点「插入该类型 meta 模板」获取起始模板,改数值
7.**✓应用到列表** → 校验并写入内存
8.**💾保存文件** → 写入 `res://data/spells.json`
9. 运行游戏(F5)→ 新法术自动出现在商店池,可购买施放
> **改完不生效?** 法术在游戏启动时由 `SpellRegistry` 加载,需要**重启游戏**(F5 重新运行)。
---
## 4. meta 字段速查(按类型)
### ACTION(弹道)
```json
{ "base_damage": 5.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0,
"damage_type": 0, "apply_status_id": 2, "pierce": 0, "shop_cost": 20 }
```
- `apply_status_id`:命中施加状态(1=燃烧 2=冻结 3=中毒…见 `status_id.gd`),可省略
- `action_kind``"zone"`(地面效果)/ `"summon"`(召唤物),省略=普通弹道
### ACTION(地面效果,毒池等)
```json
{ "action_kind": "zone", "radius": 200.0, "status_id": 3,
"duration": 5.0, "tick_interval": 1.0, "shop_cost": 28 }
```
### ACTION(召唤炮台)
```json
{ "action_kind": "summon", "lifetime": 20.0, "range": 350.0,
"fire_interval": 0.8, "base_damage": 4.0, "speed": 360.0, "shop_cost": 35 }
```
### MODIFIER(修改施法)
```json
{ "damage_add": 10.0, "damage_mult": 1.0, "spread_add": 1,
"multicast": 1, "pierce": 2, "shop_cost": 20 }
```
### TRIGGER(命中子母弹)
```json
{ "base_damage": 3.0, "speed": 250.0, "lifetime": 4.0, "radius": 6.0,
"damage_type": 0, "shop_cost": 30 }
```
### LOGIC(条件/循环)
```json
{ "logic_op": "every_n_shots", "n": 3, "reg": 0, "shop_cost": 24 }
```
`logic_op` 取值:`every_n_shots` / `if_hp_below`(threshold) / `if_enemy_nearby`(range) / `loop`(count)
---
## 5. 数据流(原理)
```
法术编辑器 dock ──保存──▶ res://data/spells.json ──游戏启动加载──▶ SpellRegistry
(编辑器内) (纯数据,可 git 版本管理) (注册到法术 VM)
商店抽取 / 背包装备 / 法术 VM 编译施放
```
- `data/spells.json` 中的法术**优先级最高**,会覆盖 `wand_preset.gd` 里的同 ID 硬编码版本
- 不在 JSON 里的法术仍由硬编码提供(向后兼容,不会丢失现有法术)
- JSON 是纯文本,可纳入 git,多人协作改法术不冲突代码
---
## 6. 扩展方向(给开发)
同一套 EditorPlugin 模式可复制出更多可视化工具:
| 工具 | 数据文件 | 对应硬编码 |
|:---|:---|:---|
| **法术编辑器**(已实现) | `data/spells.json` | `wand_preset.gd` |
| 敌人编辑器(可加) | `data/enemies.json` | `enemy_manager.gd` `_SPEED`/`_ARMOR` |
| 波次编辑器(可加) | `data/waves.json` | `wave_manager.gd` `WAVE_CONFIG` |
| Core 编辑器(可加) | `data/cores.json` | `wand_preset.gd` `make_*_core` |
| 共鸣配方编辑器(可加) | `data/resonance.json` | `spell_evaluator.gd` `_resonance_recipes` |
实现参考 `addons/spell_editor/spell_dock.gd``@tool` 脚本 + `EditorPlugin.add_control_to_dock` + JSON 读写 + 对应 Manager 的 `_load_json_*()` 加载方法。
> **Godot 编辑器扩展能力**`EditorPlugin`(停靠面板/工具栏/底部面板)、`@tool`(脚本在编辑器运行)、`EditorInspectorPlugin`(自定义检视器)、自定义 `Resource``.tres` 自带表单)、`EditorScript`(一次性脚本)。与 Unity 的 EditorWindow/CustomEditor/ScriptableObject 一一对应,且与节点系统集成更紧。
+158
View File
@@ -0,0 +1,158 @@
# 07 游戏设计器(一站式可视化配置工具)
> **核心理念**:策划 / 美术 / 开发**无需写一行代码**,在编辑器的「🎮 游戏设计器」面板里调数值、改玩法、加内容。所有改动保存为 `res://data/*.json`,游戏运行时自动加载。
---
## 1. 打开工具
1. 启用插件(默认已启用):`项目 → 项目设置 → 插件 → 勾选 "Game Designer"`
2. 编辑器**底部面板**点击 **「🎮 游戏设计器」** 标签(与"输出""调试器"并列)
3. 出现 7 个分页:**✨法术 / 👾敌人 / 🌊波次 / 🪄Core / 🔮共鸣 / 🔥状态 / ⚖平衡**
> 🏛 **纯数据驱动架构**:游戏所有内容数据**只存在于 `res://data/*.json`**,代码不含任何硬编码内容副本或回退。这意味着——改 JSON 就是改游戏,数据是唯一权威源。若某个 data 文件缺失,游戏会在输出面板 `push_error` 明确报错(而非静默用旧值)。
> 首次启用或改插件后,可能需**重启 Godot 编辑器**让面板出现。
---
## 2. 通用工作流
```
在面板里改数值/加内容 → 点「💾 保存」 → 按 F5 运行游戏 → 改动生效
```
> ⚠️ **所有改动需重启游戏(F5)生效**——数据在游戏启动时由各 Manager 一次性加载。
---
## 3. 五个分页详解
### ✨ 法术(spells.json
新增/编辑/删除法术,最丰富的玩法配置。详见 [06 法术编辑器](06_spell_editor.md)meta 字段速查表)。
- 列表 → 选中编辑;➕新增 / ⧉复制 / 🗑删除
- 字段:ID / 类型(ACTION/MODIFIER/TRIGGER/LOGIC) / 显示名 / 描述 / 元素标签 / meta(JSON)
- 「插入该类型 meta 模板」一键填入起始参数
### 👾 敌人(enemies.json
6 种敌人原型的数值表格,**直接拖动数字框**调整:
| 列 | 含义 |
|:---|:---|
| 速度 | 移动速度(px/s|
| 护甲 | 每次命中减免的固定伤害 |
| 尺寸 | 渲染直径(px),Boss 设大更醒目 |
| 颜色 | 点色块选颜色(敌人渲染色)|
原型:杂兵 / 快速 / 护甲 / 精英 / 首领(MiniBoss) / 终焉(Boss)
### 🌊 波次(waves.json
**关卡难度曲线编辑器**——逐波配置:
| 列 | 含义 |
|:---|:---|
| 怪数 | 本波杂兵数量 |
| 血量 | 杂兵基础血量 |
| 类型 | 杂兵原型(Basic/Fast/Armored…)|
| 精英 | 带寻路的精英数(W15+)|
| 金币 / XP | 波次结算奖励 |
| Boss | 无 / MiniBoss(4) / Boss(5) |
- 「➕末尾加一波」扩展关卡长度
- 改完点「💾保存 waves.json」
### 🪄 Corecores.json
法杖类型配置(影响法术排列玩法):
| 字段 | 含义 |
|:---|:---|
| 槽位数 | 可装法术数 |
| 拓扑 | LINEAR(线性) / MATRIX(矩阵邻接) / CIRCUIT(电路分叉) |
| CPU 上限 | 单次施法最大执行步数 |
| 施法间隔 | 自动施法冷却(s) |
| 特性 | 无 / PERSISTENT_MEMORY(寄存器跨帧) / … |
| 矩阵行·列 | MATRIX 专用网格尺寸 |
| 电路边 | CIRCUIT 专用,JSON 有向边 `[{"from":0,"to":1}]` |
### 🔮 共鸣(resonance.json
元素组合自动合成配方(隐藏玩法):
- **元素 A + 元素 B**(相邻放置)→ 合成**产物法术**
- 匹配方式:adjacent(相邻) / anywhere_in_deck(任意位置)
- 消耗输入:合成后是否移除原两张法术
- 例:`tag:water` + `tag:lightning``action_plasma_storm`
### 🔥 状态(status_effects.json
DoT/控制/叠层效果:
| 字段 | 含义 |
|:---|:---|
| 状态 ID | 整数(1=燃烧 2=冻结 3=中毒…),与法术 meta 的 `apply_status_id` 对应 |
| 持续时间 / 跳字间隔 | DoT 周期 |
| 叠层模式 | REFRESH(刷新) / INTENSITY(叠层) / INDEPENDENT(独立) |
| 最大层数 | 0=无限 |
| 每跳伤害 | DoT 每次伤害 |
| VFX ID | 触发的特效 |
### ⚖ 平衡(balance.json
全局数值微调:
- **难度乘子表格**:4 行(敌人血量×/玩家受伤×/波次怪数×/Boss血量×) × 3 档(初学者/标准/挑战)
- **Boss 基础血量**MiniBoss(W8) / Final Boss(W20)
调一格数字即可改变整体难度曲线。
---
## 4. 数据流原理
```
🎮 游戏设计器面板 ──保存──▶ res://data/*.json ──游戏 F5 启动加载──▶ 各 Manager
(编辑器内) (纯文本,可 git 版本管理)
spells.json → SpellRegistry
enemies.json → EnemyManager
waves.json → WaveManager
cores.json → WandPreset
balance.json → SettingsManager + WaveManager
```
- JSON 是**唯一权威数据源**——代码不含硬编码内容副本(纯数据驱动)
- 文件缺失/格式错误 → `push_error` 明确报错(不静默,便于发现问题)
- 数据是纯文本,**多人协作改数值不会冲突代码**,可走 git review
### 数据文件清单
| 文件 | 内容 | 加载方 |
|:---|:---|:---|
| `spells.json` | 全部法术定义 | SpellRegistry |
| `enemies.json` | 6 敌人原型数值 | EnemyManager |
| `waves.json` | 20 波关卡曲线 | WaveManager |
| `cores.json` | 法杖 Core 定义 | WandPreset |
| `resonance.json` | 共鸣配方 | SpellEvaluator |
| `status_effects.json` | 状态效果定义 | StatusRegistry |
| `balance.json` | 难度乘子 + Boss 血量 | SettingsManager |
---
## 5. 实操示例
### 例 1:让游戏更简单(策划)
平衡分页 → 「敌人血量×」初学者列改 `0.5` → 保存 → F5。初学者难度敌人血量减半。
### 例 2W10 提前出 Boss(策划)
波次分页 → 第 10 行 Boss 列选 `MiniBoss(4)` → 保存 → F5。
### 例 3:新增一个"冰锥"法术(策划)
法术分页 → ➕新增 → ID 填 `action_ice_spike`,类型 ACTION,「插入模板」后改 `base_damage` 为 8、加 `"apply_status_id": 2`(冻结)→ ✓应用 → 💾保存 → F5。新法术自动进商店。
### 例 4:精英敌人改成紫色更大(美术)
敌人分页 → 精英行 → 尺寸改 `30`,颜色点开选紫色 → 保存 → F5。
---
## 6. 扩展工具(给开发)
新增编辑器分页只需 3 步(参考 `addons/game_designer/`):
1. 写一个 `@tool extends VBoxContainer` 的标签脚本(读写对应 `data/xxx.json`,复用 `designer_ui.gd``spin/line/opt/btn` 辅助)
2.`designer_panel.gd``_init()``_add_tab(...)` 注册
3. 对应 Manager 加 `_load_json_xxx()`,在 `_ready()` 调用
> **Godot 编辑器扩展 = Unity 平替**`EditorPlugin`(面板/工具栏/底部面板) ↔ EditorWindow`@tool` ↔ ExecuteInEditMode`EditorInspectorPlugin` ↔ CustomEditor;自定义 `Resource` ↔ ScriptableObject。
+65
View File
@@ -0,0 +1,65 @@
# 魔法工匠:开发使用手册 (Developer & Designer Handbook)
> **适用人群**:策划、美术、程序、QA
> **引擎版本**Godot 4.6 (Mono)
> **最后更新**2026-06-05
---
## 目录
| 文档 | 适合 | 内容 |
|:---|:---|:---|
| [01 快速上手](01_quickstart.md) | 全员 | 克隆→启动→首次运行 |
| [02 内容创作指南](02_content_authoring.md) | 策划 / 程序 | 法术 / 敌人 / 波次 / Boss / 共鸣 |
| [03 资源替换指南](03_asset_replacement.md) | 美术 / 程序 | 特效 / 精灵 / 音效 / UI 图片 |
| [04 本地化工作流](04_localization.md) | 本地化 / 策划 | `.po` 维护、新增语言、字符串规范 |
| [05 发布标准清单](05_release_guide.md) | 全员 / PM | Steam / Switch 认证、发布流程 |
| [06 法术编辑器](06_spell_editor.md) | 策划 / 开发 | 法术编辑器 meta 字段速查 |
| [07 游戏设计器](07_game_designer.md) | 策划 / 美术 / 开发 | **⭐ 一站式可视化工具**:法术/敌人/波次/Core/平衡全部表单配置,零代码 |
---
## 项目目录快览
```
daihaoRogue/
├── scenes/
│ └── main/combat_s2.tscn ← 主场景(唯一可玩入口)
├── scripts/
│ ├── autoloads/ ← 全局单例(30+ Autoload
│ └── domain/
│ ├── spell_system/ ← 法术虚拟机
│ └── combat/ ← 游戏循环状态机
├── translations/ ← zh_CN / zh_TW / en / ja .po
├── audio/sfx/ ← 音效放这里(目前占位蜂鸣)
├── assets/sprites/ ← 精灵图放这里(目前几何占位)
└── docs/
├── design/ ← 游戏设计文档
├── mechanics/ ← 机制深度文档
├── technical/ ← 架构 / 实现方案
├── plan/ ← 开发计划 / 认证清单
└── handbook/ ← 本手册(你在这里)
```
---
## 关键 Autoload 速查
| Autoload | 职责 | 常用入口 |
|:---|:---|:---|
| `WandPreset` | 法术/Core 内置预设 | `make_spark_bolt()`, `make_wand_basic()` |
| `SpellEvaluator` | 法术虚拟机(编译+执行) | `compile_wand(core, nodes)`, `execute_compiled(compiled, caster_id, pos)` |
| `SpellRegistry` | 法术查询表 | `get_spell("action_fire_bolt")` |
| `WaveManager` | 波次生成/配置 | `WAVE_CONFIG[0..19]` |
| `EnemyManager` | 敌人 SoA 管理 | `spawn_enemy(pos, hp, type)` |
| `BulletManager` | 子弹 SoA 管理 | `spawn_bullet(...)` |
| `VFXManager` | 特效池 | `play("hit_spark", pos)` |
| `AudioManager` | 音效池 | `play("hit", pos)` |
| `SettingsManager` | 难度 / 音量 / 语言 | `difficulty_name()`, `enemy_hp_mult()` |
| `EndlessRecords` | 本地排行榜 | `add_record(wave, elapsed, kills)` |
| `ProfileManager` | A/B 槽局内存档 | `save_run()`, `load_run()` |
| `Locale` | 语言切换 | `set_locale("en")` |
| `StatusManager` | 状态效果(DoT 等) | `apply(entity_id, StatusID.BURN, 2)` |
| `ZoneManager` | 地面效果区域 | `spawn_zone(cx, cy, r, status_id, dur, tick, owner)` |
| `MinionManager` | 召唤物 | `spawn_minion(def_dict, owner_id, pos)` |