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:
parent
89b9106f88
commit
955413b735
16
README.md
16
README.md
@ -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,
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
25
src/game.gd
25
src/game.gd
@ -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:
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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) ---")
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user