Mechanical carnage: burnouts, grenaded gearboxes, blown head gaskets

The drag strip now has consequences on both sides of the green.

Burnouts: DRIFT+GAS in the staging box lights up the rears -- fat white
FLUX-free tyre smoke (new Fx.burnout emitter), fake rear-wheel spin via
car.spin_override, the skid loop singing while stationary -- and builds
TYRE TEMP. Warm tyres hook up off the line, cold ones spin the launch
away: a traction factor (0.78 cold to 1.20 hot) applies for the first
150 m. The burnout holds the car with power_scale 0, so it can never
red-light you; releasing into a creep still can, as is right.

Gearbox: blown shifts are now strikes. Three in one run and the box
grenades -- a panel bangs off the back (car.mech_pop), the lever goes
dead (stuck in whatever gear you murdered it into, 55% drive), and the
car starts LEAKING: oil patches spawn behind it every 7 m of travel and
sit on the strip for 30 s. Everyone can see what you did.

Coolant: riding the limiter cooks ENG TEMP at 0.30/s (boost adds a
little; driving clean cools). At 100% the head gasket lets go: a steam
jet out of the bonnet (Fx.steamer), 72% power for the rest of the run,
and COOLANT EVERYWHERE. Sitting on the redline refusing to shift is now
a self-solving problem.

HUD shows tyre temp in staging and OVERHEAT/BOX GRENADED/GASKET
STEAMING flags in the run; the result screen commemorates both failures.
Smoke suite: burnout warms without tripping the beams, three panic
shifts grenade the box and lock the gear, the leak flag sets, and a
forced overtemp pops the gasket.

Next: these failures become persistent per-car faults you repair in the
shed with a drag/click/twist minigame and the Cashies code reader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-31 12:26:57 +10:00
parent 21032191a3
commit a8fed95d42
5 changed files with 207 additions and 10 deletions

10
push16.log Normal file
View File

@ -0,0 +1,10 @@
Enumerating objects: 37, done.
Counting objects: 2% (1/37) Counting objects: 5% (2/37) Counting objects: 8% (3/37) Counting objects: 10% (4/37) Counting objects: 13% (5/37) Counting objects: 16% (6/37) Counting objects: 18% (7/37) Counting objects: 21% (8/37) Counting objects: 24% (9/37) Counting objects: 27% (10/37) Counting objects: 29% (11/37) Counting objects: 32% (12/37) Counting objects: 35% (13/37) Counting objects: 37% (14/37) Counting objects: 40% (15/37) Counting objects: 43% (16/37) Counting objects: 45% (17/37) Counting objects: 48% (18/37) Counting objects: 51% (19/37) Counting objects: 54% (20/37) Counting objects: 56% (21/37) Counting objects: 59% (22/37) Counting objects: 62% (23/37) Counting objects: 64% (24/37) Counting objects: 67% (25/37) Counting objects: 70% (26/37) Counting objects: 72% (27/37) Counting objects: 75% (28/37) Counting objects: 78% (29/37) Counting objects: 81% (30/37) Counting objects: 83% (31/37) Counting objects: 86% (32/37) Counting objects: 89% (33/37) Counting objects: 91% (34/37) Counting objects: 94% (35/37) Counting objects: 97% (36/37) Counting objects: 100% (37/37) Counting objects: 100% (37/37), done.
Delta compression using up to 32 threads
Compressing objects: 4% (1/24) Compressing objects: 8% (2/24) Compressing objects: 12% (3/24) Compressing objects: 16% (4/24) Compressing objects: 20% (5/24) Compressing objects: 25% (6/24) Compressing objects: 29% (7/24) Compressing objects: 33% (8/24) Compressing objects: 37% (9/24) Compressing objects: 41% (10/24) Compressing objects: 45% (11/24) Compressing objects: 50% (12/24) Compressing objects: 54% (13/24) Compressing objects: 58% (14/24) Compressing objects: 62% (15/24) Compressing objects: 66% (16/24) Compressing objects: 70% (17/24) Compressing objects: 75% (18/24) Compressing objects: 79% (19/24) Compressing objects: 83% (20/24) Compressing objects: 87% (21/24) Compressing objects: 91% (22/24) Compressing objects: 95% (23/24) Compressing objects: 100% (24/24) Compressing objects: 100% (24/24), done.
Writing objects: 3% (1/26) Writing objects: 7% (2/26) Writing objects: 11% (3/26) Writing objects: 15% (4/26) Writing objects: 19% (5/26) Writing objects: 23% (6/26) Writing objects: 26% (7/26) Writing objects: 34% (9/26) Writing objects: 38% (10/26) Writing objects: 42% (11/26) Writing objects: 46% (12/26) Writing objects: 50% (13/26) Writing objects: 53% (14/26) Writing objects: 61% (16/26) Writing objects: 65% (17/26) Writing objects: 69% (18/26) Writing objects: 73% (19/26) Writing objects: 76% (20/26) Writing objects: 80% (21/26) Writing objects: 84% (22/26) Writing objects: 88% (23/26) Writing objects: 92% (24/26) Writing objects: 96% (25/26) Writing objects: 100% (26/26) Writing objects: 100% (26/26), 312.96 KiB | 39.12 MiB/s, done.
Total 26 (delta 13), reused 0 (delta 0), pack-reused 0 (from 0)
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://100.71.119.27:222/monster/burnoutinfinityshitbox.git
fdbb921..17aabd7 main -> main

