Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
4.5 KiB
GDScript
152 lines
4.5 KiB
GDScript
extends Node
|
|
## Single authoritative phase coordinator; the presentation never ticks combat.
|
|
|
|
signal updated
|
|
signal battle_started
|
|
signal wave_settled(reports: Array)
|
|
|
|
const AI_SCRIPT = preload("res://scripts/ai/simple_ai.gd")
|
|
const BUILD_FIRST := 45.0
|
|
const BUILD_DURATION := 30.0
|
|
const RESULTS_DURATION := 4.0
|
|
|
|
var battle: Node3D
|
|
var remaining: float = 0.0
|
|
var ai = AI_SCRIPT.new()
|
|
var _public_snapshots: Array = [{}, {}]
|
|
var _wave_core_damage: Array = [0, 0]
|
|
|
|
|
|
func attach_battle(controller: Node3D) -> void:
|
|
battle = controller
|
|
|
|
|
|
func start_match() -> void:
|
|
if not is_instance_valid(battle):
|
|
push_error("WaveManager needs an attached BattleController.")
|
|
return
|
|
GameState.reset_match()
|
|
EconomyManager.reset_match()
|
|
_public_snapshots = [EconomyManager.get_team(0), EconomyManager.get_team(1)]
|
|
GameState.wave = 1
|
|
_open_build()
|
|
|
|
|
|
func _open_build() -> void:
|
|
EconomyManager.open_build(GameState.wave)
|
|
GameState.set_phase(GameState.Phase.BUILD)
|
|
remaining = BUILD_FIRST if GameState.wave == 1 else BUILD_DURATION
|
|
# The rival uses the previous completed formation, never a live player queue.
|
|
ai.plan(1, GameState.wave, _public_snapshots[0], GameState.last_reports[1], GameState.ai_level)
|
|
battle.show_formation(_rosters())
|
|
updated.emit()
|
|
|
|
|
|
func _rosters() -> Array:
|
|
return [EconomyManager.get_team(0).roster, EconomyManager.get_team(1).roster]
|
|
|
|
|
|
func ready_for_battle() -> void:
|
|
if GameState.phase == GameState.Phase.RESULTS:
|
|
_next_wave()
|
|
return
|
|
if GameState.phase != GameState.Phase.BUILD:
|
|
return
|
|
_public_snapshots = [EconomyManager.get_team(0), EconomyManager.get_team(1)]
|
|
var outgoing: Array = EconomyManager.freeze(GameState.wave)
|
|
var attackers: Array = [
|
|
DataCatalog.get_attackers(GameState.wave),
|
|
DataCatalog.get_attackers(GameState.wave)
|
|
]
|
|
for sender in range(2):
|
|
for send_id in outgoing[sender]:
|
|
attackers[1 - sender].append(DataCatalog.get_send(str(send_id), GameState.wave))
|
|
_wave_core_damage = [0, 0]
|
|
battle.begin_battle(_rosters(), attackers, GameState.wave)
|
|
remaining = 90.0
|
|
GameState.set_phase(GameState.Phase.COMBAT)
|
|
battle_started.emit()
|
|
updated.emit()
|
|
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if GameState.paused:
|
|
return
|
|
# 2x repeats the same fixed step rather than doubling its integration delta.
|
|
for _substep in range(GameState.speed):
|
|
advance(delta)
|
|
|
|
|
|
func advance(delta: float) -> void:
|
|
if delta <= 0.0 or GameState.paused:
|
|
return
|
|
match GameState.phase:
|
|
GameState.Phase.BUILD:
|
|
if not GameState.timed_build:
|
|
return
|
|
remaining = maxf(0.0, remaining - delta)
|
|
if remaining <= 0.0:
|
|
ready_for_battle()
|
|
GameState.Phase.COMBAT:
|
|
var report: Dictionary = battle.step(delta)
|
|
remaining = maxf(0.0, 90.0 - float(report.elapsed))
|
|
var leak_damage: Array = report.leak_damage
|
|
if leak_damage[0] > 0 or leak_damage[1] > 0:
|
|
for team in range(2):
|
|
_wave_core_damage[team] += int(leak_damage[team])
|
|
GameState.apply_core_damage(leak_damage)
|
|
for lane: int in range(2):
|
|
if leak_damage[lane] > 0:
|
|
Soundscape.play("leak", lane)
|
|
var winner: int = GameState.decide_winner(false)
|
|
if winner != GameState.ONGOING:
|
|
_finish_match(winner)
|
|
elif report.finished:
|
|
_settle(report)
|
|
GameState.Phase.RESULTS:
|
|
if not GameState.timed_build:
|
|
return
|
|
remaining = maxf(0.0, remaining - delta)
|
|
if remaining <= 0.0:
|
|
_next_wave()
|
|
|
|
|
|
func _settle(combat_report: Dictionary) -> void:
|
|
var reports: Array = []
|
|
for team in range(2):
|
|
var report: Dictionary = EconomyManager.settle(
|
|
team, GameState.wave, int(combat_report.bounties[team]))
|
|
report["leaks"] = combat_report.leaks[team]
|
|
report["core_damage"] = _wave_core_damage[team]
|
|
report["combat_seconds"] = combat_report.elapsed
|
|
report["natural_count"] = DataCatalog.get_attackers(GameState.wave).size()
|
|
report["natural_leaks"] = combat_report.get("natural_leaks", combat_report.leaks)[team]
|
|
report["front_lifetime"] = combat_report.get("front_lifetime", [90.0, 90.0])[team]
|
|
reports.append(report)
|
|
GameState.last_reports = reports
|
|
wave_settled.emit(reports)
|
|
if GameState.wave >= DataCatalog.wave_count():
|
|
_finish_match(GameState.decide_winner(true))
|
|
else:
|
|
remaining = RESULTS_DURATION
|
|
GameState.set_phase(GameState.Phase.RESULTS)
|
|
updated.emit()
|
|
|
|
|
|
func _next_wave() -> void:
|
|
if GameState.phase != GameState.Phase.RESULTS:
|
|
return
|
|
GameState.wave += 1
|
|
_open_build()
|
|
|
|
|
|
func _finish_match(winner: int) -> void:
|
|
GameState.outcome = winner
|
|
GameState.paused = false
|
|
remaining = 0.0
|
|
GameState.set_phase(GameState.Phase.GAME_OVER)
|
|
updated.emit()
|
|
|
|
|
|
func rival_snapshot() -> Dictionary:
|
|
return _public_snapshots[1].duplicate(true)
|