extends GutTest # ============================================================================= # Test: Gauntlet Candy Bubble System (v2) [Gauntlet #082] # Covers bubble-specific scoring components, phase budgets, anti-stacking, # the 3x3 blast footprint, and the grow→explode lifecycle. # Runs headless (no multiplayer peer): elapsed_time = 0 so the final-30s window # is inactive unless a test sets it directly. # ============================================================================= const GauntletManager = preload("res://scripts/managers/gauntlet_manager.gd") const GridMapMock = preload("res://tests/helpers/gridmap_mock.gd") var manager var main_mock: Node var gridmap_mock: Node func before_each(): main_mock = Node.new() add_child(main_mock) gridmap_mock = GridMapMock.new() gridmap_mock.name = "EnhancedGridMap" main_mock.add_child(gridmap_mock) manager = GauntletManager.new() main_mock.add_child(manager) manager.initialize(main_mock, gridmap_mock) manager.current_phase = 0 func after_each(): if main_mock: main_mock.queue_free() # Run a callable with the multiplayer peer detached so manager code takes the # local (non-rpc) sync path — deterministic for headless lifecycle tests. func _without_peer(fn: Callable) -> void: var saved = multiplayer.multiplayer_peer multiplayer.multiplayer_peer = null fn.call() multiplayer.multiplayer_peer = saved # ============================================================================= # Phase budget # ============================================================================= func test_bubble_budget_per_phase(): manager.current_phase = 0 assert_eq(manager._bubble_budget_for_phase(), 0, "Phase 1 → 0 bubbles") manager.current_phase = 1 assert_eq(manager._bubble_budget_for_phase(), 2, "Phase 2 → 2 bubbles") manager.current_phase = 2 assert_eq(manager._bubble_budget_for_phase(), 3, "Phase 3 → 3 bubbles") func test_phase_change_resets_counter(): manager.bubbles_this_phase = 2 manager._start_phase(manager.Phase.SURVIVAL_ENDGAME) assert_eq(manager.bubbles_this_phase, 0, "Per-phase bubble count resets on phase change") # ============================================================================= # Blast footprint (3x3, clipped) # ============================================================================= func test_blast_is_3x3_in_open_area(): var cells = manager._bubble_blast_cells(Vector2i(14, 14)) assert_eq(cells.size(), 9, "Open-area bubble blast is 3x3 = 9 cells") func test_blast_clips_npc_zone(): # Center adjacent to the NPC zone (9,9) clips blocked cells out. var cells = manager._bubble_blast_cells(Vector2i(7, 9)) assert_true(cells.size() < 9, "Blast near NPC zone is clipped below 9") for c in cells: assert_false(manager._is_npc_zone(c), "No blast cell lands in NPC zone") # ============================================================================= # Scoring components # ============================================================================= func test_bubble_camping_thresholds(): var region: Vector2i = manager._region_of(Vector2i(8, 8)) manager._camp_tracking[1] = {"region": region, "time": 6.0} assert_eq(manager._bubble_score_camping(Vector2i(8, 8)), 40.0, ">5s = +40") manager._camp_tracking[1]["time"] = 9.0 assert_eq(manager._bubble_score_camping(Vector2i(8, 8)), 60.0, ">8s = +60") func test_bubble_player_cluster(): var players = [Vector2i(5, 5), Vector2i(6, 6)] assert_eq(manager._bubble_score_player_cluster(Vector2i(5, 6), players), 20.0, "2 nearby players = +20") assert_eq(manager._bubble_score_player_cluster(Vector2i(15, 15), players), 0.0, "No nearby players = 0") func test_bubble_direct_hit_penalty(): var players = [Vector2i(5, 5)] assert_eq(manager._bubble_score_direct_hit(Vector2i(5, 5), players), -60.0, "Directly under player = -60") assert_eq(manager._bubble_score_direct_hit(Vector2i(8, 8), players), 0.0, "Not under player = 0") func test_bubble_recent_penalty(): manager.recent_bubble_positions = [Vector2i(14, 14)] assert_eq(manager._bubble_score_recent(Vector2i(11, 11)), -50.0, "Near recent bubble = -50") assert_eq(manager._bubble_score_recent(Vector2i(2, 2)), 0.0, "Far from recent bubble = 0") func test_bubble_untouched_area(): # Open arena around (10,10) → large reachable region → +30. assert_eq(manager._bubble_score_untouched_area(Vector2i(14, 14)), 30.0, "Large untouched area = +30") func test_bubble_full_score_is_finite(): var s = manager._calculate_bubble_score(Vector2i(8, 8), []) assert_true(is_finite(s), "Full bubble score is finite") # ============================================================================= # Spawn lifecycle # ============================================================================= func test_spawn_bubble_marks_growing_cells(): _without_peer(func(): manager._spawn_bubble(Vector2i(14, 14)) ) assert_eq(manager.bubbles_this_phase, 1, "Phase counter increments") assert_eq(manager.bubbles_total, 1, "Round counter increments") assert_eq(manager.active_bubbles.size(), 1, "One active bubble") assert_true(manager.bubble_cells.has(Vector2i(14, 14)), "Center marked BUBBLE_GROWING") assert_eq(manager.cell_state(Vector2i(14, 14)), manager.CellState.BUBBLE_GROWING, "cell_state reports BUBBLE_GROWING") func test_spawn_bubble_records_recent_position(): _without_peer(func(): manager._spawn_bubble(Vector2i(14, 14)) ) assert_true(manager.recent_bubble_positions.has(Vector2i(14, 14)), "Center remembered for anti-stacking") func test_recent_positions_capped(): _without_peer(func(): for i in range(manager.BUBBLE_RECENT_MEMORY + 3): manager._spawn_bubble(Vector2i(2 + i, 15)) ) assert_eq(manager.recent_bubble_positions.size(), manager.BUBBLE_RECENT_MEMORY, "Recent memory capped") # ============================================================================= # Explosion # ============================================================================= func test_update_bubbles_explodes_after_grow_duration(): _without_peer(func(): manager._spawn_bubble(Vector2i(14, 14)) manager._update_bubbles(manager.BUBBLE_GROW_DURATION + 0.1) ) assert_eq(manager.active_bubbles.size(), 0, "Bubble removed after exploding") assert_true(manager.sticky_cells.has(Vector2i(14, 14)), "Center became sticky") assert_false(manager.bubble_cells.has(Vector2i(14, 14)), "BUBBLE_GROWING cleared on explode") func test_update_bubbles_waits_for_timer(): _without_peer(func(): manager._spawn_bubble(Vector2i(14, 14)) manager._update_bubbles(manager.BUBBLE_GROW_DURATION * 0.5) ) assert_eq(manager.active_bubbles.size(), 1, "Bubble still growing before timer elapses") assert_false(manager.sticky_cells.has(Vector2i(14, 14)), "No sticky yet mid-grow") func test_explode_creates_3x3_sticky(): _without_peer(func(): manager._explode_bubble(Vector2i(14, 14), manager._bubble_blast_cells(Vector2i(14, 14))) ) var sticky_in_blast := 0 for dx in range(-1, 2): for dz in range(-1, 2): if manager.sticky_cells.has(Vector2i(14 + dx, 14 + dz)): sticky_in_blast += 1 assert_eq(sticky_in_blast, 9, "Explosion creates a full 3x3 sticky area")