extends RefCounted ## Bomb system tests — constants, state transitions, timing logic. ## ## Tests the pure constant checking and state machine logic of the ## Bomb system. Full plant/defuse timing requires NetworkTime.tick, ## so those are tested via integration tests instead. # BombState enum values (mirrors Bomb.BombState) enum BombState { IDLE = 0, CARRIED = 1, PLANTING = 2, DROPPED = 3, PLANTED = 4, DEFUSING = 5, EXPLODED = 6, DEFUSED = 7 } # Default timing constants (mirrors Bomb @export defaults) const PLANT_TIME := 3.0 const DEFUSE_TIME := 10.0 const DEFUSE_KIT_TIME := 5.0 const BOMB_TIMER := 40.0 func test_bomb_state_enum_values() -> String: var e := "" if int(BombState.IDLE) != 0: e += " IDLE should be 0" if int(BombState.CARRIED) != 1: e += " CARRIED should be 1" if int(BombState.PLANTING) != 2: e += " PLANTING should be 2" if int(BombState.DROPPED) != 3: e += " DROPPED should be 3" if int(BombState.PLANTED) != 4: e += " PLANTED should be 4" if int(BombState.DEFUSING) != 5: e += " DEFUSING should be 5" if int(BombState.EXPLODED) != 6: e += " EXPLODED should be 6" if int(BombState.DEFUSED) != 7: e += " DEFUSED should be 7" return e func test_plant_time_constant() -> String: if PLANT_TIME != 3.0: return "Plant time should be 3.0s, got %.1f" % PLANT_TIME return "" func test_defuse_time_constant() -> String: if DEFUSE_TIME != 10.0: return "Default defuse time should be 10.0s, got %.1f" % DEFUSE_TIME return "" func test_defuse_kit_time_constant() -> String: if DEFUSE_KIT_TIME != 5.0: return "Defuse kit time should be 5.0s, got %.1f" % DEFUSE_KIT_TIME return "" func test_bomb_timer_constant() -> String: if BOMB_TIMER != 40.0: return "Bomb timer should be 40.0s, got %.1f" % BOMB_TIMER return "" func test_kit_defuse_is_half_price() -> String: # Defuse kit halves the time: 10.0 → 5.0 if DEFUSE_KIT_TIME * 2 != DEFUSE_TIME: return "Defuse kit time (%.1f) should be half of default (%.1f)" % [DEFUSE_KIT_TIME, DEFUSE_TIME] return "" func test_state_transition_carrier_can_drop() -> String: # Carrier drop: CARRIED → DROPPED (also PLANTING → DROPPED) var valid_drop_states := [BombState.CARRIED, BombState.PLANTING] var e := "" if not (BombState.CARRIED in valid_drop_states): e += " CARRIED should be droppable" if not (BombState.PLANTING in valid_drop_states): e += " PLANTING should be droppable" if BombState.PLANTED in valid_drop_states: e += " PLANTED should NOT be droppable" return e func test_can_only_plant_from_carried() -> String: # try_start_plant checks: state == CARRIED var valid_plant_states := [BombState.CARRIED] var e := "" if not (BombState.CARRIED in valid_plant_states): e += " CARRIED should be plantable" if BombState.DROPPED in valid_plant_states: e += " DROPPED should NOT be plantable" if BombState.IDLE in valid_plant_states: e += " IDLE should NOT be plantable" return e func test_can_only_defuse_from_planted() -> String: # try_start_defuse checks: state == PLANTED var valid_defuse_states := [BombState.PLANTED] var e := "" if not (BombState.PLANTED in valid_defuse_states): e += " PLANTED should be defusable" if BombState.DEFUSING in valid_defuse_states: e += " DEFUSING should NOT be defusable (already defusing)" return e func test_can_only_pickup_dropped() -> String: # try_pickup checks: state == DROPPED var valid_pickup_states := [BombState.DROPPED] var e := "" if not (BombState.DROPPED in valid_pickup_states): e += " DROPPED should be pickup-able" return e func test_bomb_timer_37_seconds_minimum() -> String: # Even with bomb_timer=40s, once planted there should be at least # 37 seconds of gameplay before explosion (plant takes 3s) # Total: 40s from plant start, 3s to plant → 37s after planted var remaining_after_plant := BOMB_TIMER - PLANT_TIME if remaining_after_plant < 37.0: return "After planting, bomb should tick for at least 37s, got %.1f" % remaining_after_plant return "" func test_defuse_within_bomb_timer() -> String: # Must be able to defuse before bomb explodes # Default defuse (10s) + plant (3s) = 13s out of 40s — tight but doable if DEFUSE_TIME + PLANT_TIME >= BOMB_TIMER: return "Defuse+plant time (%.1f+%.1f=%.1f) exceeds bomb timer (%.1f)! Impossible to defuse!" % [DEFUSE_TIME, PLANT_TIME, DEFUSE_TIME + PLANT_TIME, BOMB_TIMER] return "" func test_kit_defuse_within_bomb_timer() -> String: # With kit: 5s defuse + 3s plant = 8s out of 40s — comfortable if DEFUSE_KIT_TIME + PLANT_TIME >= BOMB_TIMER: return "Kit defuse+plant time (%.1f+%.1f=%.1f) exceeds bomb timer!" % [DEFUSE_KIT_TIME, PLANT_TIME, DEFUSE_KIT_TIME + PLANT_TIME] return "" func test_state_order_is_linear() -> String: # Verify state numbering follows logical gameplay order var e := "" if BombState.IDLE >= BombState.CARRIED: e += " IDLE < CARRIED" if BombState.CARRIED >= BombState.PLANTING: e += " CARRIED < PLANTING" if BombState.PLANTING >= BombState.PLANTED: e += " PLANTING < PLANTED" if BombState.PLANTED >= BombState.EXPLODED: e += " PLANTED < EXPLODED" if BombState.PLANTED >= BombState.DEFUSED: e += " PLANTED < DEFUSED" return e func test_reset_clears_all_state() -> String: # reset() sets: IDLE, carrier=-1, position=ZERO, ... var state := BombState.EXPLODED var carrier_id := 5 var position := Vector3(10, 0, 20) var planted_site := "A" var planted_tick := 100 var plant_start := 50 var defuse_start := -1 var defuser_id := -1 # reset() state = BombState.IDLE carrier_id = -1 position = Vector3.ZERO planted_site = "" planted_tick = -1 plant_start = -1 defuse_start = -1 defuser_id = -1 var e := "" if state != BombState.IDLE: e += " state should be IDLE" if carrier_id != -1: e += " carrier_id should be -1" if position != Vector3.ZERO: e += " position should be ZERO" if planted_site != "": e += " planted_site should be empty" if planted_tick != -1: e += " planted_tick should be -1" if plant_start != -1: e += " _plant_start_tick should be -1" if defuse_start != -1: e += " _defuse_start_tick should be -1" if defuser_id != -1: e += " _defuser_id should be -1" return e