## BossManager — Boss 阶段状态机 + 攻击模式调度(Autoload) ## Boss 实体仍在 EnemyManager SoA(渲染/受击/HP);本管理器侧挂运行态、接管移动、驱动招式。 ## 数据驱动:data/bosses.json(每 boss_type 的 move_speed / phases / attack_patterns),自载。 extends Node const BOSSES_JSON: String = "res://data/bosses.json" # boss_type(int) → { move_speed:float, phases:Array[Dictionary] } var _defs: Dictionary = {} # boss_id(int) → 运行态 Dictionary var _active: Dictionary = {} func _ready() -> void: _load_defs() EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed) func _load_defs() -> void: if not FileAccess.file_exists(BOSSES_JSON): push_error("BossManager: 缺失 res://data/bosses.json") return var data = JSON.parse_string(FileAccess.get_file_as_string(BOSSES_JSON)) if not (data is Dictionary): push_error("BossManager: bosses.json 格式错误") return _defs.clear() for k in data: _defs[int(k)] = data[k] ## wave_manager._spawn_boss 生成 SoA Boss 后调用 func register_boss(boss_id: int, boss_type: int, max_hp: float, wave: int) -> void: var def: Dictionary = _defs.get(boss_type, {}) var phases: Array = def.get("phases", []) _active[boss_id] = { "boss_type": boss_type, "max_hp": max_hp, "wave": wave, "move_speed": float(def.get("move_speed", 100.0)), "phase": 0, "phases": phases, "pattern_index": 0, "cooldown": 1.0, # 入场后短暂延迟再起手 "action": {}, # 进行中招式(空=无) "spiral_phase": 0.0, # spiral 招式的累积旋转角 } EnemyManager.register_external_mover(boss_id) if not phases.is_empty(): _emit_phase(boss_id, 0) func _physics_process(delta: float) -> void: if _active.is_empty(): return for boss_id in _active.keys(): if not EnemyManager.has_entity(boss_id): continue var st: Dictionary = _active[boss_id] _check_phase(boss_id, st) _tick_movement(boss_id, st, delta) _tick_action(boss_id, st, delta) # 阶段切换:HP% 跨入更深阶段(phases 按 hp_threshold 降序,取满足阈值的最深项) func _check_phase(boss_id: int, st: Dictionary) -> void: var hp_pct: float = EnemyManager.get_hp_percent(boss_id) var phases: Array = st["phases"] var target: int = int(st["phase"]) for i in phases.size(): if hp_pct <= float(phases[i].get("hp_threshold", 1.0)): target = i if target > int(st["phase"]): st["phase"] = target st["pattern_index"] = 0 st["cooldown"] = 0.5 st["action"] = {} _summon_adds(boss_id, int(phases[target].get("summon_adds", 0))) _emit_phase(boss_id, target) func _emit_phase(boss_id: int, phase: int) -> void: EventBus.emit(EventID.BOSS_PHASE_CHANGED, { "boss_id": boss_id, "boss_type": int(_active[boss_id]["boss_type"]), "phase": phase}) func _summon_adds(boss_id: int, n: int) -> void: if n <= 0: return var boss_pos: Vector2 = EnemyManager.get_pos_by_id(boss_id) for i in n: var ang: float = TAU * float(i) / float(n) var off: Vector2 = Vector2(cos(ang), sin(ang)) * 80.0 EnemyManager.spawn_enemy(boss_pos + off, 40.0, EnemyManager.Type.FAST) func _cur_phase(st: Dictionary) -> Dictionary: var phases: Array = st["phases"] if phases.is_empty(): return {} return phases[clampi(int(st["phase"]), 0, phases.size() - 1)] # 移动:无「接管移动」的招式在进行时,朝玩家缓慢漂移 func _tick_movement(boss_id: int, st: Dictionary, delta: float) -> void: var action: Dictionary = st["action"] if not action.is_empty() and bool(action.get("controls_move", false)): return var pos: Vector2 = EnemyManager.get_pos_by_id(boss_id) var player: Vector2 = PlayerManager.get_position() var spd: float = float(st["move_speed"]) * float(_cur_phase(st).get("move_speed_mult", 1.0)) var to: Vector2 = player - pos if to.length() > 1.0: pos += to.normalized() * spd * delta EnemyManager.set_entity_pos(boss_id, pos) # 招式推进:有进行中招式 → 推进;否则冷却到点 → 起手下一招式 func _tick_action(boss_id: int, st: Dictionary, delta: float) -> void: var action: Dictionary = st["action"] if not action.is_empty(): _advance_action(boss_id, st, action, delta) return st["cooldown"] = float(st["cooldown"]) - delta if float(st["cooldown"]) > 0.0: return var patterns: Array = _cur_phase(st).get("attack_patterns", []) if patterns.is_empty(): st["cooldown"] = 1.0 return var idx: int = int(st["pattern_index"]) % patterns.size() st["pattern_index"] = int(st["pattern_index"]) + 1 _start_pattern(boss_id, st, patterns[idx]) # 招式起手(Task 8 再补 timed 招式分支) func _start_pattern(boss_id: int, st: Dictionary, p: Dictionary) -> void: var kind: String = String(p.get("kind", "ring")) var pos: Vector2 = EnemyManager.get_pos_by_id(boss_id) match kind: "ring", "spread", "aimed", "spiral": _fire_ranged(boss_id, st, kind, p, pos) st["cooldown"] = float(p.get("cooldown", 2.0)) "dash": _telegraph(p, pos) st["action"] = { "kind": "dash", "stage": "windup", "timer": float(p.get("windup", 0.8)), "controls_move": true, "p": p, "target": PlayerManager.get_position(), "dir": Vector2.RIGHT, "hit_cd": 0.0} "charge_up": _telegraph(p, pos) st["action"] = { "kind": "charge_up", "stage": "windup", "timer": float(p.get("windup", 1.0)), "controls_move": true, "p": p} "melee": _telegraph(p, pos) st["action"] = { "kind": "melee", "stage": "windup", "timer": float(p.get("windup", 0.6)), "controls_move": true, "p": p} _: st["cooldown"] = float(p.get("cooldown", 2.0)) ## 瞬时弹幕发射(ring/spread/aimed/spiral) func _fire_ranged(boss_id: int, st: Dictionary, kind: String, p: Dictionary, pos: Vector2) -> void: var count: int = maxi(1, int(p.get("count", 12))) var speed: float = float(p.get("speed", 200.0)) var dmg: float = float(p.get("bullet_damage", 8.0)) var r: float = float(p.get("bullet_radius", 8.0)) var life: float = float(p.get("lifetime", 4.0)) var to_player: Vector2 = PlayerManager.get_position() - pos var aim: float = to_player.angle() if to_player.length() > 0.01 else 0.0 match kind: "ring": for i in count: _spawn_dir(pos, TAU * float(i) / float(count), speed, life, r, dmg) "spread": var half: float = deg_to_rad(float(p.get("half_angle", 30.0))) for i in count: var t: float = 0.0 if count <= 1 else (float(i) / float(count - 1)) * 2.0 - 1.0 _spawn_dir(pos, aim + half * t, speed, life, r, dmg) "aimed": for i in count: _spawn_dir(pos, aim, speed, life, r, dmg) "spiral": var step: float = deg_to_rad(float(p.get("angle_step", 15.0))) var base_a: float = float(st.get("spiral_phase", 0.0)) for i in count: _spawn_dir(pos, base_a + TAU * float(i) / float(count), speed, life, r, dmg) st["spiral_phase"] = base_a + step func _spawn_dir(pos: Vector2, angle: float, speed: float, life: float, r: float, dmg: float) -> void: EnemyBulletManager.spawn_bullet(pos, Vector2(cos(angle), sin(angle)) * speed, life, r, dmg) ## 前摇预警 VFX func _telegraph(p: Dictionary, pos: Vector2) -> void: var vfx: String = String(p.get("telegraph_vfx", "")) if vfx != "": VFXManager.play(vfx, pos, 1.5) ## 招式推进:三段式 windup → active → recover;结束还原并置冷却 func _advance_action(boss_id: int, st: Dictionary, action: Dictionary, delta: float) -> void: action["timer"] = float(action["timer"]) - delta var p: Dictionary = action["p"] var pos: Vector2 = EnemyManager.get_pos_by_id(boss_id) match String(action["kind"]): "dash": match String(action["stage"]): "windup": if float(action["timer"]) <= 0.0: var dir: Vector2 = Vector2(action["target"]) - pos action["dir"] = dir.normalized() if dir.length() > 0.01 else Vector2.RIGHT action["stage"] = "active" action["timer"] = float(p.get("dash_time", 0.5)) "active": pos += Vector2(action["dir"]) * float(p.get("dash_speed", 600.0)) * delta EnemyManager.set_entity_pos(boss_id, pos) action["hit_cd"] = maxf(0.0, float(action["hit_cd"]) - delta) if pos.distance_to(PlayerManager.get_position()) <= float(p.get("contact_radius", 40.0)) and float(action["hit_cd"]) <= 0.0: PlayerStats.take_damage(float(p.get("contact_damage", 15.0))) action["hit_cd"] = float(p.get("contact_hit_cd", 0.4)) if float(action["timer"]) <= 0.0: action["stage"] = "recover" action["timer"] = float(p.get("recover", 0.8)) "recover": if float(action["timer"]) <= 0.0: _end_action(st, p) "charge_up": match String(action["stage"]): "windup": if float(action["timer"]) <= 0.0: _fire_ranged(boss_id, st, String(p.get("release_kind", "ring")), p, pos) action["stage"] = "recover" action["timer"] = float(p.get("recover", 0.8)) "recover": if float(action["timer"]) <= 0.0: _end_action(st, p) "melee": match String(action["stage"]): "windup": if float(action["timer"]) <= 0.0: if pos.distance_to(PlayerManager.get_position()) <= float(p.get("melee_radius", 90.0)): PlayerStats.take_damage(float(p.get("melee_damage", 20.0))) VFXManager.play("hit_spark", pos, 2.0) action["stage"] = "recover" action["timer"] = float(p.get("recover", 0.5)) "recover": if float(action["timer"]) <= 0.0: _end_action(st, p) _: st["action"] = {} func _end_action(st: Dictionary, p: Dictionary) -> void: st["action"] = {} st["cooldown"] = float(p.get("cooldown", 2.0)) func _on_enemy_killed(payload: Dictionary) -> void: var id: int = int(payload.get("entity_id", -1)) if not _active.has(id): return var st: Dictionary = _active[id] EnemyManager.unregister_external_mover(id) EventBus.emit(EventID.BOSS_KILLED, { "boss_id": id, "boss_type": int(st["boss_type"]), "wave": int(st["wave"])}) _active.erase(id) func get_active_boss_count() -> int: return _active.size() func get_phase(boss_id: int) -> int: return int(_active.get(boss_id, {}).get("phase", -1)) func reset() -> void: for id in _active.keys(): EnemyManager.unregister_external_mover(id) _active.clear()