Files
Blockline/tests/integration.gd
T

181 lines
6.2 KiB
GDScript
Raw Normal View History

2026-09-12 11:01:37 +02:00
extends SceneTree
var game: Node3D
var failures := 0
var checks := 0
func _initialize() -> void:
call_deferred("run")
func check(value: bool, title: String) -> void:
checks += 1
if value:
print("PASS ", title)
else:
failures += 1
printerr("FAIL ", title)
func settle() -> void:
await physics_frame
await physics_frame
func run() -> void:
seed(7102)
game = load("res://scenes/main.tscn").instantiate()
root.add_child(game)
await settle()
game.port = 27990
game.host({"server_name": "Integration", "mode": "TDM", "max_players": 8, "bots": 7, "score_limit": 50, "time_limit": 600})
check(game.active and game.players.size() == 8, "host creates eight slots including seven bots")
var sides := [0, 0]
for p: Fighter in game.players.values():
sides[p.team] += 1
check(sides == [4, 4], "automatic 4v4 team balance")
game.set_physics_process(false)
for p: Fighter in game.players.values():
p.set_physics_process(false)
for x in [-21, 0, 21]:
var route: PackedVector2Array = game.arena.path(Vector3(x, 0, 30), Vector3(x, 0, -30))
check(route.size() > 0, "lane %s connects north and south" % x)
var length := 0.0
for i in range(1, route.size()):
length += route[i - 1].distance_to(route[i])
print("LANE ", x, " path_m=", snappedf(length, 0.1), " sprint_seconds=", snappedf(length / 8.7, 0.1))
for z in [-30, 0, 30]:
check(game.arena.path(Vector3(-19, 0, z), Vector3(19, 0, z)).size() > 0, "cross connection at z=%s" % z)
var a: Fighter = game.players[1]
var b: Fighter = game.players[-1]
for p: Fighter in game.players.values():
p.position = Vector3(26, 0, 35 + p.peer_id * 0.1)
a.reset_at(Vector3(0, 0.05, 2))
b.reset_at(Vector3(0, 0.05, -14))
a.protection = 0
b.protection = 0
a.yaw = 0
a.pitch = -0.027
a.aiming = true
await settle()
game.combat.shoot(a)
check(b.hp < 100 and b.hp > 0 and a.magazines[0] == 29, "authoritative hitscan deals damage and consumes one round")
var body_damage := 100 - b.hp
b.hp = 100
a.pitch = 0
game.combat.shoot(a)
check(100 - b.hp > body_damage * 1.5, "head hit has increased damage")
b.hp = 100
b.protection = 1
game.combat.shoot(a)
check(b.hp == 100, "spawn protection blocks damage")
b.protection = 0
b.team = a.team
game.combat.shoot(a)
check(b.hp == 100, "TDM friendly fire disabled")
b.team = 1
a.position = Vector3(0, 0.05, 39)
b.position = Vector3(0, 0.05, 30)
await settle()
game.combat.shoot(a)
check(b.hp == 100, "spawn wall blocks hitscan")
game.combat.damage(b, a, 200, "AR-4", false)
check(b.hp == 0 and b.respawn_left == 3 and a.kills == 1 and game.scores[0] == 1, "death sets respawn timer and team score")
b.bot = false
b._physics_process(3.1)
check(b.hp == 100 and b.protection > 0 and b.magazines[0] == 30, "automatic respawn restores health, ammo and protection")
a.weapon = 0
a.magazines[0] = 2
a.reserves[0] = 17
a.input_data = {"reload": true}
game.combat.actions(a)
a._physics_process(1.9)
check(a.magazines[0] == 19 and a.reserves[0] == 0, "reload transfers only available reserve")
for w in 5:
a.weapon = w
a.magazines[w] = Arsenal.DATA[w].mag
var before: int = a.magazines[w]
game.combat.shoot(a)
check(a.magazines[w] == before - 1 and a.cooldown == Arsenal.DATA[w].rate, "weapon %s ammunition and cadence" % Arsenal.DATA[w].short)
game.config.mode = "FFA"
b.team = a.team
b.protection = 0
b.hp = 100
check(game.combat.damage(b, a, 20, "P12", false) and b.hp == 80, "FFA removes team immunity")
a.reset_at(Vector3(0, 0.05, 2))
b.reset_at(Vector3(0, 0.05, -1))
a.protection = 0
b.protection = 0
await settle()
game.combat.explode(Vector3(0, 0.3, 0), a.peer_id)
check(b.hp < 100, "grenade radial damage reaches exposed target")
a.reset_at(Vector3(0, 0.05, 39))
b.reset_at(Vector3(0, 0.05, 30))
b.protection = 0
await settle()
game.combat.explode(Vector3(0, 1, 37), a.peer_id)
check(b.hp == 100, "grenade damage does not penetrate spawn wall")
game.spawn_grenade(1, Vector3(0, 3, 0), Vector3(2, 2, 0))
await create_timer(2.9).timeout
check(game.grenades.get_child_count() == 0, "physical grenade expires after fuse")
# Actual CharacterBody collision integration, not a formula-only movement test.
a.reset_at(Vector3(0, 0.05, 20))
a.input_data = {"move": Vector2(0, -1)}
await settle()
for frame in 60:
a.move_character(1.0 / 60.0)
await physics_frame
var walked := 20 - a.position.z
check(walked > 5 and walked < 6.2, "walking travels about 5.8 metres per second")
a.reset_at(Vector3(0, 0.05, 20))
a.input_data = {"move": Vector2(0, -1), "sprint": true}
await settle()
for frame in 60:
a.move_character(1.0 / 60.0)
await physics_frame
check(20 - a.position.z > walked + 2, "sprint increases real movement speed")
a.input_data = {"move": Vector2(0, -1), "sprint": true, "crouch": true}
a.move_character(1.0 / 60.0)
check(a.slide_left > 0 and a.crouched, "sprint plus crouch initiates slide")
a.reset_at(Vector3(0, 0.05, 20))
a.input_data = {}
for frame in 8:
a.move_character(1.0 / 60.0)
await physics_frame
a.input_data = {"jump": true}
a.move_character(1.0 / 60.0)
check(a.velocity.y > 6, "jump launches from ground")
# Both ends of each platform are reachable with the small engine-sweep step.
a.reset_at(Vector3(-23, 0.05, 9))
a.slide_left = 0
a.input_data = {"move": Vector2(0, -1)}
for frame in 110:
a.move_character(1.0 / 60.0)
await physics_frame
check(a.position.y > 1.8, "stairs lead to the elevated side platform")
var enemy_index := 0
for other: Fighter in game.players.values():
if other == a:
continue
other.team = 1
other.hp = 100
other.position = game.arena.spawn_points[5 + enemy_index % 5]
enemy_index += 1
game.config.mode = "TDM"
a.team = 0
await settle()
game.respawn(a)
check(a.position.z < 0, "spawn selection flips away from occupied home base")
game.config.mode = "TDM"
game.scores = [50, 3]
game.check_win()
check(game.match_over and game.winner == "TEAM RELAY WINS", "TDM score limit ends match")
game.config.mode = "FFA"
game.match_over = false
game.config.score_limit = 25
a.kills = 25
game.check_win()
check(game.match_over and game.winner.contains(a.nickname), "FFA score limit ends match")
game.leave()
await process_frame
check(not game.active and game.players.is_empty(), "leave cleans up session")
print("RESULT ", checks - failures, "/", checks, " passed")
quit(1 if failures else 0)