Two rivals now lap the RacePath in free roam. Pull alongside one at speed and the duel arms: first to gap the other by 250 m wins. Straight-line distance decides when it ends (side-street escapes are real); path order decides who won (reversing away is a loss). Rubber-banded chase, takedown-tier boost payout, R mid-duel forfeits, 8 s re-arm cooldown. Mechanics only -- no assets from anywhere near an EA disk. Smoke test arms a duel and settles the win path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
5.1 KiB
GDScript
131 lines
5.1 KiB
GDScript
extends SceneTree
|
|
## Headless smoke test:
|
|
## /Applications/Godot.app/Contents/MacOS/Godot --headless --path . --script tests/smoke.gd
|
|
## Registry finds content; each mode boots and simulates; the car drives; traffic
|
|
## gets smashed when driven through.
|
|
|
|
func _initialize() -> void:
|
|
_run()
|
|
|
|
func _run() -> void:
|
|
var cars := Registry.cars()
|
|
var levels := Registry.levels()
|
|
assert(not cars.is_empty(), "no cars found")
|
|
assert(not levels.is_empty(), "no levels found")
|
|
for p in cars.values():
|
|
var s: CarStats = load(p)
|
|
assert(s != null and s.engine_power > 0.0, "bad stats: %s" % p)
|
|
if s.model_path != "":
|
|
assert(load(s.model_path) is PackedScene, "bad model: %s" % s.model_path)
|
|
# datto is procedural (has det_* parts); imported models legitimately have none
|
|
Game.car_path = cars.get("datto_120y", cars.values()[0])
|
|
# the carpark is the controlled arena; city levels have walls that complicate the ram test
|
|
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
|
|
|
|
# cruise: throttle 3s, car must move
|
|
var game: Game = await _boot("cruise")
|
|
if game.car.stats.model_path != "":
|
|
assert(game.car._parts.size() >= 8, "model has no detachable det_* parts")
|
|
var start: Vector3 = game.car.global_position
|
|
Input.action_press("throttle")
|
|
for i in 180:
|
|
await physics_frame
|
|
var dist := start.distance_to(game.car.global_position)
|
|
assert(dist > 5.0, "car didn't move (%.1f m)" % dist)
|
|
Input.action_release("throttle")
|
|
game.free()
|
|
|
|
# crash mode: traffic spawned, ram the nearest one, score must rise
|
|
game = await _boot("crash")
|
|
assert(game.traffic.size() == Game.TRAFFIC_COUNT["crash"], "traffic missing")
|
|
var victim: TrafficCar = game.traffic[0]
|
|
await physics_frame # traffic takes position on its first physics tick
|
|
Input.action_press("throttle")
|
|
for attempt in 8: # re-aim: victim keeps moving along the loop
|
|
if not victim.live:
|
|
break
|
|
var to_victim := (victim.global_position - game.car.global_position)
|
|
game.car.global_position = victim.global_position - victim.global_basis.z * 8.0
|
|
game.car.look_at(victim.global_position)
|
|
to_victim = (victim.global_position - game.car.global_position).normalized()
|
|
game.car.linear_velocity = to_victim * 18.0
|
|
for i in 30:
|
|
await physics_frame
|
|
if not victim.live:
|
|
break
|
|
Input.action_release("throttle")
|
|
assert(not victim.live, "rammed traffic never went dynamic")
|
|
assert(game.score > 0.0, "crash mode scored nothing")
|
|
var crash_score := game.score
|
|
game.free()
|
|
|
|
# junction: convoys spawn, driving straight in causes the crash -> settle ->
|
|
# crashbreaker -> tally
|
|
var jcts := Registry.junctions().filter(func(j): return j.level_id == "woolies_carpark")
|
|
assert(not jcts.is_empty(), "no woolies junction event")
|
|
Game.junction = jcts[0].data
|
|
game = await _boot("junction")
|
|
assert(game.traffic.size() >= 8, "junction convoys missing (%d)" % game.traffic.size())
|
|
Input.action_press("throttle")
|
|
for i in 950:
|
|
await physics_frame
|
|
if game.jstate == "settle" and not game.breaker_used:
|
|
Input.action_release("throttle")
|
|
Input.action_press("boost")
|
|
elif game.breaker_used:
|
|
Input.action_release("boost")
|
|
if game.over:
|
|
break
|
|
Input.action_release("throttle")
|
|
Input.action_release("boost")
|
|
assert(game.jstate != "run", "never crashed into the convoy")
|
|
assert(game.breaker_used, "crashbreaker never fired")
|
|
assert(game.over and game.score > 0.0, "junction never tallied (score %d)" % int(game.score))
|
|
var jct_score := game.score
|
|
Game.junction = {}
|
|
game.free()
|
|
|
|
# outrun: roaming rivals exist in cruise; pulling alongside at speed arms the
|
|
# duel, and the win path pays out and clears state
|
|
game = await _boot("cruise")
|
|
assert(game.rivals.size() == Game.ROAM_RIVALS, "no roaming rivals in cruise")
|
|
var rr: Rival = game.rivals[0]
|
|
await physics_frame
|
|
game.car.global_position = rr.global_position + Vector3(3, 0.3, 0)
|
|
game.car.linear_velocity = Vector3(0, 0, -12) # above the 8 m/s arming speed
|
|
for i in 5:
|
|
await physics_frame
|
|
if game.outrun_rival != null:
|
|
break
|
|
assert(game.outrun_rival == rr, "outrun didn't arm on proximity")
|
|
var bm0: float = game.car.boost_max
|
|
game._outrun_end(true, "test")
|
|
assert(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle")
|
|
assert(game.car.boost_max > bm0, "outrun win paid no boost")
|
|
assert(game.outrun_cd > 0.0, "no re-arm cooldown after outrun")
|
|
game.free()
|
|
|
|
# race: rivals spawn and make progress along the path
|
|
game = await _boot("race")
|
|
assert(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing")
|
|
var p0: float = game.rivals[0].progress
|
|
for i in 240:
|
|
await physics_frame
|
|
var moved := false
|
|
for r in game.rivals:
|
|
if absf(r.progress - p0) > 3.0 or r.linear_velocity.length() > 3.0:
|
|
moved = true
|
|
assert(moved, "no rival is driving")
|
|
game.free()
|
|
|
|
print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | race AI driving" %
|
|
[cars.size(), levels.size(), dist, int(crash_score), int(jct_score)])
|
|
quit(0)
|
|
|
|
func _boot(mode: String) -> Game:
|
|
Game.mode = mode
|
|
var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate()
|
|
root.add_child(game)
|
|
await process_frame # _ready is deferred until the main loop runs
|
|
return game
|