diff --git a/docs_dev/specs/2026-07-22-boss-phases-design.md b/docs_dev/specs/2026-07-22-boss-phases-design.md new file mode 100644 index 0000000..7c8326b --- /dev/null +++ b/docs_dev/specs/2026-07-22-boss-phases-design.md @@ -0,0 +1,241 @@ +# Boss 阶段 + 弹幕/位移攻击模式系统 — 设计 + +> **日期**:2026-07-22 +> **切片**:S6(Boss 深度) +> **前置状态**:Mana 系统 + 元进展已完成;Boss 目前只是 EnemyManager SoA 中 `type=4`(Mini/W8)/`type=5`(Final/W20)的厚血实体,唯一"阶段行为"是 66%/33% 跨阈值召唤 FAST 援军(`wave_manager.gd`)。本设计将其升级为数据驱动的多阶段状态机 + 攻击模式调度 + 敌方子弹子系统。 +> **依赖**:EnemyManager、PlayerManager、PlayerStats、EventBus、VFXManager、SettingsManager、SpatialGrid、game_designer 编辑器插件。 + +--- + +## 0. 范围 + +**范围内**: +- 数据驱动的 Boss 多阶段状态机(血量阈值切换)。 +- 攻击模式注册表:瞬时远程弹幕(环/扇/瞄准/旋转)+ 位移/蓄力招式(冲刺/蓄力重击),带三段式生命周期与前摇预警。 +- 新增 `EnemyBulletManager` 敌方子弹子系统(伤害玩家)。 +- 保留现有"进阶召唤援军"手感(作为每阶段可选字段)。 +- 补齐专用事件 `BOSS_PHASE_CHANGED` / `BOSS_KILLED`。 +- `data/bosses.json` + 设计器新增「Boss」分页(字段化、双语、行编辑器)。 +- 敌方子弹独立 MultiMesh 渲染层。 + +**范围外(延后)**: +- 场地机制(冰墙障碍、场地收缩)。 +- 通关演出 / `GAME_CLEARED` 事件 / Boss 死亡序列演出。 +- 玩家无敌帧系统(本轮敌方子弹单次命中即销毁,靠数值调低保证公平)。 +- `.tres` 资源(本工程纯 JSON,不引入)。 +- C# 热路径(沿用 GDScript,参照 R-08)。 + +--- + +## 1. 新增自动加载 `EnemyBulletManager`(敌方子弹) + +平行于 `BulletManager` 的**独立 SoA 池**,不污染玩家子弹热路径(玩家子弹检测 vs 敌人 SpatialGrid;敌方子弹检测 vs 玩家)。 + +- 文件:`scripts/autoloads/enemy_bullet_manager.gd`(+ `.uid`),注册进 `project.godot` autoload。 +- SoA `PackedFloat32Array`,命名常量 `ENEMY_BULLET_STRIDE = 7`: + `[ x, y, vx, vy, lifetime, radius, damage ]`(索引 0..6)。 +- `const MAX_ENEMY_BULLETS: int = 1024`;`_ready` 中 `resize(MAX_ENEMY_BULLETS * ENEMY_BULLET_STRIDE)` + `fill(0.0)`(P6-N3:禁止热循环内 `new()`)。 +- `PLAYER_HIT_RADIUS`:玩家碰撞半径常量(估算 12px,与玩家标记视觉一致)。 +- `_physics_process(delta)`: + - 逐弹积分:`x += vx*dt`、`y += vy*dt`、`lifetime -= dt`。 + - `lifetime <= 0` → `_swap_and_pop`。 + - 否则对玩家碰撞:`d² ≤ (radius + PLAYER_HIT_RADIUS)²`(读 `PlayerManager.get_position()`)→ `PlayerStats.take_damage(damage)`(走难度减伤)+ `VFXManager.play("hit_spark", pos)` + `_swap_and_pop`(单次命中即销毁)。 +- 接口: + - `spawn_bullet(pos: Vector2, vel: Vector2, lifetime: float, radius: float, damage: float) -> int`(满容返回 -1)。 + - `sync_multimesh(mm: MultiMesh)`(SoA 位置批量写入,`visible_instance_count = active`,与 BulletManager 同法)。 + - `get_active_count() -> int`、`reset()`。 +- `combat_manager` 在 `start_game` / `close_shop_and_next_wave` / `_on_player_died` 中 `EnemyBulletManager.reset()`(与 bullet/zone/minion 重置一致)。 + +--- + +## 2. 新增自动加载 `BossManager`(阶段 + 招式侧挂控制器) + +Boss 实体仍留在 EnemyManager SoA(渲染/受击/HP 不变);`BossManager` 用侧 `Dictionary` 按 `boss_id` 存运行态,**并接管 Boss 的移动**。 + +- 文件:`scripts/autoloads/boss_manager.gd`(+ `.uid`),注册进 `project.godot` autoload。 +- 运行态(每个活跃 Boss 一条): + ``` + { + boss_type: int, + max_hp: float, + phase: int, # 当前阶段(0 起) + phase_defs: Array, # 该 boss_type 的阶段配置(来自 bosses.json) + pattern_index: int, # 当前阶段招式轮询索引 + cooldown: float, # 距下次起手的冷却 + action: Dictionary, # 当前进行中的招式状态(见 §3),空 = 无 + } + ``` +- `register_boss(boss_id, boss_type, max_hp)`:`wave_manager._spawn_boss` 生成 SoA Boss 后调用;初始化运行态、置 `phase=0` 并触发 phase 0 进入。 +- `_ready` 载入 `data/bosses.json`(缺失/格式错 `push_error`,纯数据驱动无回退);订阅 `EventID.ENEMY_KILLED`。 +- `_physics_process(delta)`(有活跃 Boss 才进入循环体):对每个 Boss — + 1. `_check_phase_transition`:读 `EnemyManager.get_hp_percent(id)`,按 `phase_defs`(`hp_threshold` 降序)检查是否跨入更深阶段;跨入 → 进阶(重置 `pattern_index/cooldown`、可选召唤援军、发 `BOSS_PHASE_CHANGED`)。 + 2. `_tick_movement`:无进行中位移招式时,Boss 朝玩家缓慢漂移(`move_speed * phase.move_speed_mult`),经 `EnemyManager.set_entity_pos` 写回 SoA。 + 3. `_tick_action`:推进当前招式(§3);无招式且 `cooldown <= 0` → 选下一招式并启动。 +- `_on_enemy_killed`:`killed_id` 是注册 Boss → 发 `BOSS_KILLED {boss_id, boss_type, wave}` 并注销运行态。 +- `reset()`:清空运行态;由 `combat_manager` 在 run 重置点调用。 + +**EnemyManager 改动**: +- 新增 `set_entity_pos(id, pos)`(写 SoA x/y)。 +- `_gd_update_movement` **跳过已注册 Boss 的 slot**(BossManager 全权驱动其移动)——通过 EnemyManager 侧的一个「外部驱动」id 集合(`register_external_mover(id)` / `unregister_external_mover(id)`,类比精英寻路跳过),零额外开销(无 Boss 时集合空)。 + +**wave_manager 改动**: +- `_spawn_boss` 末尾调用 `BossManager.register_boss(...)` + `EnemyManager.register_external_mover(_boss_id)`。 +- **移除** `_update_boss_phase` / `_on_boss_phase_enter`(阶段与召唤逻辑迁入 BossManager)。 +- 保留 `_boss_id` / `_boss_wave` 及 `killed_id == _boss_id` 的**波次完成判定**(不变)。 + +--- + +## 3. 招式模型:带生命周期的动作 + +每阶段 `attack_patterns` 是招式规格列表,BossManager 轮询启动。招式分两类,均含通用字段 `kind` 与 `cooldown`(招式结束后进入的冷却秒数)。 + +### 3.A 瞬时远程弹幕(发一波即结束,Boss 期间继续漂移) + +经 `EnemyBulletManager.spawn_bullet` 从 Boss 位置发射: + +| kind | 行为 | 主要参数 | +| :--- | :--- | :--- | +| `ring` | 绕 360° 均匀 N 弹(开花/环) | `count, speed, bullet_damage, bullet_radius` | +| `spread` | 朝玩家方向 ±`half_angle` 的 N 弹扇形 | `count, half_angle, speed, bullet_damage, bullet_radius` | +| `aimed` | 瞄准玩家的单发/连发 | `count, speed, bullet_damage, bullet_radius`(连发用 burst 间隔可后续扩展) | +| `spiral` | 每次起手角度递增的旋转弹 | `count, speed, angle_step, bullet_damage, bullet_radius` | + +瞬时招式的 `action` 生命周期只占一帧(发射后立即进冷却)。 + +### 3.B 位移 / 蓄力招式(跨多帧,三段式,接管 Boss 移动) + +三段式:`windup 前摇 → active 生效 → recover 后摇`。`action` 状态含 `{ kind, stage, timer, ... }`,逐帧推进。 + +| kind | 行为 | 主要参数 | +| :--- | :--- | :--- | +| `dash`(冲刺) | 前摇:静立 + 播**预警 VFX**(给玩家闪避窗口);生效:以 `dash_speed` 冲向前摇锁定的玩家位置,冲刺中与玩家重叠 → `PlayerStats.take_damage(contact_damage)`(带内部命中冷却 `contact_hit_cd` 防每帧多段);后摇:静立恢复。 | `windup, dash_speed, dash_time, contact_damage, contact_hit_cd, recover, telegraph_vfx` | +| `charge_up`(蓄力) | 前摇:静立蓄力 + 播预警 VFX;生效:释放一次**重击**(大 `ring` 或多重 `spread`,由 `release_kind` + 弹幕参数决定);后摇:静立恢复。 | `windup, release_kind, count, speed, half_angle, bullet_damage, bullet_radius, recover, telegraph_vfx` | + +**公平性约束**:所有位移/蓄力招式**必须**有 `windup` 前摇与 `telegraph_vfx` 预警,否则冲刺不可闪避。 + +### 招式注册表实现 + +`BossManager` 内置 `pattern_id/kind → 处理逻辑`(行为在代码,调参在 JSON,类比 status/spell)。瞬时招式为「发射函数」;位移/蓄力招式为「阶段推进器」。不引入独立 Resource / 注册 autoload,逻辑内聚在 `BossManager`(招式数量少,MVP 阶段)。 + +--- + +## 4. 数据与设计器 + +### 4.1 `data/bosses.json` + +```json +{ + "4": { + "move_speed": 120.0, + "phases": [ + { + "hp_threshold": 1.0, + "move_speed_mult": 1.0, + "summon_adds": 0, + "attack_patterns": [ + { "kind": "spread", "count": 5, "half_angle": 40.0, "speed": 220.0, "bullet_damage": 8.0, "bullet_radius": 8.0, "cooldown": 2.5 }, + { "kind": "dash", "windup": 0.8, "dash_speed": 600.0, "dash_time": 0.5, "contact_damage": 15.0, "contact_hit_cd": 0.4, "recover": 0.8, "telegraph_vfx": "boss_telegraph", "cooldown": 3.0 } + ] + }, + { + "hp_threshold": 0.66, + "move_speed_mult": 1.2, + "summon_adds": 6, + "attack_patterns": [ ... ] + }, + { + "hp_threshold": 0.33, + "move_speed_mult": 1.5, + "summon_adds": 8, + "attack_patterns": [ ... ] + } + ] + }, + "5": { "move_speed": 80.0, "phases": [ ... ] } +} +``` + +- 顶层键为 `boss_type`("4"=Mini、"5"=Final),与 `waves.json` 的 `boss` 字段、`balance.json` 的 `boss_hp` 键一致。 +- `phases` 按 `hp_threshold` 降序(1.0 → …)。 +- Boss 基础血量仍取 `balance.json` 的 `boss_hp`(不迁移,避免破坏现有平衡数据);`bosses.json` 只负责阶段/招式/移动。 +- `SettingsManager` 加 getter(如 `get_boss_phase_config(boss_type) -> Array`);`BossManager._ready` 经其读取。缺失/格式错 `push_error`,无静默回退。 + +### 4.2 设计器「Boss」分页 + +按 CLAUDE.md「新增分页 3 步」: +1. `addons/game_designer/boss_tab.gd`(`@tool extends VBoxContainer`,复用 `designer_ui.gd` 的 `spin/line/opt/btn/header/set_status/load_json/save_json`;在 `_ready()` 构建 UI,非 `_init()`)。 +2. `designer_panel.gd` 的 `_init()` 里 `_add_tab("Boss", ...)` 注册。 +3. Manager `_load_json_bosses()` 于 `_ready()` 载入。 + +字段化要求(遵守插件规范): +- 所有枚举/选项**中英双语、中文在前**(`kind`:`环 ring` / `扇形 spread` / `瞄准 aimed` / `旋转 spiral` / `冲刺 dash` / `蓄力 charge_up`)。 +- `OptionButton` 存 key(如 `ring`),**不**把双语显示串写进 JSON(显示标签数组与存储值数组分离,index 对齐)。 +- 数值用 `SpinBox`,撑满宽度(`SIZE_EXPAND_FILL`,工厂已内置)。 +- 可变长的阶段列表 / 招式列表用增删行 UI(➕加 / 🗑删);如结构过深,MVP 可先做「按 boss_type 选择 → 阶段列表 → 每阶段招式行编辑」的分层表单。**禁止手写 JSON 文本框**。 +- 数据往返一致、防丢键;可选字段为默认值时可省略。 + +--- + +## 5. 事件 + +`event_ids.gd` 从 29 起新增(同步更新 `docs/technical/implementation_plan.md §2.1` 事件目录): + +| ID | 常量 | Payload | 订阅方 | +| :-- | :-- | :-- | :-- | +| 29 | `BOSS_PHASE_CHANGED` | `{ boss_id, boss_type, phase }` | HUD(阶段提示)、AudioManager(可选 BGM)| +| 30 | `BOSS_KILLED` | `{ boss_id, boss_type, wave }` | WaveManager/成就(可选)| + +`GAME_CLEARED` 延后(范围外)。沿用现有 `BOSS_SPAWNED = 19`。 + +--- + +## 6. 渲染 + +`combat_s2.gd`: +- `_setup_world_view` 增第三个 `MultiMeshInstance2D` + `_enemy_bullet_mm`(`instance_count = EnemyBulletManager.MAX_ENEMY_BULLETS`),modulate 用**红/紫**区分玩家子弹(黄)。 +- `_process` 增 `EnemyBulletManager.sync_multimesh(_enemy_bullet_mm)`。 + +--- + +## 7. 跨切片约束(P6-N)遵守 + +- `ENEMY_BULLET_STRIDE` 命名常量,禁裸整数。 +- 热循环(integrate/collision)不 `new()`;SoA 预分配;`_swap_and_pop` 就地搬运。 +- VFX `one_shot`(P6-N72)。 +- EventID 从常量取,禁硬编码整数;顺序递增(29/30)。 +- CoreFeatureTag / StatusID 等仍用枚举,不引入裸串。 +- 纯数据驱动:`bosses.json` 缺失/格式错 `push_error`,无硬编码回退副本。 + +--- + +## 8. 验证 + +- godot-mcp-pro `validate_script` 校验新脚本语法。 +- `play_scene` 跳到 W8/W20(或直接 `WaveManager` 起 boss 波),`execute_game_script` 断言: + - 敌方子弹生成并使 `PlayerStats.hp` 下降(`ring`/`spread`/`aimed`/`spiral` 各验一次)。 + - `dash` 前摇静立→冲刺位移→接触扣血(带命中冷却,不每帧多段);`charge_up` 前摇→释放弹幕。 + - HP 跨 66%/33%(或配置阈值)发 `BOSS_PHASE_CHANGED`;进阶召唤援军数正确。 + - Boss 死亡发 `BOSS_KILLED`,波次照常完成。 +- 设计器 Boss 分页对真实 `bosses.json` 跑回读往返(`execute_editor_script` 断言 `FAILS=0`);改 dock GDScript 后需 `EditorInterface.restart_editor(true)` 清缓存再测。 + +--- + +## 9. 目录 / 文件清单 + +``` +scripts/autoloads/ + enemy_bullet_manager.gd (+ .uid) # 新增 autoload + boss_manager.gd (+ .uid) # 新增 autoload +scripts/autoloads/enemy_manager.gd # +set_entity_pos / +register_external_mover / _gd_update_movement 跳过 +scripts/autoloads/wave_manager.gd # _spawn_boss 注册 BossManager;移除旧 _update_boss_phase/_on_boss_phase_enter +scripts/autoloads/event_ids.gd # +BOSS_PHASE_CHANGED=29 / +BOSS_KILLED=30 +scripts/autoloads/settings_manager.gd# +get_boss_phase_config 等 getter +scripts/domain/combat/combat_manager.gd # reset EnemyBulletManager/BossManager +scenes/main/combat_s2.gd # 敌方子弹 MMI + sync +data/bosses.json # 新增数据 +addons/game_designer/boss_tab.gd (+ .uid) # 新增设计器分页 +addons/game_designer/designer_panel.gd # _add_tab 注册 +project.godot # 注册两个 autoload + 分页无关 +docs/technical/implementation_plan.md# §2.1 事件目录 +29/+30 +docs/technical/boss_design.md # 更新实现现状注记(改文档追认) +```