View File

@ -43,6 +43,9 @@ var boosting := false
var slip := 0.0 # sideways m/s
var wrecked := false
var spawn_xform: Transform3D
var spin_override := 0.0 # extra rear-wheel spin (rad/s) for burnouts
var burnout_on := false # keeps the skid loop singing while stationary
var leaking := false # a grenaded gearbox drips oil patches behind
var _prev_lv := Vector3.ZERO
var _impact_cd := 0.0
@ -54,6 +57,9 @@ var _snd_boost: AudioStreamPlayer3D
var _vis_wheels: Array = [] # {node, front, radius, spin}
var _smoke: GPUParticles3D
var _dust: GPUParticles3D
var _burn: GPUParticles3D # burnout tyre smoke, made lazily on first use
var _steam: GPUParticles3D # head-gasket steam, likewise
var _leak_accum := 0.0
@onready var wheels: Array[Node] = [$FL, $FR, $RL, $RR]
@ -158,7 +164,8 @@ func _physics_process(delta: float) -> void:
+ (0.25 if boosting else 0.0)
_snd_eng.volume_db = (-9.0 if is_player else -17.0) + absf(throttle_in) * 4.0
if _snd_skid:
var skidding := grounded >= 2 and absf(slip) > 4.0 and linear_velocity.length() > 8.0
var skidding := (grounded >= 2 and absf(slip) > 4.0 and linear_velocity.length() > 8.0) \
or burnout_on
if skidding and not _snd_skid.playing:
_snd_skid.play()
elif not skidding and _snd_skid.playing:
@ -173,8 +180,14 @@ func _physics_process(delta: float) -> void:
var fwd_speed := -global_basis.z.dot(linear_velocity)
for w in _vis_wheels:
w["spin"] = fposmod(w["spin"] + fwd_speed / w["radius"] * delta, TAU)
var rate: float = fwd_speed / w["radius"] + (0.0 if w["front"] else spin_override)
w["spin"] = fposmod(w["spin"] + rate * delta, TAU)
(w["node"] as Node3D).rotation = Vector3(w["spin"], steer if w["front"] else 0.0, 0.0)
if leaking:
_leak_accum += linear_velocity.length() * delta
if _leak_accum > 7.0:
_leak_accum = 0.0
_drip()
if _smoke:
_smoke.emitting = wrecked or (_deform != null and _deform.damage > 0.7)
if _dust:
@ -190,6 +203,45 @@ func _physics_process(delta: float) -> void:
func earn_boost(amount: float) -> void:
boost = minf(boost + amount, boost_max)
func set_burnout(on: bool) -> void:
## Lighting up the rears in the box: fake wheel spin, white smoke, skid loop.
burnout_on = on
spin_override = 34.0 if on else 0.0
if on and _burn == null:
_burn = Fx.burnout(self, Vector3(0, 0.15, stats.length * 0.42))
if _burn:
_burn.emitting = on
func set_steam(on: bool) -> void:
## The head gasket has left the building; the bonnet announces it.
if on and _steam == null:
_steam = Fx.steamer(self, Vector3(0, 0.55, -stats.length * 0.38))
if _steam:
_steam.emitting = on
func mech_pop() -> void:
## A mechanical letting-go (grenaded gearbox and friends): one panel off
## the back, sparks, a low clunk -- damage without a wreck.
Fx.sparks(self, 30)
_shed_panel(Vector3(0, 0, 1))
Sfx.shot(self, "clunk", 0.0, 0.6)
func _drip() -> void:
# an oil patch on the road behind us; the strip remembers what you did
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = Vector3(0.55, 0.012, 0.95)
var m := StandardMaterial3D.new()
m.albedo_color = Color(0.05, 0.045, 0.03)
m.roughness = 0.25
m.metallic = 0.4
bm.material = m
mi.mesh = bm
get_parent().add_child(mi)
mi.global_position = global_position + Vector3(0, -0.52, 0)
mi.rotation.y = rotation.y
get_tree().create_timer(30.0).timeout.connect(mi.queue_free)
func _impact(jolt: Vector3) -> void:
var dv := jolt.length()
crashed.emit(dv)

