Wrench phase 3: driveway customers -- the shed becomes a business

Visit The Shed and one of the locals is waiting on the driveway with a
broken shitbox and cash: their actual car model parked there under a
floating Label3D job sign -- who they are, what they reckon is wrong
("it makes a noise at church speed"), the honest fault name, and the
offer. Pull up beside it and hold still to take the job; the wrench
minigame opens on THEIR car.

The roster is the extended universe: Nanna, P-Plate Pepper, Mullet
Merv, Torque'n Tezza, the Self-Described Bathurst Legend, Your Mate
Dazza (don't tell Shazza), Karen from Number 12, and the Widowmaker,
who says nothing and pays cash. Roster and fault rotate
deterministically off jobs_done.

Settlement: full pay at 60+, a $50 tip at 85+, half below 60 -- and
SHED REP moves with the workmanship (+3 clean, -6 botched, cap 40),
scaling the next offer from 70% of shop rate at rep 0 to 100% at cap.
Rep shows on the menu and garage header. Customer jobs bank NO quality
buffs: the customer's car id might be a car the player owns, and
fixing Merv's Kingswood must not secretly tune yours -- the wrench
consumes Garage.ui_customer and routes _finish to customer_done()
instead of clear_fault/set_quality.

The shed guards the spawn (parent is Game and mode == cruise) so the
wrench scene's backdrop copy of the level never summons a customer
mid-job. The spare parked shitbox moves to the kerb; the driveway
belongs to commerce now.

Smoke: customer waits after a shed visit, holding beside the spot takes
the job (flag-guarded for headless), the driven wrench job pays, rep
rises, the driveway clears, and the customer's own-car quality stays
untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-31 19:12:15 +10:00
parent 89b9106f88
commit 955413b735
8 changed files with 193 additions and 12 deletions

View File

@ -182,8 +182,20 @@ into BRRRT clicks** (torque windows keep the hand skill -- guns strip), ramps
speed the jacking, the fluids shelf speeds pours, and the tier-3 scan tool speed the jacking, the fluids shelf speeds pours, and the tier-3 scan tool
skips diagnosis entirely. Workmanship reaches the strip: an 85+ gearbox skips diagnosis entirely. Workmanship reaches the strip: an 85+ gearbox
rebuild takes an **extra blown shift** before grenading, an 85+ gasket job rebuild takes an **extra blown shift** before grenading, an 85+ gasket job
**raises the overheat ceiling** -- both asserted in the suite. Phase 3 is **raises the overheat ceiling** -- both asserted in the suite.
driveway customers.
Phase 3 makes the shed a business: **driveway customers**. Visit The Shed and
one of the locals is waiting on the driveway -- Nanna's Morry ("it makes a
noise at church speed"), Your Mate Dazza's ute ("don't tell Shazza"), Karen
from Number 12 ("I will be watching. the entire time") -- their actual car
parked there with a floating job sign naming the fault and the offer. Pull up
beside it and hold still to take the job; the wrench opens on THEIR car. Cash
on completion, a $50 tip at 85+, half pay below 60 -- and **SHED REP** moves
with the workmanship (+3 clean, -6 botched, capped at 40), scaling what the
next customer offers (70% of shop rate at rep 0 up to 100% at cap). Roster
and fault rotate deterministically off jobs done. Customer jobs bank no
quality buffs -- their car id might be a car you own, and fixing Merv's
Kingswood must not secretly tune yours.
**The Shed is also a place** (`levels/the_shed`): a drivable suburban home base **The Shed is also a place** (`levels/the_shed`): a drivable suburban home base
— brick house, paling fence, hills hoist mid-spin with the washing still on it, — brick house, paling fence, hills hoist mid-spin with the washing still on it,

View File

@ -55,11 +55,43 @@ func _ready() -> void:
m.name = "WB_%d" % i m.name = "WB_%d" % i
m.position = Vector3(18.8, 0, 17.0 - i * 1.4) m.position = Vector3(18.8, 0, 17.0 - i * 1.4)
add_child(m) add_child(m)
# the spare shitbox parks on the street now -- the driveway is for customers
var pp := Marker3D.new() var pp := Marker3D.new()
pp.name = "PP_drive" pp.name = "PP_kerb"
pp.position = Vector3(12, 0, 3) pp.position = Vector3(-8, 0, 30.5)
pp.rotate_y(0.06) pp.rotate_y(PI * 0.5 + 0.04)
add_child(pp) add_child(pp)
# a customer waits on the driveway (real play only: the wrench scene uses
# this level as backdrop and must never summon one mid-job)
if get_parent() is Game and Game.mode == "cruise":
_customer()
func _customer() -> void:
var cust := Garage.customer_arrive()
if cust.is_empty():
return
var cars := Registry.cars()
if cars.has(cust["car"]):
var st: CarStats = load(cars[cust["car"]])
if st.model_path != "" and ResourceLoader.exists(st.model_path):
var m := (load(st.model_path) as PackedScene).instantiate()
add_child(m)
m.position = Vector3(12, 0.72, 6.5)
m.rotate_y(0.12)
var spot := Marker3D.new()
spot.name = "CustomerSpot"
spot.position = Vector3(12, 0, 6.5)
add_child(spot)
var sign := Label3D.new()
sign.text = "%s\n\"%s\"\n%s -- $%d" % [cust["name"], cust["blurb"],
Garage.FAULTS[cust["fault"]]["name"], cust["pay"]]
sign.font_size = 64
sign.pixel_size = 0.004
sign.billboard = BaseMaterial3D.BILLBOARD_ENABLED
sign.modulate = Color(1, 0.85, 0.4)
sign.outline_size = 12
sign.position = Vector3(12, 2.7, 6.5)
add_child(sign)
func _process(delta: float) -> void: func _process(delta: float) -> void:
if hoist: if hoist:

View File

@ -140,6 +140,9 @@ var _oncoming_toast := 0.0
var garage_bay: Node3D = null # The Shed's bay: roll in, stop, garage opens var garage_bay: Node3D = null # The Shed's bay: roll in, stop, garage opens
var bay_t := 0.0 var bay_t := 0.0
var garage_requested := false var garage_requested := false
var customer_spot: Node3D = null # the driveway job: stop beside it to accept
var cust_t := 0.0
var job_taken := false
var car: Car var car: Car
var cam: Camera3D var cam: Camera3D
@ -196,6 +199,7 @@ func _ready() -> void:
race_path = path race_path = path
garage_bay = level.find_child("GarageBay", true, false) garage_bay = level.find_child("GarageBay", true, false)
customer_spot = level.find_child("CustomerSpot", true, false)
car = (load("res://src/car.tscn") as PackedScene).instantiate() car = (load("res://src/car.tscn") as PackedScene).instantiate()
car.stats = load(car_path) car.stats = load(car_path)
@ -740,6 +744,27 @@ func _physics_process(delta: float) -> void:
get_tree().change_scene_to_file("res://src/garage.tscn") get_tree().change_scene_to_file("res://src/garage.tscn")
else: else:
bay_t = 0.0 bay_t = 0.0
if mode == "cruise" and customer_spot != null and not job_taken and not car.wrecked:
# the driveway customer: pull up beside their heap, hold still, job's yours
var cd := car.global_position.distance_to(customer_spot.global_position)
if cd < 5.0 and car.linear_velocity.length() < 1.5:
var was_c := cust_t
cust_t += delta
var cust := Garage.customer()
if not cust.is_empty():
if was_c < 0.3 and cust_t >= 0.3:
_msg("%s: \"%s\"\nHOLD STILL TO TAKE THE JOB ($%d)"
% [cust["name"], cust["blurb"], cust["pay"]], 32)
if cust_t >= 1.4:
job_taken = true
Garage.ui_stats_path = Registry.cars()[cust["car"]]
Garage.ui_fault = cust["fault"]
Garage.ui_customer = true
Sfx.shot_2d("clunk", -4.0)
if get_tree().current_scene != null:
get_tree().change_scene_to_file("res://src/wrench.tscn")
else:
cust_t = 0.0
if mode == "drag" and not over: if mode == "drag" and not over:
_drag_tick(delta) _drag_tick(delta)
if mode == "rage" and not over: if mode == "rage" and not over:

View File

@ -16,6 +16,7 @@ const SAVE := "user://garage.save"
static var save_path := SAVE static var save_path := SAVE
static var ui_stats_path := "" # menu -> garage screen: which car is selected static var ui_stats_path := "" # menu -> garage screen: which car is selected
static var ui_fault := "" # garage screen -> repair minigame: which fault static var ui_fault := "" # garage screen -> repair minigame: which fault
static var ui_customer := false # wrench job is a CUSTOMER's car, not yours
## id -> {name, desc, price, effects, build (visual constructor)} ## id -> {name, desc, price, effects, build (visual constructor)}
## Effects: power_mult (multiplies engine_power), grip_add, boost_add (base ## Effects: power_mult (multiplies engine_power), grip_add, boost_add (base
@ -303,6 +304,70 @@ static func add_fault(car_id: String, fault: String) -> void:
_data["faults"] = f _data["faults"] = f
_flush() _flush()
## ---- driveway customers: the shed becomes a business -----------------------
## Locals turn up at The Shed with a broken shitbox and cash. Fix it in the
## wrench minigame; a clean job (85+) builds REP, a botch (<60) bleeds it, and
## rep scales what the next customer offers. Roster and fault rotate
## deterministically off jobs_done, so the queue is fair and testable.
const CUSTOMERS := [
{"name": "NANNA (SHOPPING RUN)", "car": "morry", "blurb": "it makes a noise at church speed"},
{"name": "P-PLATE PEPPER", "car": "excel_x3", "blurb": "mum says it's fine. it is not fine"},
{"name": "MULLET MERV", "car": "kingswood_hq", "blurb": "she's mint mate. just needs a tickle"},
{"name": "TORQUE'N TEZZA", "car": "vl_terbo", "blurb": "started making the EXPENSIVE noise"},
{"name": "SELF-DESCRIBED BATHURST LEGEND", "car": "floored_xd", "blurb": "raced Brocky once. anyway. it's cooked"},
{"name": "YOUR MATE DAZZA", "car": "hardon_wb_ute", "blurb": "mate. MATE. don't tell Shazza"},
{"name": "KAREN FROM NUMBER 12", "car": "chaebol_90", "blurb": "I will be watching. the entire time"},
{"name": "THE WILLOWBANK WIDOWMAKER", "car": "falcodore_gtho", "blurb": "... (pays cash)"},
]
const REP_CAP := 40
static func rep() -> int:
_load()
return int(_data.get("rep", 0))
static func customer() -> Dictionary:
_load()
return _data.get("customer", {})
static func customer_arrive() -> Dictionary:
## One waiting customer at a time; returns the current or a fresh one.
## Pay offer = shop rate scaled by rep (70% at rep 0 up to 100% at cap).
_load()
var cur: Dictionary = _data.get("customer", {})
if not cur.is_empty():
return cur
var jobs := int(_data.get("jobs_done", 0))
var who: Dictionary = CUSTOMERS[jobs % CUSTOMERS.size()]
var fkeys := FAULTS.keys()
var fault: String = fkeys[jobs % fkeys.size()]
var pay := int(int(FAULTS[fault]["shop"]) * (0.7 + mini(rep(), REP_CAP) * 0.0075))
cur = {"name": who["name"], "car": who["car"], "blurb": who["blurb"],
"fault": fault, "pay": pay}
_data["customer"] = cur
_flush()
return cur
static func customer_done(quality: float) -> int:
## Settle up: full pay at 60+, a $50 tip at 85+, half pay below 60.
## Rep moves with the workmanship -- word gets around either way.
_load()
var cur: Dictionary = _data.get("customer", {})
if cur.is_empty():
return 0
var pay := int(cur["pay"])
if quality >= 85.0:
pay += 50
_data["rep"] = mini(rep() + 3, REP_CAP)
elif quality < 60.0:
pay = pay / 2
_data["rep"] = maxi(rep() - 6, 0)
_data["bucks"] = int(_data["bucks"]) + pay
_data["jobs_done"] = int(_data.get("jobs_done", 0)) + 1
_data["customer"] = {}
_flush()
return pay
## ---- workmanship: 0-100 per fixed system, 85+ is a buff -------------------- ## ---- workmanship: 0-100 per fixed system, 85+ is a buff --------------------
static func quality(car_id: String, sys: String) -> float: static func quality(car_id: String, sys: String) -> float:

View File

@ -86,7 +86,7 @@ func _refresh() -> void:
var keep := list.get_selected_items() var keep := list.get_selected_items()
list.clear() list.clear()
entries.clear() entries.clear()
bucks_label.text = "BUCKS: $%d" % Garage.bucks() bucks_label.text = "BUCKS: $%d | SHED REP %d" % [Garage.bucks(), Garage.rep()]
var flt := Garage.faults(car_id) var flt := Garage.faults(car_id)
if not flt.is_empty(): if not flt.is_empty():
_header("--- IT'S BROKEN (fix it or pay up) ---") _header("--- IT'S BROKEN (fix it or pay up) ---")

View File

@ -39,7 +39,7 @@ func _ready() -> void:
if not level_paths.is_empty(): if not level_paths.is_empty():
level_list.select(0) level_list.select(0)
mode_list.select(0) mode_list.select(0)
$VBox/Bucks.text = "BUCKS: $%d" % Garage.bucks() $VBox/Bucks.text = "BUCKS: $%d | SHED REP %d" % [Garage.bucks(), Garage.rep()]
$VBox/Buttons/Start.pressed.connect(_start) $VBox/Buttons/Start.pressed.connect(_start)
$VBox/Buttons/Garage.pressed.connect(_garage) $VBox/Buttons/Garage.pressed.connect(_garage)

View File

@ -27,6 +27,7 @@ const LIGHT_POOL := ["ENGINE TEMP", "DRIVETRAIN", "OIL PRESSURE", "BATTERY", "AI
var car_id := "" var car_id := ""
var fault := "" var fault := ""
var customer := false # it's THEIR car: payout + rep, no buffs for you
var stats: CarStats var stats: CarStats
var steps: Array = [] var steps: Array = []
var step_index := 0 var step_index := 0
@ -57,6 +58,8 @@ var _oil_fx: GPUParticles3D = null
func _ready() -> void: func _ready() -> void:
car_id = Garage.car_id_of(Garage.ui_stats_path) car_id = Garage.car_id_of(Garage.ui_stats_path)
fault = Garage.ui_fault fault = Garage.ui_fault
customer = Garage.ui_customer
Garage.ui_customer = false # consume: the next wrench is yours by default
stats = load(Garage.ui_stats_path) stats = load(Garage.ui_stats_path)
add_child((load("res://levels/the_shed/level.tscn") as PackedScene).instantiate()) add_child((load("res://levels/the_shed/level.tscn") as PackedScene).instantiate())
@ -601,11 +604,23 @@ func _advance() -> void:
func _finish() -> void: func _finish() -> void:
done = true done = true
quality = clampf(quality, 0.0, 100.0) quality = clampf(quality, 0.0, 100.0)
Garage.clear_fault(car_id, fault) if customer:
Garage.set_quality(car_id, fault, quality) # their car, your invoice: cash now, rep either way. No buffs banked --
step_label.text = "DONE. QUALITY %d%%" % int(quality) # set_quality on THEIR car id would secretly tune your own copy of it.
note_label.text = ("that's a PROPER fix -- it'll drive better than stock" if quality >= 85.0 var pay := Garage.customer_done(quality)
else "it'll hold. probably. no buff at that standard, mate") step_label.text = "PAID $%d. QUALITY %d%%" % [pay, int(quality)]
if quality >= 85.0:
note_label.text = "word gets around. the GOOD word. (rep %d)" % Garage.rep()
elif quality >= 60.0:
note_label.text = "it'll hold. invoice says so. (rep %d)" % Garage.rep()
else:
note_label.text = "that one's coming back on a truck. (rep %d)" % Garage.rep()
else:
Garage.clear_fault(car_id, fault)
Garage.set_quality(car_id, fault, quality)
step_label.text = "DONE. QUALITY %d%%" % int(quality)
note_label.text = ("that's a PROPER fix -- it'll drive better than stock" if quality >= 85.0
else "it'll hold. probably. no buff at that standard, mate")
Sfx.shot_2d("sting_win" if quality >= 85.0 else "clunk", -4.0) Sfx.shot_2d("sting_win" if quality >= 85.0 else "clunk", -4.0)
var b := Button.new() var b := Button.new()
b.text = "BACK TO THE GARAGE" b.text = "BACK TO THE GARAGE"

View File

@ -140,6 +140,38 @@ func _run() -> void:
break break
_check(gg.garage_requested, "rolling into the bay never opened the garage") _check(gg.garage_requested, "rolling into the bay never opened the garage")
gg.free() 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]) 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 # 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. # buffs the car; a sabotaged torque window strips a thread on the oil job.