ShitboxInfinity/src/garage_ui.gd
m3ultra 955413b735 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>
2026-07-31 19:12:15 +10:00

206 lines
6.9 KiB
GDScript

extends Control
## The shop. Pick a part, read its lie, hand over the bucks.
## Which car you're outfitting comes in via Garage.ui_stats_path (set by the menu).
var car_id := ""
var car_name := ""
var list: ItemList
var bucks_label: Label
var desc_label: Label
var buy_btn: Button
var entries: Array = [] # row -> {"kind": "part"|"tool"|"tune"|"header", "id": ...}
func _ready() -> void:
car_id = Garage.car_id_of(Garage.ui_stats_path)
var st: CarStats = load(Garage.ui_stats_path)
car_name = st.display_name if st else car_id
var bg := ColorRect.new()
bg.color = Color(0.07, 0.06, 0.09)
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(bg)
var box := VBoxContainer.new()
box.set_anchors_preset(Control.PRESET_FULL_RECT)
box.offset_left = 60
box.offset_top = 40
box.offset_right = -60
box.offset_bottom = -40
box.add_theme_constant_override("separation", 14)
add_child(box)
var title := Label.new()
title.text = "THE GARAGE -- %s" % car_name
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 40)
title.add_theme_color_override("font_color", Color(1, 0.82, 0.35))
box.add_child(title)
bucks_label = Label.new()
bucks_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
bucks_label.add_theme_font_size_override("font_size", 26)
box.add_child(bucks_label)
list = ItemList.new()
list.size_flags_vertical = Control.SIZE_EXPAND_FILL
list.add_theme_font_size_override("font_size", 22)
list.item_selected.connect(_on_select)
box.add_child(list)
desc_label = Label.new()
desc_label.add_theme_font_size_override("font_size", 20)
desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
box.add_child(desc_label)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 20)
box.add_child(row)
buy_btn = Button.new()
buy_btn.text = "BUY"
buy_btn.add_theme_font_size_override("font_size", 28)
buy_btn.size_flags_horizontal = Control.SIZE_EXPAND_FILL
buy_btn.pressed.connect(_on_buy)
row.add_child(buy_btn)
var back := Button.new()
back.text = "BACK"
back.add_theme_font_size_override("font_size", 28)
back.size_flags_horizontal = Control.SIZE_EXPAND_FILL
back.pressed.connect(func() -> void:
get_tree().change_scene_to_file("res://src/main.tscn"))
row.add_child(back)
_refresh()
for i in list.item_count: # row 0 is a section header now
if list.is_item_selectable(i):
list.select(i)
_on_select(i)
break
func _header(text: String) -> void:
var i := list.add_item(text)
list.set_item_selectable(i, false)
list.set_item_custom_fg_color(i, Color(1, 0.82, 0.35))
entries.append({"kind": "header"})
func _refresh() -> void:
var keep := list.get_selected_items()
list.clear()
entries.clear()
bucks_label.text = "BUCKS: $%d | SHED REP %d" % [Garage.bucks(), Garage.rep()]
var flt := Garage.faults(car_id)
if not flt.is_empty():
_header("--- IT'S BROKEN (fix it or pay up) ---")
for id in flt:
var c: Dictionary = Garage.FAULTS[id]
list.add_item("%-28s %s" % ["FIX: " + c["name"], "DIY IN THE SHED"])
entries.append({"kind": "fault", "id": id})
list.add_item("%-28s $%d" % [" ...or pay someone", c["shop"]])
entries.append({"kind": "shopfix", "id": id})
_header("--- BOLT-ON PARTS (this car) ---")
for id in Garage.CATALOGUE:
var c: Dictionary = Garage.CATALOGUE[id]
var tag := "[FITTED]" if Garage.has(car_id, id) else "$%d" % c["price"]
list.add_item("%-28s %s" % [c["name"], tag])
entries.append({"kind": "part", "id": id})
_header("--- THE SHED (tools; buy again to upgrade the tier) ---")
for id in Garage.TOOLS:
var c: Dictionary = Garage.TOOLS[id]
var tier := Garage.tool_tier(id)
var shown: String = c["tiers"][maxi(tier - 1, 0)]["name"]
var tag: String
if tier == 0:
tag = "$%d" % int(c["price"])
elif tier < 3:
tag = "[T%d] UP $%d" % [tier, Garage.tool_upgrade_price(id)]
else:
tag = "[T3 MAXED]"
list.add_item("%-28s %s" % [shown, tag])
entries.append({"kind": "tool", "id": id})
_header("--- TUNE-UPS (this car, needs the tool) ---")
for id in Garage.TUNES:
var c: Dictionary = Garage.TUNES[id]
var tag: String
if Garage.has_tune(car_id, id):
tag = "[DONE]"
elif not Garage.has_tool(c["tool"]):
tag = "[NEEDS %s]" % Garage.TOOLS[c["tool"]]["name"]
else:
tag = "$%d" % c["price"]
list.add_item("%-28s %s" % [c["name"], tag])
entries.append({"kind": "tune", "id": id})
if not keep.is_empty() and keep[0] < list.item_count and list.is_item_selectable(keep[0]):
list.select(keep[0])
func _entry_info(i: int) -> Dictionary:
var e: Dictionary = entries[i]
match e.get("kind"):
"fault":
var c: Dictionary = Garage.FAULTS[e["id"]]
return {"desc": c["desc"] + " (free, uses your tools and your dignity)",
"price": 0, "done": false, "locked": false}
"shopfix":
var c: Dictionary = Garage.FAULTS[e["id"]]
return {"desc": "Someone else's problem for $%d. No questions, no bolts left over." % c["shop"],
"price": c["shop"], "done": false, "locked": false}
"part":
var c: Dictionary = Garage.CATALOGUE[e["id"]]
return {"desc": c["desc"], "price": c["price"],
"done": Garage.has(car_id, e["id"]), "locked": false}
"tool":
var c: Dictionary = Garage.TOOLS[e["id"]]
var tier := Garage.tool_tier(e["id"])
var d: String = c["tiers"][mini(tier, 2)]["desc"]
if tier > 0 and tier < 3:
d = "NEXT: " + String(c["tiers"][tier]["name"]) + " -- " + d
return {"desc": d, "price": Garage.tool_upgrade_price(e["id"]),
"done": tier >= 3, "locked": false}
"tune":
var c: Dictionary = Garage.TUNES[e["id"]]
return {"desc": c["desc"], "price": c["price"],
"done": Garage.has_tune(car_id, e["id"]),
"locked": not Garage.has_tool(c["tool"])}
return {}
func _on_select(i: int) -> void:
var info := _entry_info(i)
if info.is_empty():
return
desc_label.text = info["desc"]
if info["done"]:
buy_btn.text = "DONE"
buy_btn.disabled = true
elif info["locked"]:
buy_btn.text = "LOCKED (buy the tool)"
buy_btn.disabled = true
else:
buy_btn.text = "BUY ($%d)" % int(info["price"]) if int(info["price"]) > 0 \
else "GET UNDER IT (free)"
buy_btn.disabled = Garage.bucks() < int(info["price"])
func _on_buy() -> void:
var sel := list.get_selected_items()
if sel.is_empty():
return
var e: Dictionary = entries[sel[0]]
var ok := false
match e.get("kind"):
"part": ok = Garage.buy(car_id, e["id"])
"tool": ok = Garage.buy_tool(e["id"])
"tune": ok = Garage.buy_tune(car_id, e["id"])
"shopfix": ok = Garage.shop_fix(car_id, e["id"])
"fault":
# DIY: off to the wrench minigame; it clears the fault on completion
Garage.ui_fault = e["id"]
get_tree().change_scene_to_file("res://src/%s.tscn"
% String(Garage.FAULTS[e["id"]].get("scene", "wrench")))
return
if ok:
Sfx.shot_2d("clunk")
_refresh()
if sel[0] < list.item_count:
_on_select(sel[0])
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_cancel"):
get_tree().change_scene_to_file("res://src/main.tscn")