extends RefCounted ## Weapon data tests — verifies all 6 weapon resources have correct stats. ## ## These tests load the .tres resource files directly and check every ## exported property matches the expected game balance values. # Weapon IDs (mirrors WeaponRegistry) const NONE := 0 const KNIFE := 1 const GLOCK := 2 const USP := 3 const AK47 := 4 const M4A1 := 5 const AWP := 6 # Expected weapon stats — single source of truth for balance verification const WEAPON_EXPECTED := { KNIFE: { "name": "Knife", "slot": 0, "damage": 50, "fire_rate": 0.4, "fire_mode": 2, # MELEE "magazine_size": 0, "reserve_size": 0, "move_speed": 6.0, "recoil_per_shot": 0.0, "recoil_recovery": 0.0, "kill_reward": 1500, "armor_penetration": 1.0, "price": 0, "team": -1, "max_distance": 2.0, "reload_time": 0.0, }, GLOCK: { "name": "Glock", "slot": 1, "damage": 25, "fire_rate": 0.15, "fire_mode": 0, # SEMI "magazine_size": 20, "reserve_size": 120, "move_speed": 5.5, "recoil_per_shot": 1.5, "recoil_recovery": 10.0, "kill_reward": 300, "armor_penetration": 0.47, "price": 400, "team": 0, "max_distance": 1000.0, "reload_time": 2.2, }, USP: { "name": "USP", "slot": 1, "damage": 34, "fire_rate": 0.17, "fire_mode": 0, # SEMI "magazine_size": 12, "reserve_size": 48, "move_speed": 5.5, "recoil_per_shot": 1.2, "recoil_recovery": 10.0, "kill_reward": 300, "armor_penetration": 0.5, "price": 500, "team": 1, # CT only "max_distance": 1000.0, "reload_time": 2.7, }, AK47: { "name": "AK-47", "slot": 2, "damage": 36, "fire_rate": 0.1, "fire_mode": 1, # AUTO "magazine_size": 30, "reserve_size": 90, "move_speed": 4.5, "recoil_per_shot": 2.5, "recoil_recovery": 10.0, "kill_reward": 300, "armor_penetration": 0.77, "price": 2500, "team": 0, # T only "max_distance": 1000.0, "reload_time": 2.5, }, M4A1: { "name": "M4A1", "slot": 2, "damage": 33, "fire_rate": 0.09, "fire_mode": 1, # AUTO "magazine_size": 30, "reserve_size": 90, "move_speed": 4.7, "recoil_per_shot": 2.0, "recoil_recovery": 10.0, "kill_reward": 300, "armor_penetration": 0.7, "price": 3100, "team": 1, # CT only "max_distance": 1000.0, "reload_time": 3.1, }, AWP: { "name": "AWP", "slot": 2, "damage": 110, "fire_rate": 1.5, "fire_mode": 0, # SEMI "magazine_size": 10, "reserve_size": 30, "move_speed": 4.0, "recoil_per_shot": 0.0, "recoil_recovery": 0.0, "kill_reward": 50, "armor_penetration": 0.97, "price": 4750, "team": -1, # both teams "max_distance": 1000.0, "reload_time": 3.7, }, } # All weapon resource paths const WEAPON_PATHS := { KNIFE: "res://examples/multiplayer-fps/scripts/data/weapons/knife.tres", GLOCK: "res://examples/multiplayer-fps/scripts/data/weapons/glock.tres", USP: "res://examples/multiplayer-fps/scripts/data/weapons/usp.tres", AK47: "res://examples/multiplayer-fps/scripts/data/weapons/ak47.tres", M4A1: "res://examples/multiplayer-fps/scripts/data/weapons/m4a1.tres", AWP: "res://examples/multiplayer-fps/scripts/data/weapons/awp.tres", } func test_all_weapons_loadable() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) if w == null: e += " Weapon %d failed to load" % wid return e func test_weapon_ids() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) if w.weapon_id != wid: e += " %s: weapon_id should be %d, got %d" % [w.weapon_name, wid, w.weapon_id] return e func test_weapon_names() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["name"] if w.weapon_name != exp: e += " Weapon %d: name should be '%s', got '%s'" % [wid, exp, w.weapon_name] return e func test_all_damage_values() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["damage"] if w.damage != exp: e += " %s: damage should be %d, got %d" % [w.weapon_name, exp, w.damage] return e func test_all_fire_rates() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["fire_rate"] if absf(w.fire_rate - exp) > 0.001: e += " %s: fire_rate should be %.3f, got %.3f" % [w.weapon_name, exp, w.fire_rate] return e func test_all_fire_modes() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["fire_mode"] if w.fire_mode != exp: e += " %s: fire_mode should be %d, got %d" % [w.weapon_name, exp, w.fire_mode] return e func test_all_magazine_sizes() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["magazine_size"] if w.magazine_size != exp: e += " %s: magazine_size should be %d, got %d" % [w.weapon_name, exp, w.magazine_size] return e func test_all_reserve_sizes() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["reserve_size"] if w.reserve_size != exp: e += " %s: reserve_size should be %d, got %d" % [w.weapon_name, exp, w.reserve_size] return e func test_all_move_speeds() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["move_speed"] if absf(w.move_speed - exp) > 0.01: e += " %s: move_speed should be %.1f, got %.1f" % [w.weapon_name, exp, w.move_speed] return e func test_all_recoils() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["recoil_per_shot"] if absf(w.recoil_per_shot - exp) > 0.01: e += " %s: recoil_per_shot should be %.1f, got %.1f" % [w.weapon_name, exp, w.recoil_per_shot] return e func test_all_recoil_recovery() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["recoil_recovery"] if absf(w.recoil_recovery - exp) > 0.01: e += " %s: recoil_recovery should be %.1f, got %.1f" % [w.weapon_name, exp, w.recoil_recovery] return e func test_all_kill_rewards() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["kill_reward"] if w.kill_reward != exp: e += " %s: kill_reward should be %d, got %d" % [w.weapon_name, exp, w.kill_reward] return e func test_all_armor_penetration() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["armor_penetration"] if absf(w.armor_penetration - exp) > 0.01: e += " %s: armor_penetration should be %.2f, got %.2f" % [w.weapon_name, exp, w.armor_penetration] return e func test_all_prices() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["price"] if w.price != exp: e += " %s: price should be %d, got %d" % [w.weapon_name, exp, w.price] return e func test_all_team_restrictions() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["team"] if w.team != exp: e += " %s: team restriction should be %d, got %d" % [w.weapon_name, exp, w.team] return e func test_all_max_distances() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["max_distance"] if absf(w.max_distance - exp) > 0.1: e += " %s: max_distance should be %.1f, got %.1f" % [w.weapon_name, exp, w.max_distance] return e func test_all_reload_times() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["reload_time"] if absf(w.reload_time - exp) > 0.01: e += " %s: reload_time should be %.1f, got %.1f" % [w.weapon_name, exp, w.reload_time] return e func test_all_slots() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) var exp = WEAPON_EXPECTED[wid]["slot"] if w.slot != exp: e += " %s: slot should be %d, got %d" % [w.weapon_name, exp, w.slot] return e func test_ak47_damage_balance() -> String: # AK-47: 3 hits should kill (3*36=108 > 100) var ak = load(WEAPON_PATHS[AK47]) if ak.damage * 3 <= 100: return "AK-47 3-hit kill should exceed 100 HP (3*%d=%d)" % [ak.damage, ak.damage * 3] if ak.damage * 2 >= 100: return "AK-47 should NOT 2-hit kill (2*%d=%d)" % [ak.damage, ak.damage * 2] return "" func test_m4a1_damage_balance() -> String: # M4A1: 4 hits should kill (4*33=132 > 100) var m4 = load(WEAPON_PATHS[M4A1]) if m4.damage * 4 <= 100: return "M4A1 4-hit kill should exceed 100 HP (4*%d=%d)" % [m4.damage, m4.damage * 4] return "" func test_awp_one_shot_kill() -> String: # AWP: 1 shot should kill (115 > 100) var awp = load(WEAPON_PATHS[AWP]) if awp.damage <= 100: return "AWP should one-shot kill (damage=%d)" % awp.damage return "" func test_knife_one_shot_kill() -> String: # Knife does 50 damage, 2 hits = 100 = exactly max HP var knife = load(WEAPON_PATHS[KNIFE]) if knife.damage * 2 < 100: return "Knife should 2-hit kill (2*%d=%d)" % [knife.damage, knife.damage * 2] return "" func test_t_side_default_weapon() -> String: # Terrorists start with Glock (team=0), costs 400 var glock = load(WEAPON_PATHS[GLOCK]) if glock.team != 0: return "Glock should be T-only (team=0), got %d" % glock.team if glock.price > 500: return "Glock should be affordable first round ($400), price=%d" % glock.price return "" func test_ct_side_default_weapon() -> String: # CTs start with USP (team=1), costs 500 var usp = load(WEAPON_PATHS[USP]) if usp.team != 1: return "USP should be CT-only (team=1), got %d" % usp.team if usp.price > 800: return "USP ($500) should be affordable with start money ($800)" return "" func test_weapon_registry_team_default() -> String: var Glock := load("res://examples/multiplayer-fps/scripts/data/weapons/glock.tres") var USP := load("res://examples/multiplayer-fps/scripts/data/weapons/usp.tres") var e := "" # T should get Glock as default if Glock.team != 0: e += " Glock team should be 0 (T)" # CT should get USP as default if USP.team != 1: e += " USP team should be 1 (CT)" return e func test_no_negative_prices() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) if w.price < 0: e += " %s has negative price %d" % [w.weapon_name, w.price] return e func test_no_zero_damage_weapons() -> String: var e := "" for wid in WEAPON_PATHS: var w = load(WEAPON_PATHS[wid]) if w.damage <= 0: e += " %s has zero/negative damage %d" % [w.weapon_name, w.damage] return e