View File

@ -172,3 +172,54 @@ static func duster(parent: Node3D, offset: Vector3) -> GPUParticles3D:
p.position = offset
parent.add_child(p)
return p
static var _burnout_mat: ParticleProcessMaterial
static var _steam_mat: ParticleProcessMaterial
static func burnout(parent: Node3D, offset: Vector3) -> GPUParticles3D:
## The good stuff: fat white tyre smoke for the burnout box. Caller toggles.
_init_shared()
if _burnout_mat == null:
_burnout_mat = ParticleProcessMaterial.new()
_burnout_mat.direction = Vector3(0, 1, 0)
_burnout_mat.spread = 70.0
_burnout_mat.initial_velocity_min = 1.5
_burnout_mat.initial_velocity_max = 3.6
_burnout_mat.gravity = Vector3(0, 1.4, 0)
_burnout_mat.scale_min = 1.2
_burnout_mat.scale_max = 2.6
_burnout_mat.color_ramp = _ramp([Color(0.92, 0.92, 0.92, 0.0),
Color(0.88, 0.88, 0.88, 0.6), Color(0.80, 0.80, 0.80, 0.0)])
var p := GPUParticles3D.new()
p.process_material = _burnout_mat
p.draw_pass_1 = _smoke_draw
p.amount = 60
p.lifetime = 1.7
p.emitting = false
p.position = offset
parent.add_child(p)
return p
static func steamer(parent: Node3D, offset: Vector3) -> GPUParticles3D:
## Head-gasket steam: a thin hot jet out of the bonnet. Caller toggles.
_init_shared()
if _steam_mat == null:
_steam_mat = ParticleProcessMaterial.new()
_steam_mat.direction = Vector3(0, 1, 0)
_steam_mat.spread = 12.0
_steam_mat.initial_velocity_min = 3.5
_steam_mat.initial_velocity_max = 5.5
_steam_mat.gravity = Vector3(0, 2.5, 0)
_steam_mat.scale_min = 0.5
_steam_mat.scale_max = 1.1
_steam_mat.color_ramp = _ramp([Color(0.95, 0.96, 0.98, 0.0),
Color(0.93, 0.94, 0.96, 0.55), Color(0.9, 0.9, 0.92, 0.0)])
var p := GPUParticles3D.new()
p.process_material = _steam_mat
p.draw_pass_1 = _smoke_draw
p.amount = 34
p.lifetime = 1.1
p.emitting = false
p.position = offset
parent.add_child(p)
return p

