extends RefCounted ## Integration tests for FightSim — RaidState + BossDirector wired together. var _test_failures := 0 func _make_players() -> Array[Dictionary]: return [ {"player_id": 0, "name": "Tank", "class_id": "warden", "role": "tank", "level": 5}, {"player_id": 1, "name": "Heal", "class_id": "luminary", "role": "healer", "level": 5}, {"player_id": 2, "name": "DPS1", "class_id": "blade", "role": "dps", "level": 5}, {"player_id": 3, "name": "DPS2", "class_id": "arcanist", "role": "dps", "level": 5} ] func _make_encounter(timeline: Array = [], enrage_type: String = "hard", enrage_time: float = 999.0) -> Dictionary: return { "id": "test_boss", "boss_hp": 1000.0, "enrage_type": enrage_type, "enrage_start_time": enrage_time, "timeline": timeline } ## Builds a sim with 4 players at the given HP and starts the fight. func _make_started_sim(encounter: Dictionary = {}, player_hp: float = 1000.0) -> FightSim: if encounter.is_empty(): encounter = _make_encounter() var sim := FightSim.new(encounter, _make_players()) for p in sim.raid_state.players: p.max_hp = player_hp p.hp = player_hp sim.start() return sim func test_fight_starts_active() -> void: var sim := _make_started_sim() assert_eq(sim.raid_state.fight_phase, RaidState.FightPhase.ACTIVE, "Should be ACTIVE after start") assert_almost(sim.raid_state.boss_hp, 1000.0, "Boss should be at full HP") assert_true(sim.director.is_active(), "Director should be active") func test_time_event_fires_and_records_dialog() -> void: var enc := _make_encounter([{"time": 5.0, "event_type": "dialog", "params": {"line": "hello"}}]) var sim := _make_started_sim(enc) for i in 6: sim.tick(1.0) assert_eq_str(sim.last_dialog_line, "hello", "Dialog line should be recorded") assert_eq(sim.event_log.size(), 1, "Exactly one event should have fired") func test_aoe_damages_all_alive_players() -> void: var enc := _make_encounter([{"time": 2.0, "event_type": "aoe_spread", "params": {"damage": 100.0}}]) var sim := _make_started_sim(enc) for i in 3: sim.tick(1.0) for p in sim.raid_state.players: assert_almost(p.hp, 900.0, "Player %s should have taken the aoe" % p.name) func test_boss_attack_targets_tank_role() -> void: var enc := _make_encounter([{"time": 1.0, "event_type": "boss_attack", "params": {"target": "tank", "damage": 250.0}}]) var sim := _make_started_sim(enc) for i in 2: sim.tick(1.0) assert_almost(sim.raid_state.players[0].hp, 750.0, "Tank should take the hit") assert_almost(sim.raid_state.players[1].hp, 1000.0, "Healer should be untouched") func test_hp_threshold_event_and_phase_shift() -> void: var enc := _make_encounter([ {"hp_threshold": 0.5, "event_type": "phase_shift", "params": {"new_phase": 2}} ]) var sim := _make_started_sim(enc) sim.deal_damage_to_boss(600.0) # 1000 -> 400 = 40%, crosses the 50% threshold assert_eq(sim.raid_state.boss_phase, 2, "Phase should advance via timeline event") var found := false for e in sim.event_log: if e["type"] == "phase_shift": found = true assert_true(found, "phase_shift should be in the event log") func test_boss_kill_completes_and_stops_director() -> void: var sim := _make_started_sim() var killed := sim.deal_damage_to_boss(1000.0) assert_true(killed, "Full HP damage should kill the boss") assert_eq(sim.raid_state.fight_phase, RaidState.FightPhase.COMPLETE, "Fight should be COMPLETE") assert_false(sim.director.is_active(), "Director should stop on kill") var log_size := sim.event_log.size() sim.tick(1.0) # Ticks after completion are no-ops assert_eq(sim.event_log.size(), log_size, "No events should fire after completion") func test_hard_enrage_wipes_at_timestamp() -> void: var enc := _make_encounter([], "hard", 10.0) var sim := _make_started_sim(enc) for i in 12: sim.tick(1.0) assert_eq(sim.raid_state.fight_phase, RaidState.FightPhase.WIPED, "Should wipe at hard enrage") assert_false(sim.director.is_active(), "Director should stop on wipe") func test_soft_enrage_ramps() -> void: var enc := _make_encounter( [{"time": 5.0, "event_type": "enrage_soft_start", "params": {}}], "soft") var sim := _make_started_sim(enc) for i in 8: sim.tick(1.0) assert_true(sim.raid_state.soft_enrage_active, "Soft enrage should be active") assert_true(sim.raid_state.soft_enrage_ramp > 0.0, "Ramp should be increasing") assert_true(sim.get_soft_enrage_multiplier() > 1.0, "Multiplier should exceed 1.0") func test_all_players_dead_wipes() -> void: var enc := _make_encounter([{"time": 1.0, "event_type": "aoe_spread", "params": {"damage": 5000.0}}]) var sim := _make_started_sim(enc) for i in 2: sim.tick(1.0) assert_eq(sim.raid_state.fight_phase, RaidState.FightPhase.WIPED, "One-shot aoe should wipe the raid") func test_revive_flow() -> void: var sim := _make_started_sim() # Kill player 0 manually (bypassing mechanics). sim.raid_state.deal_damage_to_player(0, 5000.0) var p0 = sim.raid_state.players[0] assert_true(p0.is_dead, "Player should be dead") assert_true(sim.raid_state.revive_player(0), "Revive should succeed on a dead player") assert_false(p0.is_dead, "Player should be alive after revive") assert_almost(p0.hp, 300.0, "Should spawn at 30% of max HP") assert_almost(p0.weakness_timer, 90.0, "Weakness debuff should be 90s") for i in 5: sim.tick(1.0) assert_almost(p0.weakness_timer, 85.0, "Weakness should tick down") func test_t1e1_data_file_loads_and_simulates() -> void: var data := FightSim.load_encounter("res://data/bosses/t1e1.json") assert_false(data.is_empty(), "t1e1.json should load") # Give players huge HP so the sim can run to the 480s enrage without wipes. var sim := FightSim.new(data, _make_players()) for p in sim.raid_state.players: p.max_hp = 1000000.0 p.hp = 1000000.0 sim.start() assert_almost(sim.raid_state.boss_hp, 50000.0, "t1e1 boss HP from data file") # Fast-forward one full minute of fight time in 1s ticks. for i in 60: sim.tick(1.0) assert_almost(sim.raid_state.fight_time, 60.0, "Fight time should advance") assert_true(sim.event_log.size() >= 2, "Early aoe_spread events should have fired") # Fast-forward to the 480s soft enrage. for i in 420: sim.tick(1.0) assert_true(sim.raid_state.soft_enrage_active, "Soft enrage should trigger at 480s") assert_eq(sim.raid_state.fight_phase, RaidState.FightPhase.ACTIVE, "Fight should still be active") # --- Assertions --- func assert_true(condition: bool, msg: String = "") -> void: if not condition: _test_failures += 1 push_error("ASSERT FAILED [true]: " + msg) func assert_false(condition: bool, msg: String = "") -> void: if condition: _test_failures += 1 push_error("ASSERT FAILED [false]: " + msg) func assert_eq(value: int, expected: int, msg: String = "") -> void: if value != expected: _test_failures += 1 push_error("ASSERT FAILED [%d != %d]: %s" % [value, expected, msg]) func assert_eq_str(value: String, expected: String, msg: String = "") -> void: if value != expected: _test_failures += 1 push_error("ASSERT FAILED ['%s' != '%s']: %s" % [value, expected, msg]) func assert_almost(value: float, expected: float, msg: String = "", tolerance: float = 0.5) -> void: if abs(value - expected) > tolerance: _test_failures += 1 push_error("ASSERT FAILED [%f != %f (tol %f)]: %s" % [value, expected, tolerance, msg])