Burnout 3 ran about 110 km/h and felt like 300. None of this makes the
car faster; all of it makes the car feel faster.
Screen pass (src/speed_fx.gd, shader authored in code like everything
else, layer 0 so the HUD stays sharp):
- RADIAL BLUR smearing the world outward from the crosshair
- SPEED LINES, the Sonic trick: 300 radial lanes, randomised phase,
streaming outward, sparse while merely quick and a storm on boost
- CHROMATIC ABERRATION folded into the blur as a per-channel radius
- TUNNEL VIGNETTE squeezing the frame under boost
Camera (game.gd _speed_cam): drops and hugs the boot as speed rises
(2.4/6.0 m -> 1.75/5.1), lags on hard acceleration so the car pulls
away from the lens, aims further up the road, dutch-rolls into the
steering (doubled mid-drift), and gains speed-scaled micro-shake on top
of the impact shake. FOV runs 72 -> 96 with speed, +12 on boost, and
PUNCHES +9 the instant boost lights -- the punch is what sells the
shove.
Plus motes streaming past the lens (Fx.rusher, parented to the camera
in local space -- the radial blur smears each speck into a streak for
free) and a procedural WIND loop whose volume and pitch ride the same
curve, so the ears agree with the eyes.
Everything keys off one number, km/h, with a squared response: nothing
happens at town speed, then it piles on. Effects cut in at 55 km/h and
max at 165.
Two things measurement caught that reading wouldn't: bolting chromatic
aberration on AFTER the blur samples sharp red/blue against blurred
green and fringes every edge magenta (it looked like a broken display,
not a fast car) -- the fix is per-channel sample radii inside the blur
loop; and the first pass was simply too much everywhere, so boost is
now the visual event and cruising fast is merely urgent.
Smoke: at ~160 km/h the lens must open >8 deg, the screen pass must
engage and become visible, the motes must emit, and pressing boost must
kick the FOV further still.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
552 lines
24 KiB
GDScript
552 lines
24 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()
|
|
_check(not cars.is_empty(), "no cars found")
|
|
_check(not levels.is_empty(), "no levels found")
|
|
for p in cars.values():
|
|
var s: CarStats = load(p)
|
|
_check(s != null and s.engine_power > 0.0, "bad stats: %s" % p)
|
|
if s.model_path != "":
|
|
_check(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])
|
|
|
|
# garage economy: scratch save (NEVER the real one), earn/buy roundtrip,
|
|
# stats + bolt-ons applied to the player, then reset so later stages run stock
|
|
print(" .. garage")
|
|
Garage.save_path = "user://garage_smoketest.save"
|
|
Garage._reset_for_test()
|
|
_check(Garage.bucks() == 0, "scratch garage not empty")
|
|
Garage.earn(4000)
|
|
_check(Garage.bucks() == 4000, "earn didn't bank")
|
|
_check(Garage.buy("datto_120y", "spoiler"), "couldn't buy the wing")
|
|
_check(Garage.buy("datto_120y", "turbo"), "couldn't buy the turbo")
|
|
_check(not Garage.buy("datto_120y", "spoiler"), "bought the same wing twice")
|
|
_check(not Garage.buy("datto_120y", "exhaust"), "bought a $2000 pipe with $500")
|
|
_check(Garage.bucks() == 500, "change is wrong (%d)" % Garage.bucks())
|
|
var base_power: float = (load("res://cars/datto_120y/stats.tres") as CarStats).engine_power
|
|
var gg: Game = await _boot("cruise")
|
|
_check(gg.car.stats.engine_power > base_power * 1.15, "turbo didn't boost power")
|
|
_check(gg.car.stats.grip > (load("res://cars/datto_120y/stats.tres") as CarStats).grip + 0.5,
|
|
"wing added no grip")
|
|
_check(gg.car.stats.resource_path == "", "upgrades mutated the SHARED stats resource")
|
|
gg.free()
|
|
# the shed: tunes are gated on tools, and retorque hardens the panels
|
|
Garage.earn(2000)
|
|
_check(not Garage.buy_tune("datto_120y", "retorque"), "tuned without owning the tool")
|
|
_check(Garage.buy_tool("sockets"), "couldn't buy the socket set")
|
|
_check(Garage.buy_tune("datto_120y", "retorque"), "tool owned but tune refused")
|
|
_check(not Garage.buy_tune("datto_120y", "retorque"), "same tune sold twice")
|
|
gg = await _boot("cruise")
|
|
_check(gg.car.part_dv > Car.PART_DV + 1.0, "retorque didn't harden panels (%.1f)" % gg.car.part_dv)
|
|
gg.free()
|
|
# raceday persistence: ladder rung and per-car quarter-mile PBs
|
|
_check(Garage.drag_rung() == 0, "fresh save not on ladder rung 0")
|
|
Garage.drag_advance()
|
|
_check(Garage.drag_rung() == 1, "ladder didn't advance")
|
|
_check(Garage.record_drag_pb("datto_120y", 14.5), "first ET not a PB")
|
|
_check(not Garage.record_drag_pb("datto_120y", 15.0), "slower ET counted as a PB")
|
|
_check(Garage.record_drag_pb("datto_120y", 13.9), "faster ET refused as PB")
|
|
_check(absf(Garage.drag_pb("datto_120y") - 13.9) < 0.01, "PB not stored")
|
|
# tool tiers: buying again upgrades; effects are exercised by wrench jobs
|
|
Garage.earn(1500 + 3000)
|
|
_check(Garage.tool_tier("sockets") == 1, "owned sockets not tier 1")
|
|
_check(Garage.buy_tool("sockets"), "tier 2 upgrade refused")
|
|
_check(Garage.buy_tool("sockets"), "tier 3 upgrade refused")
|
|
_check(Garage.tool_tier("sockets") == 3 and not Garage.buy_tool("sockets"),
|
|
"tier cap broken (tier %d)" % Garage.tool_tier("sockets"))
|
|
_check(Garage.has_tool("sockets"), "tiered tool lost has_tool")
|
|
# faults: break it, boot wounded, pay the shop for one, wrench the other
|
|
print(" .. repair")
|
|
Garage.add_fault("datto_120y", "gearbox")
|
|
Garage.add_fault("datto_120y", "gasket")
|
|
_check(Garage.faults("datto_120y").size() == 2, "faults didn't record")
|
|
gg = await _boot("cruise")
|
|
_check(gg.car.stats.engine_power < base_power * 0.9, "wounded car boots at full power")
|
|
_check(gg.car.leaking, "gearbox-faulted car isn't leaking")
|
|
gg.free()
|
|
Garage.earn(1000)
|
|
_check(Garage.shop_fix("datto_120y", "gasket"), "shop wouldn't take the money")
|
|
_check(not Garage.faults("datto_120y").has("gasket"), "shop fix didn't clear the fault")
|
|
# the gearbox goes through the 3D wrench: diagnose (wrong first, penalty),
|
|
# then the toolless 60 kg drag path -- no hoist owned on this save
|
|
Garage.ui_stats_path = "res://cars/datto_120y/stats.tres"
|
|
Garage.ui_fault = "gearbox"
|
|
var ui = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
|
root.add_child(ui)
|
|
await process_frame
|
|
_check(ui.steps[0]["verb"] == "diagnose", "gearbox job skipped diagnosis")
|
|
_check(ui.steps.size() == 8, "toolless gearbox sheet wrong (%d steps)" % ui.steps.size())
|
|
var right := -1
|
|
for i in ui.light_options.size():
|
|
if ui.light_options[i] == Garage.FAULTS["gearbox"]["light"]:
|
|
right = i
|
|
ui.pick_light((right + 1) % 3)
|
|
_check(ui.penalty_turns > 0.0 and ui.quality < 100.0, "wrong diagnosis cost nothing")
|
|
ui.pick_light(right)
|
|
_check(ui.step_index == 1, "right light didn't advance the job")
|
|
await _drive_wrench(ui, false)
|
|
_check(ui.done, "gearbox job never finished (step %d)" % ui.step_index)
|
|
_check(not Garage.faults("datto_120y").has("gearbox"), "wrench fix didn't clear the fault")
|
|
_check(Garage.quality("datto_120y", "gearbox") >= 85.0, "clean-ish gearbox scored %d"
|
|
% int(ui.quality))
|
|
ui.free()
|
|
# a gasket job at 85+ raises the overheat ceiling; gearbox 85+ adds a strike
|
|
Garage.add_fault("datto_120y", "gasket")
|
|
Garage.ui_fault = "gasket"
|
|
ui = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
|
root.add_child(ui)
|
|
await process_frame
|
|
await _drive_wrench(ui, false)
|
|
_check(ui.done and Garage.quality("datto_120y", "gasket") >= 85.0,
|
|
"gasket job didn't finish clean (%d)" % int(ui.quality))
|
|
ui.free()
|
|
Game.level_path = levels["willowbank_dragway"]
|
|
gg = await _boot("drag")
|
|
_check(gg.box_limit == 4, "85+ gearbox rebuild paid no extra strike (%d)" % gg.box_limit)
|
|
_check(gg.gasket_cap > 1.1, "85+ gasket job raised no ceiling (%.2f)" % gg.gasket_cap)
|
|
gg.free()
|
|
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
|
|
# THE SHED home base: owned tools materialise (sockets were bought above,
|
|
# ramps were not), and rolling into the bay and holding still opens the garage
|
|
print(" .. shed")
|
|
Game.level_path = levels["the_shed"]
|
|
gg = await _boot("cruise")
|
|
_check(gg.garage_bay != null, "shed has no GarageBay")
|
|
var owned_props: Array = gg.find_children("*", "RigidBody3D", true, false).filter(
|
|
func(n): return n.has_meta("shed_tool"))
|
|
_check(owned_props.size() == 1 and owned_props[0].get_meta("shed_tool") == "sockets",
|
|
"owned-tool display wrong (%d shed_tool props)" % owned_props.size())
|
|
var bay_at: Vector3 = gg.garage_bay.global_position
|
|
var bay_xf := Transform3D(Basis.IDENTITY, bay_at + Vector3.UP * 0.75)
|
|
PhysicsServer3D.body_set_state(gg.car.get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, bay_xf)
|
|
gg.car.linear_velocity = Vector3.ZERO
|
|
gg.car._prev_lv = Vector3.ZERO
|
|
for i in 150:
|
|
await physics_frame
|
|
gg.car.linear_velocity = Vector3.ZERO
|
|
if gg.garage_requested:
|
|
break
|
|
_check(gg.garage_requested, "rolling into the bay never opened the garage")
|
|
gg.free()
|
|
# phase 3: a driveway customer waits, gets their car fixed, and pays
|
|
print(" .. customer")
|
|
var cust := Garage.customer()
|
|
_check(not cust.is_empty(), "no customer turned up at the shed")
|
|
gg = await _boot("cruise")
|
|
_check(gg.customer_spot != null, "customer car has no CustomerSpot")
|
|
var cs_xf := Transform3D(Basis.IDENTITY, gg.customer_spot.global_position + Vector3.UP * 0.75)
|
|
PhysicsServer3D.body_set_state(gg.car.get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, cs_xf)
|
|
gg.car.linear_velocity = Vector3.ZERO
|
|
gg.car._prev_lv = Vector3.ZERO
|
|
for i in 150:
|
|
await physics_frame
|
|
gg.car.linear_velocity = Vector3.ZERO
|
|
if gg.job_taken:
|
|
break
|
|
_check(gg.job_taken and Garage.ui_customer, "holding by the customer never took the job")
|
|
_check(Garage.ui_fault == cust["fault"], "job fault mismatch")
|
|
gg.free()
|
|
var bucks_before: int = Garage.bucks()
|
|
var rep_before: int = Garage.rep()
|
|
var cw = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
|
root.add_child(cw)
|
|
await process_frame
|
|
_check(cw.customer, "wrench didn't know it was a customer job")
|
|
await _drive_wrench(cw, false)
|
|
_check(cw.done, "customer job never finished (step %d)" % cw.step_index)
|
|
_check(Garage.bucks() > bucks_before, "customer didn't pay")
|
|
_check(Garage.rep() > rep_before, "clean customer job earned no rep")
|
|
_check(Garage.customer().is_empty(), "customer never left the driveway")
|
|
_check(Garage.quality(String(cust["car"]), String(cust["fault"])) == 0.0,
|
|
"customer job polluted own-car quality")
|
|
cw.free()
|
|
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
|
|
# wrench phase 1: the 3D shed jobs. A clean flat-tyre run scores 90+ and
|
|
# buffs the car; a sabotaged torque window strips a thread on the oil job.
|
|
print(" .. wrench")
|
|
Garage.add_fault("datto_120y", "flat")
|
|
Garage.ui_stats_path = "res://cars/datto_120y/stats.tres"
|
|
Garage.ui_fault = "flat"
|
|
var wr = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
|
root.add_child(wr)
|
|
await process_frame
|
|
_check(wr.steps.size() == 7, "flat-tyre job sheet wrong (%d steps)" % wr.steps.size())
|
|
await _drive_wrench(wr, false)
|
|
_check(wr.done, "flat-tyre job never finished (step %d)" % wr.step_index)
|
|
_check(wr.quality >= 90.0, "clean flat run scored %d" % int(wr.quality))
|
|
_check(not Garage.faults("datto_120y").has("flat"), "flat fault not cleared")
|
|
_check(Garage.quality("datto_120y", "flat") >= 85.0, "workmanship not recorded")
|
|
wr.free()
|
|
gg = await _boot("cruise") # 85+ fix = grip buff on the next boot
|
|
var dstats: CarStats = load("res://cars/datto_120y/stats.tres")
|
|
_check(gg.car.stats.grip > dstats.grip + 0.7, "85+ fix paid no grip buff (%.2f vs %.2f)"
|
|
% [gg.car.stats.grip, dstats.grip])
|
|
gg.free()
|
|
Garage.add_fault("datto_120y", "oil")
|
|
Garage.ui_fault = "oil"
|
|
wr = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
|
root.add_child(wr)
|
|
await process_frame
|
|
await _drive_wrench(wr, true)
|
|
_check(wr.done, "oil job never finished (step %d)" % wr.step_index)
|
|
_check(wr.quality <= 90.0, "stripped thread cost nothing (%d)" % int(wr.quality))
|
|
_check(wr.steps.size() == 8, "extraction step never appeared (%d steps)" % wr.steps.size())
|
|
wr.free()
|
|
Garage._reset_for_test() # later stages must run stock
|
|
|
|
# cruise: throttle 3s, car must move
|
|
print(" .. cruise")
|
|
var game: Game = await _boot("cruise")
|
|
if game.car.stats.model_path != "":
|
|
_check(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)
|
|
_check(dist > 5.0, "car didn't move (%.1f m)" % dist)
|
|
Input.action_release("throttle")
|
|
game.free()
|
|
|
|
# speed kit: at pace the lens must widen, the screen pass must engage, the
|
|
# motes must fly and the wind must blow -- all off ONE number (km/h)
|
|
print(" .. speed")
|
|
game = await _boot("cruise")
|
|
_check(game.speed_fx != null and game._rush_p != null, "speed kit not built")
|
|
var fov0: float = game.cam.fov
|
|
for i in 30:
|
|
await physics_frame
|
|
game.car.linear_velocity = -game.car.global_basis.z * 45.0 # ~160 km/h
|
|
await process_frame
|
|
_check(game.cam.fov > fov0 + 8.0, "FOV never opened up (%.1f -> %.1f)" % [fov0, game.cam.fov])
|
|
_check(game.speed_fx.rush > 0.25, "screen rush never engaged (%.2f)" % game.speed_fx.rush)
|
|
_check(game.speed_fx.rect.visible, "screen pass stayed hidden at speed")
|
|
_check(game._rush_p.emitting, "rush motes not emitting at speed")
|
|
var fast_fov: float = game.cam.fov
|
|
game.car.boosting = true
|
|
await process_frame
|
|
_check(game.cam.fov > fast_fov, "boost punch didn't kick the lens")
|
|
game.free()
|
|
|
|
# crash mode: traffic spawned, ram the nearest one, score must rise
|
|
print(" .. crash")
|
|
game = await _boot("crash")
|
|
_check(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")
|
|
_check(not victim.live, "rammed traffic never went dynamic")
|
|
_check(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")
|
|
_check(not jcts.is_empty(), "no woolies junction event")
|
|
Game.junction = jcts[0].data
|
|
print(" .. junction")
|
|
game = await _boot("junction")
|
|
_check(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")
|
|
_check(game.jstate != "run", "never crashed into the convoy")
|
|
_check(game.breaker_used, "crashbreaker never fired")
|
|
_check(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")
|
|
_check(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
|
|
_check(game.outrun_rival == rr, "outrun didn't arm on proximity")
|
|
var bm0: float = game.car.boost_max
|
|
game._outrun_end(true, "test")
|
|
_check(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle")
|
|
_check(game.car.boost_max > bm0, "outrun win paid no boost")
|
|
_check(game.outrun_cd > 0.0, "no re-arm cooldown after outrun")
|
|
game.free()
|
|
|
|
# pursuit: smashing raises heat, heat summons cops, fleeing loses them
|
|
print(" .. pursuit")
|
|
game = await _boot("cruise")
|
|
for i in 10:
|
|
game.add_heat(Game.HEAT_PER_SMASH)
|
|
_check(game.cops.size() >= Game.COPS_MIN, "heat didn't summon cops")
|
|
game.car.global_position += Vector3(900, 0, 900)
|
|
for i in 700:
|
|
await physics_frame
|
|
if game.cops.is_empty():
|
|
break
|
|
_check(game.escapes == 1 and game.cops.is_empty(), "escape never resolved")
|
|
_check(game.heat_cd > 0.0, "no cooldown after escape")
|
|
game.free()
|
|
|
|
# drag: the Willowbank quarter mile -- tree staging, ladder rival, then the box
|
|
Game.level_path = levels["willowbank_dragway"]
|
|
print(" .. drag")
|
|
game = await _boot("drag")
|
|
_check(absf(game.drag_len - 402.0) < 5.0, "quarter mile measures %.0f m" % game.drag_len)
|
|
_check(game.rivals.size() == 1, "drag had no opponent")
|
|
_check(game.drag_state == "stage", "no tree staging phase")
|
|
_check(game.drag_rung_now == 0, "fresh ladder not starting at rung 0")
|
|
_check(game.rivals[0].stats.resource_path.contains("morry"),
|
|
"rung 0 rival isn't Nanna (%s)" % game.rivals[0].stats.resource_path)
|
|
_check(game._tree_bulbs.size() == 6, "christmas tree bulbs missing (%d)" % game._tree_bulbs.size())
|
|
# TRELLIS'd cashies tools: once the library exists, the paddock is strewn
|
|
var tool_dir := DirAccess.open("res://assets/tools")
|
|
if tool_dir != null:
|
|
var glbs := 0
|
|
for f in tool_dir.get_files():
|
|
if f.ends_with(".glb"):
|
|
glbs += 1
|
|
if glbs > 0:
|
|
var strewn := 0
|
|
for n in game.find_children("*", "RigidBody3D", true, false):
|
|
if n.has_meta("cc_tool"):
|
|
strewn += 1
|
|
_check(strewn == glbs, "tool library has %d GLBs but %d spawned" % [glbs, strewn])
|
|
await physics_frame
|
|
_check(game.rivals[0].power_scale == 0.0, "rival not held at the tree")
|
|
# burnout in the box: DRIFT+GAS warms the tyres and must NOT roll the beams
|
|
Input.action_press("drift")
|
|
Input.action_press("throttle")
|
|
for i in 80:
|
|
await physics_frame
|
|
if game.drag_state != "stage":
|
|
break
|
|
_check(game.tyre_temp > 0.25, "burnout warmed nothing (%.2f)" % game.tyre_temp)
|
|
_check(not game.jumped, "burnout rolled the beams (red light)")
|
|
_check(game.car.burnout_on, "burnout fx never engaged")
|
|
Input.action_release("drift")
|
|
Input.action_release("throttle")
|
|
# hold still like a disciplined launch: no red light, wait out the ambers
|
|
var tree_spin := 0
|
|
while game.drag_state != "run" and tree_spin < 400:
|
|
await physics_frame
|
|
tree_spin += 1
|
|
_check(game.drag_state == "run", "tree never went green")
|
|
_check(not game.jumped, "stationary car scored a red light")
|
|
var green_m := (game._tree_bulbs["TreeGreen"] as MeshInstance3D).material_override as StandardMaterial3D
|
|
_check(green_m.emission_enabled, "green bulb didn't light")
|
|
Input.action_press("throttle")
|
|
for i in 10:
|
|
await physics_frame
|
|
_check(game.revs > 0.0, "revs never climbed")
|
|
var g0: int = game.gear
|
|
# shift the moment revs enter the window. First gear redlines in ~1.6 s, so
|
|
# idling here first pins revs at 1.0 and the shift correctly scores as blown.
|
|
var spin := 0
|
|
while game.revs < Game.SHIFT_LO and spin < 600: # bounded: never hang the suite
|
|
await physics_frame
|
|
spin += 1
|
|
_check(game.revs >= Game.SHIFT_LO and game.revs < Game.REDLINE,
|
|
"revs %.2f not inside the shift window [%.2f, %.2f)" % [game.revs, Game.SHIFT_LO, Game.REDLINE])
|
|
Input.action_press("boost")
|
|
await physics_frame
|
|
Input.action_release("boost")
|
|
await physics_frame
|
|
_check(game.gear == g0 + 1, "shift didn't change gear")
|
|
_check(game.clean_shifts == 1 and game.blown_shifts == 0,
|
|
"in-window shift scored as blown (%d/%d)" % [game.clean_shifts, game.blown_shifts])
|
|
# grenade the box: three panic shifts nowhere near the window
|
|
for i in 3:
|
|
game.revs = 0.10
|
|
Input.action_press("boost")
|
|
await physics_frame
|
|
Input.action_release("boost")
|
|
await physics_frame
|
|
_check(game.box_blown, "gearbox survived three blown shifts (%d strikes)" % game.box_strikes)
|
|
_check(game.car.leaking, "grenaded box isn't leaking oil")
|
|
var locked: int = game.gear
|
|
Input.action_press("boost")
|
|
await physics_frame
|
|
Input.action_release("boost")
|
|
await physics_frame
|
|
_check(game.gear == locked, "blown box still shifts")
|
|
# and cook the coolant: past 1.0 the head gasket lets go
|
|
game.eng_temp = 1.05
|
|
await physics_frame
|
|
_check(game.gasket_blown, "overcooked engine kept its head gasket")
|
|
# finishing the run sends the carnage home as persistent faults
|
|
game._drag_finish(true)
|
|
_check(Garage.faults("datto_120y").has("gearbox") and Garage.faults("datto_120y").has("gasket"),
|
|
"carnage didn't follow the car home as faults")
|
|
Garage._reset_for_test() # don't leave a wounded car for later stages
|
|
Input.action_release("throttle")
|
|
game.free()
|
|
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
|
|
|
|
# derby: opponents spawn in a ring and each hunts something
|
|
print(" .. derby")
|
|
game = await _boot("derby")
|
|
_check(game.derby.size() == Game.DERBY_CARS, "derby field wrong size")
|
|
await physics_frame
|
|
for d in game.derby:
|
|
_check(d.target != null and d.target != d, "derby car has no/self target")
|
|
game.free()
|
|
|
|
# road rage: rivals spawn, a takedown near the player counts and pays
|
|
print(" .. rage")
|
|
game = await _boot("rage")
|
|
_check(game.rivals.size() == Game.RAGE_RIVALS, "rage field wrong size")
|
|
var prey: Rival = game.rivals[0]
|
|
await physics_frame
|
|
game.car.global_position = prey.global_position + Vector3(3, 0.2, 0)
|
|
await physics_frame
|
|
prey.wreck_started.emit()
|
|
_check(game.rage_downs == 1, "rage takedown didn't count (%d)" % game.rage_downs)
|
|
game.time_left = 0.05
|
|
for i in 10:
|
|
await physics_frame
|
|
_check(game.over, "rage never tallied")
|
|
game.free()
|
|
|
|
# oncoming: rolling against the road tangent at speed earns boost
|
|
game = await _boot("cruise")
|
|
await physics_frame
|
|
var curve := game.race_path.curve
|
|
var road_pt := game.race_path.to_global(curve.sample_baked(20.0))
|
|
var tan := (game.race_path.to_global(curve.sample_baked(24.0)) - road_pt).normalized()
|
|
# Teleport via the physics server: setting global_position on a
|
|
# continuous_cd body SWEEPS it there, slamming through everything en route
|
|
# -- the first version of this test wrecked the car before it could earn.
|
|
var xf := Transform3D(Basis.looking_at(-tan, Vector3.UP), road_pt + Vector3.UP * 0.75)
|
|
PhysicsServer3D.body_set_state(game.car.get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, xf)
|
|
game.car.wrecked = false
|
|
game.car.boost = 0.0
|
|
game.car._prev_lv = -tan * 16.0 # don't let the velocity jump read as a crash
|
|
game.car.linear_velocity = -tan * 16.0
|
|
for i in 45:
|
|
await physics_frame
|
|
game.car.linear_velocity = -tan * 16.0 # hold course against drag
|
|
_check(game.car.boost > 0.05, "oncoming earned no boost (%.3f)" % game.car.boost)
|
|
game.free()
|
|
|
|
# race: grid up at Queensland Raceway -- the paperclip is ~3126 m of Path3D
|
|
Game.level_path = levels["qld_raceway"]
|
|
print(" .. race")
|
|
game = await _boot("race")
|
|
var lap: float = game.race_path.curve.get_baked_length()
|
|
_check(absf(lap - 3126.0) < 60.0, "paperclip lap measures %.0f m" % lap)
|
|
_check(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
|
|
_check(moved, "no rival is driving")
|
|
game.free()
|
|
|
|
print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | pursuit spawns+escapes | quarter mile stages+shifts | derby %d cars | paperclip race AI driving" %
|
|
[cars.size(), levels.size(), dist, int(crash_score), int(jct_score), Game.DERBY_CARS])
|
|
quit(0)
|
|
|
|
func _drive_wrench(wr, sabotage: bool) -> void:
|
|
## Plays any wrench job through its API. sabotage blows the first torque
|
|
## window wide open to prove that threads strip.
|
|
var guard := 0
|
|
var sabotaged := false
|
|
while not wr.done and guard < 30:
|
|
guard += 1
|
|
var st: Dictionary = wr.steps[wr.step_index]
|
|
var need: float = (float(st.get("turns", 0.0)) + wr.penalty_turns) * TAU
|
|
match String(st["verb"]):
|
|
"diagnose":
|
|
for i in wr.light_options.size():
|
|
if wr.light_options[i] == Garage.FAULTS[wr.fault]["light"]:
|
|
wr.pick_light(i)
|
|
break
|
|
"pump":
|
|
for i in int(st["clicks"]):
|
|
wr.pump()
|
|
"twist":
|
|
if bool(st.get("window", false)):
|
|
if sabotage and not sabotaged:
|
|
sabotaged = true
|
|
wr.twist(float(st["dir"]) * (need + 4.0))
|
|
else:
|
|
wr.twist(float(st["dir"]) * (need + 0.3))
|
|
wr.twist_release()
|
|
else:
|
|
wr.twist(float(st["dir"]) * (need + 0.2))
|
|
"yank":
|
|
wr.yank_ok()
|
|
"grab":
|
|
wr.grab(String(st["item"]))
|
|
wr.drop_at(wr._drop_target(String(st["at"])))
|
|
"steady":
|
|
while not wr.done and wr.steps[wr.step_index]["verb"] == "steady":
|
|
wr.steady_tick(true, 0.5)
|
|
"clamps":
|
|
wr.clamp_on("red")
|
|
wr.clamp_on("black")
|
|
"wait":
|
|
wr.skip_wait()
|
|
await physics_frame
|
|
await physics_frame
|
|
|
|
func _check(cond: bool, msg: String) -> void:
|
|
## NOT assert(): a failed assert in a headless SceneTree script halts the
|
|
## script but leaves the tree running, so the process spins forever instead
|
|
## of failing. One bad assertion cost 53 minutes of wall clock before this.
|
|
if cond:
|
|
return
|
|
push_error(msg)
|
|
print("SMOKE FAIL: %s" % msg)
|
|
quit(1)
|
|
|
|
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
|