View File

@ -114,6 +114,12 @@ var rival_et := 0.0 # rival's ET once it crosses; 0 = hasn't yet
var rival_wait := 0.0 # grace timer after the rival crosses first
var _tree_bulbs := {} # "TreeGreen" etc -> MeshInstance3D (dragway only)
var _launch_pos := Vector3.ZERO
## Mechanical sympathy: none of these reset until the scene does.
var tyre_temp := 0.0 # 0 cold .. 1 hot, built in the burnout box
var eng_temp := 0.0 # limiter abuse; at 1.0 the head gasket lets go
var box_strikes := 0 # blown shifts this run; 3 grenades the gearbox
var box_blown := false
var gasket_blown := false
var derby: Array[Cop] = [] # arena opponents; Cop AI already hunts a target
var derby_centre: Vector3
@ -787,27 +793,53 @@ func _drag_tick(delta: float) -> void:
if gear <= DRAG_GEARS:
revs = minf(revs + DRAG_REV_RATE[gear] * delta * (1.0 if bog == 0.0 else 0.25), 1.0)
if Input.is_action_just_pressed("boost"):
if Input.is_action_just_pressed("boost") and not box_blown:
if revs >= SHIFT_LO and revs < REDLINE:
clean_shifts += 1
Sfx.shot(car, "clunk", -2.0)
_msg("CLEAN SHIFT", 34)
else:
blown_shifts += 1
box_strikes += 1
bog = 0.9
Sfx.shot(car, "bog", -2.0)
_msg("MISSED IT", 34)
if box_strikes >= 3:
# third mistreated shift: the box keeps what's in it
box_blown = true
car.mech_pop()
car.leaking = true
_msg("GEARBOX GRENADED!\nSTUCK IN %d, LEAKING" % mini(gear + 1, DRAG_GEARS), 44)
else:
_msg("MISSED IT", 34)
gear += 1
revs = 0.28 if gear <= DRAG_GEARS else 1.0
elif revs >= REDLINE:
bog = maxf(bog, 0.35) # sitting on the limiter goes nowhere
# coolant: riding the limiter cooks the engine; at 1.0 the head gasket goes
if gear <= DRAG_GEARS and revs >= REDLINE:
eng_temp += delta * 0.30
elif car.boosting:
eng_temp = minf(eng_temp + delta * 0.06, 0.99 if not gasket_blown else 1.0)
else:
eng_temp = maxf(eng_temp - delta * 0.04, 0.0)
if eng_temp >= 1.0 and not gasket_blown:
gasket_blown = true
car.set_steam(true)
Sfx.shot(car, "bog", -2.0, 0.55)
_msg("HEAD GASKET!\nCOOLANT EVERYWHERE", 44)
# drive comes from the box, not the throttle: clean shifts pay, bogging hurts
var tune := 1.0 + clean_shifts * 0.07 - blown_shifts * 0.05
# launch traction: warm tyres hook up, cold ones spin it away off the line
var gone := Vector3(car.global_position.x, 0, car.global_position.z).distance_to(
Vector3(_launch_pos.x, 0, _launch_pos.z))
var traction := (0.78 + 0.42 * tyre_temp) if gone < 150.0 else 1.0
# clamp the gear term: gear runs to DRAG_GEARS+1 as the "past top" sentinel,
# and un-clamped that paid 6/5 -- more grunt than top gear for shifting into nothing
car.power_scale = (0.35 if bog > 0.0 else tune) \
* (0.75 + 0.25 * mini(gear, DRAG_GEARS) / float(DRAG_GEARS))
* (0.75 + 0.25 * mini(gear, DRAG_GEARS) / float(DRAG_GEARS)) \
* traction * (0.55 if box_blown else 1.0) * (0.72 if gasket_blown else 1.0)
var flat := Vector3(car.global_position.x, 0.0, car.global_position.z)
var left := flat.distance_to(drag_end)
@ -816,12 +848,19 @@ func _drag_tick(delta: float) -> void:
if rf.distance_to(drag_end) < 12.0:
rival_et = drag_t
var pb := Garage.drag_pb(Garage.car_id_of(car_path))
info_label.text = "VS %s%s\nGEAR %d %s %dm %.1fs" % [
var mech := ""
if box_blown:
mech += " BOX GRENADED"
if gasket_blown:
mech += " GASKET STEAMING"
elif eng_temp > 0.6:
mech += " ENG %d%%!" % int(eng_temp * 100)
info_label.text = "VS %s%s\nGEAR %d %s %dm %.1fs%s" % [
DRAG_LADDER[drag_rung_now]["name"],
(" | PB %.2f" % pb) if pb > 0.0 else "",
mini(gear, DRAG_GEARS),
("BOGGED" if bog > 0.0 else ("SHIFT!" if revs >= SHIFT_LO else "%d%%" % int(revs * 100))),
int(left), drag_t]
int(left), drag_t, mech]
# the event ends when YOU cross: losing to the rival mustn't rob you of your
# ET and PB. If the rival's crossed and you dawdle 10 s, it gets called.
if rival_et > 0.0:
@ -838,7 +877,16 @@ func _stage_tick(delta: float) -> void:
stage_t += delta
if not rivals.is_empty():
rivals[0].power_scale = 0.0
car.power_scale = 0.8
# the burnout box: DRIFT+GAS lights up the rears -- smoke pours, tyres warm,
# and the handbrake holds you off the beams so it can't red-light you
var burning := Input.is_action_pressed("drift") \
and Input.get_axis("brake", "throttle") > 0.2 and not car.wrecked
car.set_burnout(burning)
if burning:
car.power_scale = 0.0
tyre_temp = minf(tyre_temp + delta * 0.30, 1.0)
else:
car.power_scale = 0.8
if was <= 0.0:
_bulb("TreeStage", true)
for i in TREE_AMBERS.size():
@ -852,10 +900,11 @@ func _stage_tick(delta: float) -> void:
_msg("RED LIGHT", 44)
var rung: Dictionary = DRAG_LADDER[drag_rung_now]
var pb := Garage.drag_pb(Garage.car_id_of(car_path))
info_label.text = "VS %s%s\nHOLD FOR THE GREEN..." % [rung["name"],
(" | PB %.2fs" % pb) if pb > 0.0 else ""]
info_label.text = "VS %s%s\nTYRES %d%% (DRIFT+GAS to burn out)\nHOLD FOR THE GREEN..." % [
rung["name"], (" | PB %.2fs" % pb) if pb > 0.0 else "", int(tyre_temp * 100)]
if stage_t >= TREE_GREEN:
drag_state = "run"
car.set_burnout(false)
for i in 3:
_bulb("TreeAmber%d" % (i + 1), false)
_bulb("TreeGreen", true)
@ -890,6 +939,10 @@ func _drag_finish(crossed: bool) -> void:
lines.append("TOP OF THE LADDER. AGAIN?")
if jumped:
lines.append("(red light. we saw it. everyone saw it.)")
if box_blown:
lines.append("(the gearbox is on the strip somewhere behind you)")
if gasket_blown:
lines.append("(and the radiator's boiled itself dry)")
Garage.earn(pay)
lines.append("+$%d bucks | %d clean / %d blown" % [pay, clean_shifts, blown_shifts])
lines.append("R to retry, Esc for menu")

View File

@ -175,6 +175,18 @@ func _run() -> void:
_check(game._tree_bulbs.size() == 6, "christmas tree bulbs missing (%d)" % game._tree_bulbs.size())
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:
@ -204,6 +216,25 @@ func _run() -> void:
_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")
Input.action_release("throttle")
game.free()
Game.level_path = levels.get("woolies_carpark", levels.values()[0])