Files
spellforge/scripts/autoloads/wave_manager.gd
T
2026-07-20 10:56:52 +08:00

175 lines
6.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## WaveManager — 波次管理(Autoload: WaveManager
## S6 内容:Wave 1~20 完整配置(敌人原型/精英寻路/Mini Boss W8/Final Boss W20
## 设计参考:game_design.md §3.3W1-5 数值 / W6-10 机制 / W11-20 弹幕)、§4.5 Boss
extends Node
enum WaveState { IDLE = 0, BATTLE = 1, COMPLETE = 2 }
var current_wave: int = 0
var wave_state: int = WaveState.IDLE
var enemies_alive: int = 0
var _wave_start_time: float = 0.0
const ARENA_RADIUS: float = 700.0
const MAX_WAVE: int = 20
## Boss 追踪
var _boss_id: int = -1
var _boss_max_hp: float = 0.0
var _boss_phase: int = 0
var _boss_wave: bool = false
## 波次配置表:纯数据驱动,唯一来源 data/waves.json(游戏设计器「波次」面板维护)
## 字段:count, hp, type, elite, gold, xp, boss(-1=无 / 4=MiniBoss / 5=Boss)
var WAVE_CONFIG: Array = []
const WAVES_JSON: String = "res://data/waves.json"
func _ready() -> void:
EventBus.subscribe(EventID.ENEMY_KILLED, _on_enemy_killed)
_load_json_waves()
## 数据驱动:从 data/waves.json 加载波次表
func _load_json_waves() -> void:
if not FileAccess.file_exists(WAVES_JSON):
push_error("WaveManager: 缺失 res://data/waves.json(无波次配置)")
return
var data = JSON.parse_string(FileAccess.get_file_as_string(WAVES_JSON))
if data is Array and not data.is_empty():
WAVE_CONFIG = data
else:
push_error("WaveManager: waves.json 格式错误(应为非空数组)")
func _physics_process(_delta: float) -> void:
if _boss_wave and _boss_id >= 0:
_update_boss_phase()
## 开始指定波次(由 CombatManager 调用)
func start_wave(wave_num: int) -> void:
current_wave = wave_num
wave_state = WaveState.BATTLE
_wave_start_time = Time.get_ticks_msec() / 1000.0
_boss_id = -1
_boss_phase = 0
_boss_wave = false
var cfg: Dictionary = _get_wave_config(wave_num)
enemies_alive = int(cfg["count"])
_spawn_wave_enemies(cfg)
if int(cfg.get("boss", -1)) >= 0:
_spawn_boss(int(cfg["boss"]), wave_num)
EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": "SHOP", "to": "BATTLE", "wave": wave_num})
print("[WaveManager] Wave %d 开始:%d 杂兵 + %d 精英%s" % [
wave_num, int(cfg["count"]), int(cfg.get("elite", 0)),
("Boss!)" if int(cfg.get("boss", -1)) >= 0 else "")])
func _spawn_wave_enemies(cfg: Dictionary) -> void:
# 难度乘子:怪物数量(挑战×1.2)、怪物血量(初学者×0.7)
var count: int = int(round(int(cfg["count"]) * SettingsManager.wave_count_mult()))
var hp: float = float(cfg["hp"]) * SettingsManager.enemy_hp_mult()
var etype: int = int(cfg.get("type", 0))
# enemies_alive 已按 cfg.count 初始化,按难度修正差额
enemies_alive += count - int(cfg["count"])
for _i in count:
EnemyManager.spawn_enemy(_ring_pos(), hp, etype)
# W15+ 精英:带 NavigationAgent2D 寻路(ADR-A4),更高血量
var elites: int = int(cfg.get("elite", 0))
for _e in elites:
EnemyManager.spawn_enemy(_ring_pos(), hp * 2.5, EnemyManager.Type.ELITE, true)
enemies_alive += elites # 精英计入存活
func _spawn_boss(boss_type: int, wave_num: int) -> void:
# Boss 基础血量由 SettingsManagerbalance.json 可调)提供 × 难度乘子
var hp: float = SettingsManager.get_boss_base_hp(boss_type) * SettingsManager.boss_hp_mult()
_boss_max_hp = hp
# Boss 在玩家上方稍远处入场
_boss_id = EnemyManager.spawn_enemy(Vector2(0.0, -ARENA_RADIUS * 0.8), hp, boss_type)
_boss_wave = true
_boss_phase = 1
enemies_alive += 1
EventBus.emit(EventID.BOSS_SPAWNED, {
"boss_id": _boss_id, "boss_type": boss_type, "wave": wave_num, "max_hp": hp})
print("[WaveManager] Boss 入场!type=%d hp=%.0f (wave %d)" % [boss_type, hp, wave_num])
## Boss 阶段:HP 跨越阈值时进阶并召唤援军(简化 AI,game_design §4.5
func _update_boss_phase() -> void:
if not EnemyManager.has_entity(_boss_id):
return
var frac: float = EnemyManager.get_hp_percent(_boss_id)
var target_phase: int = 1
if frac <= 0.33:
target_phase = 3
elif frac <= 0.66:
target_phase = 2
if target_phase > _boss_phase:
_boss_phase = target_phase
_on_boss_phase_enter(target_phase)
func _on_boss_phase_enter(phase: int) -> void:
# 进阶召唤一批援军施压(Phase 2/3)
var add_count: int = 4 + phase * 2
var boss_pos: Vector2 = EnemyManager.get_pos_by_id(_boss_id)
for _i in add_count:
var off := Vector2(randf_range(-120, 120), randf_range(-120, 120))
var aid: int = EnemyManager.spawn_enemy(boss_pos + off, 40.0, EnemyManager.Type.FAST)
if aid >= 0:
enemies_alive += 1
EventBus.emit(EventID.GAME_STATE_CHANGED, {"from": "BATTLE", "to": "BOSS_PHASE_%d" % phase})
print("[WaveManager] Boss 进入 Phase %d,召唤 %d 援军" % [phase, add_count])
func _ring_pos() -> Vector2:
var angle: float = randf() * TAU
var radius: float = randf_range(250.0, ARENA_RADIUS)
return Vector2(cos(angle) * radius, sin(angle) * radius)
func _on_enemy_killed(payload: Dictionary) -> void:
if wave_state != WaveState.BATTLE:
return
var killed_id: int = int(payload.get("entity_id", -1))
# Boss 波:Boss 死亡即通关本波(援军为附赠)
if _boss_wave and killed_id == _boss_id:
_complete_wave()
return
enemies_alive -= 1
if enemies_alive <= 0 and not _boss_wave:
_complete_wave()
func _complete_wave() -> void:
if wave_state != WaveState.BATTLE:
return
wave_state = WaveState.COMPLETE
_boss_wave = false
_boss_id = -1
var cfg: Dictionary = _get_wave_config(current_wave)
PlayerStats.gain_gold(int(cfg["gold"]))
PlayerStats.gain_xp(int(cfg["xp"]))
var elapsed: float = Time.get_ticks_msec() / 1000.0 - _wave_start_time
EventBus.emit(EventID.WAVE_COMPLETE, {"wave": current_wave, "elapsed_sec": elapsed})
print("[WaveManager] Wave %d 完成!奖励: %dG, %d XP" % [current_wave, int(cfg["gold"]), int(cfg["xp"])])
func _get_wave_config(wave_num: int) -> Dictionary:
var idx: int = clamp(wave_num - 1, 0, WAVE_CONFIG.size() - 1)
var base: Dictionary = WAVE_CONFIG[idx].duplicate()
# 超过 W20Endless):数值膨胀
if wave_num > MAX_WAVE:
var extra: int = wave_num - MAX_WAVE
base["count"] = int(base["count"]) + extra * 6
base["hp"] = float(base["hp"]) * pow(1.12, extra)
base["gold"] = int(base["gold"]) + extra * 8
base["xp"] = int(base["xp"]) + extra * 12
base["boss"] = -1 # Endless 不重复终 Boss
return base
func get_wave_count(wave_num: int) -> int:
return int(_get_wave_config(wave_num)["count"])
func is_boss_wave(wave_num: int) -> bool:
return int(_get_wave_config(wave_num).get("boss", -1)) >= 0
func reset() -> void:
current_wave = 0
wave_state = WaveState.IDLE
enemies_alive = 0
_boss_id = -1
_boss_phase = 0
_boss_wave = false