extends GutTest ## The boss format is the thing the MVP has to prove is reusable, so these tests ## are written against [BossDef]/[BossPhase] rather than against the Warden -- ## a second boss should pass them unchanged. var world: SimWorld func before_each() -> void: world = SimWorld.new(3) world.add_player(1, "tester") world.players[1].pos = Vector2(0.0, 200.0) func _spawn_warden() -> SimBoss: return world.spawn_boss(Content.warden()) func test_phase_selection_follows_hp_fraction() -> void: var def := Content.warden() assert_eq(def.phase_index_for(1.0), 0) assert_eq(def.phase_index_for(0.9), 0) assert_eq(def.phase_index_for(0.5), 1) assert_eq(def.phase_index_for(0.3), 2) assert_eq(def.phase_index_for(0.05), 3) func test_boss_advances_phase_when_damaged() -> void: var boss := _spawn_warden() world.step() assert_eq(boss.phase_index, 0) boss.hp = int(float(boss.def.max_hp) * 0.5) world.step() assert_eq(boss.phase_index, 1) assert_eq(boss.phase_tick, 1, "entering a phase restarts its timeline") func test_phase_change_is_announced() -> void: var boss := _spawn_warden() world.step() world.drain_events() boss.hp = int(float(boss.def.max_hp) * 0.3) world.step() var phases := world.events.filter( func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.BOSS_PHASE) assert_eq(phases.size(), 1) assert_eq(int(phases[0]["phase"]), 2) func test_telegraph_holds_fire_before_the_phase_starts() -> void: var boss := _spawn_warden() var telegraph: int = boss.def.phases[0].telegraph_ticks for _i in telegraph: world.step() assert_eq(world.pool.live_count, 0, "the telegraph window must be silent") world.step() assert_gt(world.pool.live_count, 0, "the pattern must start once telegraphed") func test_the_boss_actually_fills_the_arena() -> void: _spawn_warden() for _i in 600: world.step() assert_gt(world.pool.live_count, 40, "a boss phase should keep a real bullet field in the air") func test_armour_multiplier_scales_incoming_damage() -> void: var def := Content.warden() def.phases[0].damage_taken_mult = 0.5 var boss := world.spawn_boss(def) boss.pos = Vector2.ZERO world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 4.0, 60, 100, SimConfig.TEAM_PLAYER, SimConfig.KIND_PLAYER_SHOT) world.step() assert_eq(boss.hp, def.max_hp - 50) func test_boss_death_is_announced_once() -> void: var boss := _spawn_warden() boss.pos = Vector2.ZERO boss.hp = 10 world.pool.spawn(Vector2.ZERO, Vector2.ZERO, 4.0, 60, 100, SimConfig.TEAM_PLAYER, SimConfig.KIND_PLAYER_SHOT) world.step() assert_false(boss.alive) var deaths := world.events.filter( func(e: Dictionary) -> bool: return int(e["t"]) == SimEvent.Type.BOSS_DIED) assert_eq(deaths.size(), 1) ## The point of the format: a boss built from scratch in a test, with no code ## anywhere in the simulation that knows about it, has to work. func test_a_brand_new_boss_needs_no_engine_changes() -> void: var ring := RingEmitter.new() ring.interval = 20 ring.count = 6 var phase := BossPhase.new() phase.name = "Only Phase" phase.enter_at_hp_fraction = 1.0 phase.telegraph_ticks = 0 phase.loop_ticks = 100 phase.emitters = [ring] var def := BossDef.new() def.id = &"test_boss" def.display_name = "Test Boss" def.max_hp = 500 def.radius = 30.0 def.phases = [phase] world.spawn_boss(def) world.step() assert_eq(world.pool.live_count, 6)