Files
shawn 012d038025 Integrate Kenney Starter Kit FPS assets + comprehensive test suite
Assets (CC0 license):
- 3D models: Blaster gun, walls, platforms, grass, clouds, enemy
- Sounds: blaster fire, impacts, footsteps, jumps, weapon change
- Sprites: crosshair, hit marker, muzzle flash, skybox, blob shadow
- Font: Lilita One

Code changes:
- weapon_manager.gd: load blaster.glb as gun model (replaces box mesh)
- player.tscn: use Kenney sounds (blaster.ogg, enemy_hurt/destroy) and crosshair

Test suite (100 unit tests + 4 integration scenarios):
- weapon_data.gd: 27 tests — all 6 weapons every stat verified
- economy.gd: 19 tests — constants, loss streak, buy thresholds
- bomb.gd: 15 tests — state machine, timing constants
- round_manager.gd: 10 tests — win conditions, elimination logic
- team_manager.gd: 8 tests — auto-balance, team assignment
- headless_test_bot.gd: integration bot with movement/idle/rounds scenarios
- run_multi_bot.sh: multi-client launcher
2026-07-03 17:13:36 -04:00

161 lines
6.2 KiB
GDScript

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