Files
Blockline/tests/features.gd
T

199 lines
7.2 KiB
GDScript
Raw Normal View History

2026-09-12 11:01:37 +02:00
extends SceneTree
var game: Node3D
var a: Fighter
var b: Fighter
var checks := 0
var failures := 0
func _initialize() -> void:
call_deferred("run")
func check(ok: bool, title: String) -> void:
checks += 1
if ok: print("PASS ", title)
else:
failures += 1
printerr("FAIL ", title)
func settle() -> void:
await physics_frame
await physics_frame
func place(p: Fighter, position: Vector3) -> void:
p.reset_at(position)
p.protection = 0
p.set_physics_process(false)
func run() -> void:
create_timer(50).timeout.connect(func(): printerr("FAIL feature timeout"); quit(1))
game = load("res://scenes/main.tscn").instantiate()
root.add_child(game)
game.port = 27980
var settings: Dictionary = game.config.duplicate()
settings.bots = 0
game.host(settings)
game.set_physics_process(false)
a = game.players[1]
b = game.add_player(-1, "Feature target", false, 0)
place(a, Vector3(28, 0.1, 30))
place(b, Vector3(5, 0.1, 0))
await settle()
game.combat.explode(Vector3(0, 0.2, 0), 1)
check(b.hp < 100 and b.hp > 0, "blast damages exposed target within eight metres")
check(game.burn_zones.zones.size() == 1, "frag creates an authoritative burn zone")
place(b, Vector3(9, 0.1, 0))
await settle()
game.combat.explode(Vector3(0, 0.2, 0), 1)
check(b.hp == 100, "blast stops outside eight metres")
game.burn_zones.clear()
game.burn_zones.ignite(Vector3(0, 0.2, 0), 1)
place(b, Vector3(3, 0.1, 0))
await settle()
game.burn_zones.advance(1)
check(is_equal_approx(b.hp, 92), "burn deals eight HP per second in four ticks")
b.team = a.team
game.burn_zones.advance(0.5)
check(is_equal_approx(b.hp, 92), "burn preserves team damage immunity")
b.team = 1
b.protection = 1
game.burn_zones.advance(0.5)
check(is_equal_approx(b.hp, 92), "burn respects spawn protection")
b.protection = 0
b.position.y = 3.7
game.burn_zones.advance(0.5)
check(is_equal_approx(b.hp, 92), "ground fire does not reach another storey")
var rows: Array = game.burn_zones.snapshot()
game.burn_zones.advance(0.5)
check(game.burn_zones.zones.is_empty(), "burn ends after three seconds")
game.is_host = false
game.burn_zones.sync(rows)
check(game.burn_zones.zones.size() == 1 and is_equal_approx(game.burn_zones.zones.values()[0].left, 0.5), "late snapshot retains remaining burn lifetime")
game.burn_zones.sync([])
check(game.burn_zones.zones.is_empty(), "client removes expired fire from snapshot")
game.is_host = true
place(b, Vector3(0, 0.1, 30))
await settle()
game.burn_zones.ignite(Vector3(0, 0.2, 37), 1)
game.burn_zones.advance(1)
check(b.hp == 100, "solid wall blocks lingering fire")
game.burn_zones.clear()
place(b, Vector3(0, 0.1, -6))
b.yaw = PI
await settle()
game.combat.flash(Vector3(0, 1.65, 0))
var direct: float = b.flash_left
b.flash_left = 0
b.yaw = 0
game.combat.flash(Vector3(0, 1.65, 0))
check(direct > b.flash_left * 2 and b.flash_left > 0, "flash strength depends on viewing direction")
check(b.hp == 100 and game.burn_zones.zones.is_empty(), "flashbang causes neither damage nor fire")
place(b, Vector3(0, 0.1, 30))
await settle()
game.combat.flash(Vector3(0, 1.65, 37))
check(b.flash_left == 0, "wall blocks flash exposure")
game.config.mode = "DOM"
game.objectives.reset()
game.scores = [0, 0]
place(a, Vector3(0, 0.1, 0))
place(b, Vector3(0, 0.1, 1))
await settle()
game.objectives.advance(4.1)
check(game.objectives.points[1].owner == -1, "contested domination point is not captured")
place(b, Vector3(28, 0.1, 30))
await settle()
game.objectives.advance(4.1)
check(game.objectives.points[1].owner == 0 and game.scores[0] > 0, "uncontested domination capture awards team points")
game.objectives.reset()
a.position.y = 3.7
await settle()
game.objectives.advance(4.1)
check(game.objectives.points[1].owner == -1, "domination cannot be captured from the floor above")
game.config.mode = "KC"
game.scores = [0, 0]
game.objectives.reset()
place(a, Vector3(0, 0.1, 0))
place(b, Vector3(0, 0.1, -0.8))
await settle()
game.combat.damage(b, a, 200, "P12", false)
check(game.scores[0] == 0 and game.objectives.tags.size() == 1, "kill confirmed drops a tag instead of awarding an immediate point")
game.objectives.advance(0.1)
check(game.scores[0] == 1 and game.objectives.tags.is_empty(), "enemy tag collection confirms the team point")
game.objectives.death(a)
game.objectives.advance(0.1)
check(game.scores[0] == 1 and game.objectives.tags.is_empty(), "friendly tag collection denies without adding points")
game.config.mode = "TDM"
place(a, Vector3(0, 0.1, 2))
place(b, Vector3(0, 0.1, -12))
a.weapon = 3
a.yaw = 0
a.pitch = 0
a.aiming = true
await settle()
game.combat.shoot(a)
check(game.projectiles.get_child_count() == 1 and b.hp == 100, "sniper creates a travelling projectile rather than instant damage")
await create_timer(0.15).timeout
check(b.hp == 0 and game.projectiles.get_child_count() == 0, "sniper swept projectile hits and cleans up")
place(a, Vector3(0, 0.1, 39))
place(b, Vector3(0, 0.1, 30))
a.weapon = 3
a.yaw = 0
a.aiming = true
await settle()
game.combat.shoot(a)
await create_timer(0.15).timeout
check(b.hp == 100, "sniper projectile cannot tunnel through spawn wall")
place(a, Vector3(3, 0.1, -20.6))
a.yaw = 0
for frame in 6:
a.move_character(1.0 / 60)
await physics_frame
a.input_data = {"jump": true}
for frame in 25:
a.move_character(1.0 / 60)
await physics_frame
check(a.position.y > 1.3 and a.position.z < -21.2, "mantle moves the real capsule onto a low cover")
place(a, Vector3(0, 0.1, 36))
a.yaw = 0
await settle()
check(not Mantle.destination(a).is_finite(), "mantle refuses tall walls")
game.history.frames.clear()
place(a, Vector3(0, 0.1, 2))
place(b, Vector3(0, 0.1, -10))
game.history.record([b.snapshot()])
b.position.x = 7
await settle()
var hit: Dictionary = game.history.rewind_hit(a, a.eye(), Vector3(0, 1.75, -20), 0.1)
check(hit.get("collider") == b, "rewind hit uses the recorded historical position")
b.life += 1
hit = game.history.rewind_hit(a, a.eye(), Vector3(0, 1.75, -20), 0.1)
check(hit.get("collider") != b, "rewind never hits a previous life after respawn")
var path := "res://tests/audit-settings.cfg"
game.nickname = "Audit Operator"
game.base_fov = 97
check(PlayerSettings.save(game, path) == OK, "settings can be saved to a writable path")
game.nickname = "Changed"
game.base_fov = 80
PlayerSettings.restore(game, path)
check(game.nickname == "Audit Operator" and game.base_fov == 97, "settings restore player and viewing preferences")
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
place(a, Vector3(28, 0.1, 30))
place(b, Vector3(-17, 0.1, -22.6))
b.team = a.team
b.bot = true
b.bot_target = Vector3(-15, 7.4, -22.6)
b.bot_path = game.bots.routes.path(b.position, b.bot_target)
b.bot_think = 1000
for frame in 650:
game.bots.update(b, 1.0 / 60)
b.move_character(1.0 / 60)
await physics_frame
if b.position.distance_to(b.bot_target) < 0.5: break
check(b.position.distance_to(b.bot_target) < 0.5, "bot follows the actual exterior stair route onto the roof at %s" % b.position)
game.leave()
check(game.burn_zones.zones.is_empty() and game.history.frames.is_empty(), "leaving clears persistent combat and replay state")
game.queue_free()
await process_frame
print("FEATURES RESULT ", checks - failures, "/", checks)
quit(1 if failures else 0)