The repair minigame: what you break, you fix -- with YOUR tools
Carnage now follows the car home. Grenading the gearbox or blowing the
head gasket on the strip records a persistent FAULT in the garage save,
and the car boots WOUNDED everywhere -- gearbox: 80% power and leaking
oil patches; gasket: 85% power and steaming from the bonnet -- until
someone fixes it. The garage screen grows an "IT'S BROKEN" section with
two routes per fault: pay the shop's someone-else's-problem rate ($700
to $900), or GET UNDER IT for free in the shed.
Getting under it opens the repair minigame (src/repair_ui.gd, all code
like every other screen). Step one is diagnosis off the dash warning
lights: with the CASHIES CODE READER the fault name comes through the
cracked screen ("D!IV!TR!IN") and the right light glows amber; without
it you're on vibes, and a wrong pick adds two penalty turns to every
nut in the job. Then the hands-on steps: hold-and-circle the mouse to
spin nuts (direction enforced -- a full turn the wrong way earns
"OTHER WAY."), click to pump the jack, drag the dead box to the bench.
The shed skill tree pays off physically: ALDI SOCKETS halve every nut,
SUPERCHEAP RAMPS halve the jacking, the CASHIES HOIST deletes the 60 kg
drag-the-box step entirely, the ADDITIVE CRATE shortens the coolant
top-up -- and finishing a job without the right tools leaves three
bolts over ("they're spares now").
Mouse plumbing routes everything through pick_light/twist/pump/drop_at,
so the smoke suite plays a complete toolless gearbox swap headless:
wrong light penalised, right light advances, every step type exercised,
fault cleared at the end. Wounded boot, shop fix and the drag-finish
fault handoff are covered too.
Two traps re-confirmed on the way: `var x :=` cannot infer through
InputEvent.position (base-class Variant) -- the parse error took the
whole suite down as a silent hang; and a full-rect background ColorRect
eats every mouse event before root._gui_input sees it unless its
mouse_filter is IGNORE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a8fed95d42
commit
e47fecf2a8
22
README.md
22
README.md
@ -128,8 +128,28 @@ one thing parts can't buy: panels that hang on through bigger hits (it raises
|
||||
the car's shed-a-panel and wreck thresholds). Parts buy bling; the shed buys
|
||||
labour.
|
||||
|
||||
**Faults and the repair minigame.** What you break follows the car home:
|
||||
grenading the gearbox (three blown shifts in one drag run) or blowing the head
|
||||
gasket (cooking the engine on the limiter) records a persistent **fault**, and
|
||||
the car boots WOUNDED — down on power, leaking oil patches or steaming — until
|
||||
it's fixed. The garage screen offers two routes: pay the shop's
|
||||
"someone else's problem" rate, or **GET UNDER IT** in the shed for free. That
|
||||
opens the repair minigame ([src/repair_ui.gd](src/repair_ui.gd)): diagnose off
|
||||
the dash warning lights first (the CASHIES CODE READER names the fault through
|
||||
its cracked screen — "D!IV!TR!IN" — and points at the right light; without it
|
||||
you're on vibes, and a wrong guess adds penalty turns to every nut), then work
|
||||
through drag / click / twist steps — hold-and-circle the mouse to spin nuts
|
||||
(righty-tighty enforced; a full turn the wrong way earns "OTHER WAY."), pump
|
||||
the jack by clicking, drag the dead box to the bench. Owning the right shed
|
||||
tool shortens the job: sockets halve every nut, ramps halve the jacking, the
|
||||
hoist deletes the 60 kg gearbox-drag step entirely, and finishing without the
|
||||
right tools leaves three bolts over. The whole minigame routes through a
|
||||
testable API (`pick_light`/`twist`/`pump`/`drop_at`), so the smoke suite plays
|
||||
a complete toolless gearbox swap headless.
|
||||
|
||||
Purchases are per-car (tools are global) and persist in `user://garage.save`,
|
||||
which also carries the drag **ladder rung** and per-car **quarter-mile PBs**.
|
||||
which also carries the drag **ladder rung**, per-car **quarter-mile PBs** and
|
||||
outstanding **faults**.
|
||||
Upgrades apply to a duplicate of the CarStats resource -- the .tres is shared
|
||||
with traffic and parked spawns of the same model, and tuning your Lazza must
|
||||
not secretly tune every Lazza in the city. Bolt-ons attach after the deformer
|
||||
|
||||
10
push17.log
Normal file
10
push17.log
Normal file
@ -0,0 +1,10 @@
|
||||
Enumerating objects: 16, done.
|
||||
Counting objects: 6% (1/16)
Counting objects: 12% (2/16)
Counting objects: 18% (3/16)
Counting objects: 25% (4/16)
Counting objects: 31% (5/16)
Counting objects: 37% (6/16)
Counting objects: 43% (7/16)
Counting objects: 50% (8/16)
Counting objects: 56% (9/16)
Counting objects: 62% (10/16)
Counting objects: 68% (11/16)
Counting objects: 75% (12/16)
Counting objects: 81% (13/16)
Counting objects: 87% (14/16)
Counting objects: 93% (15/16)
Counting objects: 100% (16/16)
Counting objects: 100% (16/16), done.
|
||||
Delta compression using up to 32 threads
|
||||
Compressing objects: 11% (1/9)
Compressing objects: 22% (2/9)
Compressing objects: 33% (3/9)
Compressing objects: 44% (4/9)
Compressing objects: 55% (5/9)
Compressing objects: 66% (6/9)
Compressing objects: 77% (7/9)
Compressing objects: 88% (8/9)
Compressing objects: 100% (9/9)
Compressing objects: 100% (9/9), done.
|
||||
Writing objects: 11% (1/9)
Writing objects: 22% (2/9)
Writing objects: 33% (3/9)
Writing objects: 44% (4/9)
Writing objects: 55% (5/9)
Writing objects: 66% (6/9)
Writing objects: 77% (7/9)
Writing objects: 88% (8/9)
Writing objects: 100% (9/9)
Writing objects: 100% (9/9), 5.50 KiB | 5.50 MiB/s, done.
|
||||
Total 9 (delta 7), 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
|
||||
17aabd7..927b3a9 main -> main
|
||||
@ -939,10 +939,14 @@ 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.)")
|
||||
# what you broke follows the car home: faults persist until repaired
|
||||
var cid := Garage.car_id_of(car_path)
|
||||
if box_blown:
|
||||
lines.append("(the gearbox is on the strip somewhere behind you)")
|
||||
Garage.add_fault(cid, "gearbox")
|
||||
lines.append("(the gearbox is on the strip somewhere behind you -- FIX IT IN THE GARAGE)")
|
||||
if gasket_blown:
|
||||
lines.append("(and the radiator's boiled itself dry)")
|
||||
Garage.add_fault(cid, "gasket")
|
||||
lines.append("(and the radiator's boiled itself dry -- FIX IT IN THE GARAGE)")
|
||||
Garage.earn(pay)
|
||||
lines.append("+$%d bucks | %d clean / %d blown" % [pay, clean_shifts, blown_shifts])
|
||||
lines.append("R to retry, Esc for menu")
|
||||
|
||||
@ -15,6 +15,7 @@ const SAVE := "user://garage.save"
|
||||
## REAL path would wipe the player's actual progression.
|
||||
static var save_path := SAVE
|
||||
static var ui_stats_path := "" # menu -> garage screen: which car is selected
|
||||
static var ui_fault := "" # garage screen -> repair minigame: which fault
|
||||
|
||||
## id -> {name, desc, price, effects, build (visual constructor)}
|
||||
## Effects: power_mult (multiplies engine_power), grip_add, boost_add (base
|
||||
@ -186,6 +187,60 @@ static func buy_tune(car_id: String, tune: String) -> bool:
|
||||
_flush()
|
||||
return true
|
||||
|
||||
## ---- faults: what you broke stays broken until you fix it -----------------
|
||||
## Recorded by game.gd when a run grenades something; the car boots WOUNDED
|
||||
## (power down, leaking/steaming) until repaired -- either the shed minigame
|
||||
## (free, needs your hands) or paying the shop's "someone else's problem" rate.
|
||||
## "tool" is the shed tool that makes the DIY fix civilised; without it the
|
||||
## minigame is longer and the commentary crueller.
|
||||
|
||||
const FAULTS := {
|
||||
"gearbox": {"name": "GRENADED GEARBOX", "light": "DRIVETRAIN", "shop": 900,
|
||||
"tool": "hoist",
|
||||
"desc": "Third gear is a maraca now. Leaks on everything it loves."},
|
||||
"gasket": {"name": "BLOWN HEAD GASKET", "light": "ENGINE TEMP", "shop": 700,
|
||||
"tool": "sockets",
|
||||
"desc": "Mayonnaise under the oil cap. Not the good kind."},
|
||||
}
|
||||
|
||||
static func faults(car_id: String) -> Array:
|
||||
_load()
|
||||
return _data.get("faults", {}).get(car_id, [])
|
||||
|
||||
static func add_fault(car_id: String, fault: String) -> void:
|
||||
_load()
|
||||
if not FAULTS.has(fault):
|
||||
return
|
||||
var f: Dictionary = _data.get("faults", {})
|
||||
var lst: Array = f.get(car_id, [])
|
||||
if fault in lst:
|
||||
return
|
||||
lst.append(fault)
|
||||
f[car_id] = lst
|
||||
_data["faults"] = f
|
||||
_flush()
|
||||
|
||||
static func clear_fault(car_id: String, fault: String) -> void:
|
||||
_load()
|
||||
var f: Dictionary = _data.get("faults", {})
|
||||
var lst: Array = f.get(car_id, [])
|
||||
lst.erase(fault)
|
||||
f[car_id] = lst
|
||||
_data["faults"] = f
|
||||
_flush()
|
||||
|
||||
static func shop_fix(car_id: String, fault: String) -> bool:
|
||||
## The coward's route: full price, no left-over bolts.
|
||||
_load()
|
||||
if not fault in faults(car_id):
|
||||
return false
|
||||
var price: int = FAULTS[fault]["shop"]
|
||||
if int(_data["bucks"]) < price:
|
||||
return false
|
||||
_data["bucks"] = int(_data["bucks"]) - price
|
||||
clear_fault(car_id, fault)
|
||||
return true
|
||||
|
||||
## ---- raceday: the drag ladder + personal bests ----------------------------
|
||||
## Rung index into game.gd's DRAG_LADDER; beat your current rung to advance.
|
||||
## PBs are per-car elapsed times (seconds from the green), 0.0 = none yet.
|
||||
@ -228,7 +283,8 @@ static func apply(car: Node3D, stats_path: String, is_player: bool) -> void:
|
||||
var cid := car_id_of(stats_path)
|
||||
var ups := owned(cid)
|
||||
var tns := tunes(cid)
|
||||
if ups.is_empty() and tns.is_empty():
|
||||
var flt := faults(cid)
|
||||
if ups.is_empty() and tns.is_empty() and flt.is_empty():
|
||||
return
|
||||
var st = car.get("stats")
|
||||
if st != null:
|
||||
@ -236,6 +292,14 @@ static func apply(car: Node3D, stats_path: String, is_player: bool) -> void:
|
||||
var pm := 1.0
|
||||
var ga := 0.0
|
||||
var ba := 0.0
|
||||
# unfixed faults: the car boots wounded and looks it
|
||||
if "gearbox" in flt:
|
||||
pm *= 0.80
|
||||
car.set("leaking", true)
|
||||
if "gasket" in flt:
|
||||
pm *= 0.85
|
||||
if car.has_method("set_steam"):
|
||||
car.call("set_steam", true)
|
||||
for u in ups:
|
||||
match u:
|
||||
"stripes": pm += 0.06
|
||||
|
||||
@ -87,6 +87,15 @@ func _refresh() -> void:
|
||||
list.clear()
|
||||
entries.clear()
|
||||
bucks_label.text = "BUCKS: $%d" % Garage.bucks()
|
||||
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]
|
||||
@ -117,6 +126,14 @@ func _refresh() -> void:
|
||||
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"],
|
||||
@ -144,7 +161,8 @@ func _on_select(i: int) -> void:
|
||||
buy_btn.text = "LOCKED (buy the tool)"
|
||||
buy_btn.disabled = true
|
||||
else:
|
||||
buy_btn.text = "BUY ($%d)" % int(info["price"])
|
||||
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:
|
||||
@ -157,6 +175,12 @@ func _on_buy() -> void:
|
||||
"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 shed minigame; it clears the fault on completion
|
||||
Garage.ui_fault = e["id"]
|
||||
get_tree().change_scene_to_file("res://src/repair.tscn")
|
||||
return
|
||||
if ok:
|
||||
Sfx.shot_2d("clunk")
|
||||
_refresh()
|
||||
|
||||
8
src/repair.tscn
Normal file
8
src/repair.tscn
Normal file
@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://src/repair_ui.gd" id="1"]
|
||||
|
||||
[node name="Repair" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1")
|
||||
369
src/repair_ui.gd
Normal file
369
src/repair_ui.gd
Normal file
@ -0,0 +1,369 @@
|
||||
extends Control
|
||||
## The shed repair minigame: fix what you broke with the tools you actually
|
||||
## own. Diagnose off the dash lights (the CASHIES CODE READER earns its keep
|
||||
## here), then work through drag / click / twist steps. Owning the right shed
|
||||
## tool makes a step civilised; not owning it means vice grips and regret.
|
||||
##
|
||||
## Which car and which fault come in via Garage.ui_stats_path / Garage.ui_fault
|
||||
## (set by the garage screen). The input handlers all route through pick_light /
|
||||
## twist / pump / drop_at, so the smoke suite can play the whole game headless.
|
||||
|
||||
const LIGHT_POOL := ["ENGINE TEMP", "DRIVETRAIN", "OIL PRESSURE", "BATTERY", "AIRBAG (ALWAYS ON)"]
|
||||
const NUT_R := 150.0 # grab radius around the nut, px
|
||||
|
||||
var car_id := ""
|
||||
var fault := ""
|
||||
var steps: Array = [] # {kind: diagnose|twist|drag|pump, ...}
|
||||
var step_index := 0
|
||||
var done := false
|
||||
var penalty_turns := 0.0 # wrong diagnosis = extra turns on every nut
|
||||
var twist_prog := 0.0
|
||||
var wrong_way := 0.0
|
||||
var pump_count := 0
|
||||
var dragging := false
|
||||
var missing_tool := false # any step done the hard way -> bolts left over
|
||||
|
||||
var title_label: Label
|
||||
var step_label: Label
|
||||
var tool_label: Label
|
||||
var prog_bar: ProgressBar
|
||||
var count_label: Label
|
||||
var stage: Control # per-step widgets live here, cleared each step
|
||||
var nut: Control = null
|
||||
var drag_item: Panel = null
|
||||
var drag_target: Panel = null
|
||||
var light_btns: Array = []
|
||||
|
||||
func _ready() -> void:
|
||||
car_id = Garage.car_id_of(Garage.ui_stats_path)
|
||||
fault = Garage.ui_fault
|
||||
steps = _build_steps()
|
||||
|
||||
var bg := ColorRect.new()
|
||||
bg.color = Color(0.06, 0.06, 0.08)
|
||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
# IGNORE or the backdrop eats every click before root._gui_input sees it
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(bg)
|
||||
|
||||
var box := VBoxContainer.new()
|
||||
box.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
box.offset_left = 60
|
||||
box.offset_top = 30
|
||||
box.offset_right = -60
|
||||
box.offset_bottom = -30
|
||||
box.add_theme_constant_override("separation", 10)
|
||||
add_child(box)
|
||||
|
||||
title_label = Label.new()
|
||||
title_label.text = "THE SHED -- %s" % Garage.FAULTS.get(fault, {}).get("name", "???")
|
||||
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title_label.add_theme_font_size_override("font_size", 36)
|
||||
title_label.add_theme_color_override("font_color", Color(1, 0.82, 0.35))
|
||||
box.add_child(title_label)
|
||||
|
||||
count_label = Label.new()
|
||||
count_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
count_label.add_theme_font_size_override("font_size", 20)
|
||||
box.add_child(count_label)
|
||||
|
||||
step_label = Label.new()
|
||||
step_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
step_label.add_theme_font_size_override("font_size", 30)
|
||||
box.add_child(step_label)
|
||||
|
||||
tool_label = Label.new()
|
||||
tool_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
tool_label.add_theme_font_size_override("font_size", 19)
|
||||
tool_label.add_theme_color_override("font_color", Color(0.65, 0.68, 0.72))
|
||||
box.add_child(tool_label)
|
||||
|
||||
stage = Control.new()
|
||||
stage.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
box.add_child(stage)
|
||||
|
||||
prog_bar = ProgressBar.new()
|
||||
prog_bar.custom_minimum_size = Vector2(0, 20)
|
||||
prog_bar.show_percentage = false
|
||||
box.add_child(prog_bar)
|
||||
|
||||
var back := Button.new()
|
||||
back.text = "WALK AWAY (Esc)"
|
||||
back.add_theme_font_size_override("font_size", 20)
|
||||
back.pressed.connect(_leave)
|
||||
box.add_child(back)
|
||||
|
||||
_show_step()
|
||||
|
||||
func _build_steps() -> Array:
|
||||
## The job sheet. Tools shorten it or soften it; their absence is content.
|
||||
var out: Array = [{"kind": "diagnose"}]
|
||||
match fault:
|
||||
"gasket":
|
||||
var t := 4.0 if Garage.has_tool("sockets") else 8.0
|
||||
out.append({"kind": "twist", "label": "UNDO THE HEAD BOLTS", "dir": -1.0, "turns": t})
|
||||
out.append({"kind": "drag", "label": "OLD GASKET INTO THE BIN", "item": "CRUSTY GASKET", "target": "THE BIN"})
|
||||
out.append({"kind": "drag", "label": "NEW GASKET ONTO THE BLOCK", "item": "GASKET (EBAY, \"GENUINE\")", "target": "THE BLOCK"})
|
||||
out.append({"kind": "twist", "label": "TORQUE THE HEAD DOWN", "dir": 1.0, "turns": t})
|
||||
out.append({"kind": "pump", "label": "TOP UP THE COOLANT", "clicks": 5 if Garage.has_tool("additives") else 8})
|
||||
"gearbox":
|
||||
out.append({"kind": "pump", "label": "JACK IT UP", "clicks": 5 if Garage.has_tool("ramps") else 10})
|
||||
var t := 5.0 if Garage.has_tool("sockets") else 10.0
|
||||
out.append({"kind": "twist", "label": "UNDO THE BELLHOUSING BOLTS", "dir": -1.0, "turns": t})
|
||||
if not Garage.has_tool("hoist"):
|
||||
out.append({"kind": "drag", "label": "DRAG THE DEAD BOX OUT (it's 60kg)", "item": "DEAD GEARBOX", "target": "THE BENCH"})
|
||||
out.append({"kind": "drag", "label": "CASHIES BOX IN", "item": "GEARBOX (ONE OWNER, HONEST)", "target": "THE TUNNEL"})
|
||||
out.append({"kind": "twist", "label": "BOLT IT ALL BACK UP", "dir": 1.0, "turns": t})
|
||||
return out
|
||||
|
||||
## ---- step display ---------------------------------------------------------
|
||||
|
||||
func _show_step() -> void:
|
||||
for c in stage.get_children():
|
||||
c.queue_free()
|
||||
nut = null
|
||||
drag_item = null
|
||||
drag_target = null
|
||||
light_btns.clear()
|
||||
twist_prog = 0.0
|
||||
wrong_way = 0.0
|
||||
pump_count = 0
|
||||
if step_index >= steps.size():
|
||||
_finish()
|
||||
return
|
||||
var st: Dictionary = steps[step_index]
|
||||
count_label.text = "STEP %d / %d" % [step_index + 1, steps.size()]
|
||||
prog_bar.value = 0
|
||||
prog_bar.max_value = 1.0
|
||||
match st["kind"]:
|
||||
"diagnose":
|
||||
_show_diagnose()
|
||||
"twist":
|
||||
step_label.text = st["label"]
|
||||
var need := _turns_needed(st)
|
||||
tool_label.text = ("ALDI SOCKETS: metric-ish, fits mostly" if Garage.has_tool("sockets")
|
||||
else "VICE GRIPS: this will end badly")
|
||||
if not Garage.has_tool("sockets"):
|
||||
missing_tool = true
|
||||
prog_bar.max_value = need * TAU
|
||||
nut = _make_nut(st)
|
||||
"drag":
|
||||
step_label.text = st["label"]
|
||||
tool_label.text = "drag it. with the mouse. like your life."
|
||||
_make_drag(st)
|
||||
"pump":
|
||||
step_label.text = st["label"]
|
||||
tool_label.text = ("SUPERCHEAP RAMPS: rated to \"most cars, probably\""
|
||||
if fault == "gearbox" and Garage.has_tool("ramps") else "click. click. click.")
|
||||
prog_bar.max_value = int(st["clicks"])
|
||||
_make_pump(st)
|
||||
|
||||
func _show_diagnose() -> void:
|
||||
step_label.text = "WHAT'S THAT LIGHT, THEN?"
|
||||
var correct: String = Garage.FAULTS[fault]["light"]
|
||||
var pool := LIGHT_POOL.filter(func(l): return l != correct)
|
||||
# deterministic decoys: seeded off the fault so tests can rely on layout
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = hash(fault)
|
||||
pool.shuffle()
|
||||
var lights: Array = [correct, pool[0], pool[1]]
|
||||
lights.shuffle()
|
||||
if Garage.has_tool("reader"):
|
||||
tool_label.text = "CASHIES READER SAYS: \"%s\" (screen cracked)" % _garble(correct)
|
||||
else:
|
||||
tool_label.text = "no code reader. vibes only."
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 26)
|
||||
row.set_anchors_preset(Control.PRESET_CENTER)
|
||||
row.grow_horizontal = Control.GROW_DIRECTION_BOTH # centre ON the anchor
|
||||
stage.add_child(row)
|
||||
for l in lights:
|
||||
var b := Button.new()
|
||||
b.text = "(!) %s" % l
|
||||
b.add_theme_font_size_override("font_size", 24)
|
||||
if Garage.has_tool("reader") and l == correct:
|
||||
b.modulate = Color(1.0, 0.75, 0.35) # the reader points, kind of
|
||||
b.pressed.connect(func() -> void: pick_light(lights.find(l)))
|
||||
row.add_child(b)
|
||||
light_btns.append({"btn": b, "light": l})
|
||||
|
||||
func _garble(s: String) -> String:
|
||||
var out := ""
|
||||
for i in s.length():
|
||||
out += ["#", "!", "0"][i % 3] if i % 3 == 1 and s[i] != " " else s[i]
|
||||
return out
|
||||
|
||||
func _make_nut(st: Dictionary) -> Control:
|
||||
var n := Panel.new()
|
||||
n.mouse_filter = Control.MOUSE_FILTER_IGNORE # let root handle the twisting
|
||||
n.custom_minimum_size = Vector2(120, 120)
|
||||
n.set_anchors_preset(Control.PRESET_CENTER)
|
||||
n.position = Vector2(-60, -60)
|
||||
n.pivot_offset = Vector2(60, 60)
|
||||
n.rotation = 0.0
|
||||
stage.add_child(n)
|
||||
var l := Label.new()
|
||||
l.text = "NUT"
|
||||
l.set_anchors_preset(Control.PRESET_CENTER)
|
||||
l.add_theme_font_size_override("font_size", 26)
|
||||
n.add_child(l)
|
||||
var hint := Label.new()
|
||||
hint.text = "hold + drag in circles (%s)" % ("anticlockwise" if st["dir"] < 0 else "clockwise")
|
||||
hint.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
||||
hint.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
hint.position.y = -60
|
||||
hint.add_theme_font_size_override("font_size", 18)
|
||||
stage.add_child(hint)
|
||||
return n
|
||||
|
||||
func _make_drag(st: Dictionary) -> void:
|
||||
drag_target = Panel.new()
|
||||
drag_target.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
drag_target.custom_minimum_size = Vector2(260, 150)
|
||||
drag_target.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
|
||||
drag_target.position += Vector2(-290, -75)
|
||||
stage.add_child(drag_target)
|
||||
var tl := Label.new()
|
||||
tl.text = String(st["target"])
|
||||
tl.set_anchors_preset(Control.PRESET_CENTER)
|
||||
tl.add_theme_font_size_override("font_size", 20)
|
||||
drag_target.add_child(tl)
|
||||
drag_item = Panel.new()
|
||||
drag_item.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
drag_item.custom_minimum_size = Vector2(200, 90)
|
||||
drag_item.self_modulate = Color(0.8, 0.55, 0.3)
|
||||
drag_item.set_anchors_preset(Control.PRESET_CENTER_LEFT)
|
||||
drag_item.position += Vector2(60, -45)
|
||||
stage.add_child(drag_item)
|
||||
var il := Label.new()
|
||||
il.text = String(st["item"])
|
||||
il.set_anchors_preset(Control.PRESET_CENTER)
|
||||
il.add_theme_font_size_override("font_size", 17)
|
||||
drag_item.add_child(il)
|
||||
|
||||
func _make_pump(st: Dictionary) -> void:
|
||||
var b := Button.new()
|
||||
b.text = "PUMP"
|
||||
b.custom_minimum_size = Vector2(220, 120)
|
||||
b.add_theme_font_size_override("font_size", 34)
|
||||
b.set_anchors_preset(Control.PRESET_CENTER)
|
||||
b.position = Vector2(-110, -60)
|
||||
b.pressed.connect(pump)
|
||||
stage.add_child(b)
|
||||
|
||||
## ---- the actual game (also the test API) ----------------------------------
|
||||
|
||||
func pick_light(i: int) -> void:
|
||||
if done or steps[step_index]["kind"] != "diagnose" or i >= light_btns.size():
|
||||
return
|
||||
if light_btns[i]["light"] == Garage.FAULTS[fault]["light"]:
|
||||
Sfx.shot_2d("clunk", -6.0)
|
||||
_advance()
|
||||
else:
|
||||
penalty_turns += 2.0
|
||||
Sfx.shot_2d("bog", -6.0)
|
||||
tool_label.text = "nope. pulled the wrong thing apart first. every nut is +2 turns now."
|
||||
|
||||
func _turns_needed(st: Dictionary) -> float:
|
||||
return float(st["turns"]) + penalty_turns
|
||||
|
||||
func twist(rad: float) -> void:
|
||||
## rad > 0 = clockwise on screen. Only the step's direction makes progress;
|
||||
## a full turn the wrong way earns commentary.
|
||||
if done or steps[step_index]["kind"] != "twist":
|
||||
return
|
||||
var st: Dictionary = steps[step_index]
|
||||
var toward: float = rad * float(st["dir"])
|
||||
if toward > 0.0:
|
||||
twist_prog += toward
|
||||
wrong_way = 0.0
|
||||
else:
|
||||
wrong_way += -toward
|
||||
if wrong_way > TAU:
|
||||
wrong_way = 0.0
|
||||
tool_label.text = "OTHER WAY."
|
||||
Sfx.shot_2d("bog", -10.0)
|
||||
if nut:
|
||||
nut.rotation = twist_prog * float(st["dir"])
|
||||
prog_bar.value = twist_prog
|
||||
if twist_prog >= _turns_needed(st) * TAU:
|
||||
Sfx.shot_2d("clunk", -4.0)
|
||||
_advance()
|
||||
|
||||
func pump() -> void:
|
||||
if done or steps[step_index]["kind"] != "pump":
|
||||
return
|
||||
pump_count += 1
|
||||
Sfx.shot_2d("clunk", -12.0)
|
||||
prog_bar.value = pump_count
|
||||
if pump_count >= int(steps[step_index]["clicks"]):
|
||||
_advance()
|
||||
|
||||
func drop_at(pos: Vector2) -> void:
|
||||
if done or steps[step_index]["kind"] != "drag" or drag_target == null:
|
||||
return
|
||||
if drag_target.get_global_rect().has_point(pos):
|
||||
Sfx.shot_2d("clunk", -4.0)
|
||||
_advance()
|
||||
|
||||
func _advance() -> void:
|
||||
step_index += 1
|
||||
_show_step()
|
||||
|
||||
func _finish() -> void:
|
||||
done = true
|
||||
Garage.clear_fault(car_id, fault)
|
||||
step_label.text = "FIXED. PROBABLY."
|
||||
count_label.text = ""
|
||||
tool_label.text = ("Three bolts left over. They're spares now." if missing_tool
|
||||
else "Went back together like it wanted to live.")
|
||||
prog_bar.value = prog_bar.max_value
|
||||
Sfx.shot_2d("sting_win", -4.0)
|
||||
var b := Button.new()
|
||||
b.text = "BACK TO THE GARAGE"
|
||||
b.add_theme_font_size_override("font_size", 26)
|
||||
b.set_anchors_preset(Control.PRESET_CENTER)
|
||||
b.position = Vector2(-160, -30)
|
||||
b.custom_minimum_size = Vector2(320, 60)
|
||||
b.pressed.connect(_leave)
|
||||
stage.add_child(b)
|
||||
|
||||
func _leave() -> void:
|
||||
get_tree().change_scene_to_file("res://src/garage.tscn")
|
||||
|
||||
## ---- mouse plumbing: everything routes to the API above -------------------
|
||||
|
||||
var _twisting := false
|
||||
var _prev_angle := 0.0
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if done or step_index >= steps.size():
|
||||
return
|
||||
var st: Dictionary = steps[step_index]
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
if st["kind"] == "twist" and nut != null \
|
||||
and event.position.distance_to(nut.get_global_rect().get_center()) < NUT_R:
|
||||
_twisting = true
|
||||
_prev_angle = (event.position - nut.get_global_rect().get_center()).angle()
|
||||
elif st["kind"] == "drag" and drag_item != null \
|
||||
and drag_item.get_global_rect().has_point(event.position):
|
||||
dragging = true
|
||||
else:
|
||||
_twisting = false
|
||||
if dragging:
|
||||
dragging = false
|
||||
drop_at(event.position)
|
||||
elif event is InputEventMouseMotion:
|
||||
if _twisting and nut != null:
|
||||
# explicit type: event is base InputEvent here, so .position is a
|
||||
# Variant and := cannot infer through it (the parse error hangs cold)
|
||||
var a: float = (event.position - nut.get_global_rect().get_center()).angle()
|
||||
twist(wrapf(a - _prev_angle, -PI, PI))
|
||||
_prev_angle = a
|
||||
elif dragging and drag_item != null:
|
||||
drag_item.global_position = event.position - drag_item.size * 0.5
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_leave()
|
||||
1
src/repair_ui.gd.uid
Normal file
1
src/repair_ui.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bray7xq7dem20
|
||||
@ -59,6 +59,48 @@ func _run() -> void:
|
||||
_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")
|
||||
# faults: break it, boot wounded, pay the shop for one, DIY 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")
|
||||
# DIY the gearbox: play the whole minigame headless through its API
|
||||
Garage.ui_stats_path = "res://cars/datto_120y/stats.tres"
|
||||
Garage.ui_fault = "gearbox"
|
||||
var ui = (load("res://src/repair.tscn") as PackedScene).instantiate()
|
||||
root.add_child(ui)
|
||||
await process_frame
|
||||
_check(ui.steps.size() >= 6, "gearbox job suspiciously short (%d steps)" % ui.steps.size())
|
||||
var right := -1
|
||||
for i in ui.light_btns.size():
|
||||
if ui.light_btns[i]["light"] == Garage.FAULTS["gearbox"]["light"]:
|
||||
right = i
|
||||
ui.pick_light((right + 1) % 3)
|
||||
_check(ui.penalty_turns > 0.0, "wrong diagnosis cost nothing")
|
||||
ui.pick_light(right)
|
||||
_check(ui.step_index == 1, "right light didn't advance the job")
|
||||
var guard := 0
|
||||
while not ui.done and guard < 12:
|
||||
guard += 1
|
||||
var st: Dictionary = ui.steps[ui.step_index]
|
||||
match st["kind"]:
|
||||
"twist":
|
||||
ui.twist(float(st["dir"]) * ((float(st["turns"]) + ui.penalty_turns) * TAU + 0.1))
|
||||
"pump":
|
||||
for i in int(st["clicks"]):
|
||||
ui.pump()
|
||||
"drag":
|
||||
ui.drop_at(ui.drag_target.get_global_rect().get_center())
|
||||
_check(ui.done, "repair never finished (stuck on step %d)" % ui.step_index)
|
||||
_check(not Garage.faults("datto_120y").has("gearbox"), "DIY fix didn't clear the fault")
|
||||
ui.free()
|
||||
Garage._reset_for_test() # later stages must run stock
|
||||
|
||||
# cruise: throttle 3s, car must move
|
||||
@ -235,6 +277,11 @@ func _run() -> void:
|
||||
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])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user