Wrench phase 2: gearbox and gasket in 3D, the crane ceremony, tool tiers
The 2D repair panel retires -- every fault is a wrench job on the real
car in the real shed now. Gearbox and gasket open with DIAGNOSIS: three
lit warning lights, one honest, the code reader pointing at it through
its cracked screen ("D!IV!TR!IN"); a wrong pick costs quality and makes
every nut in the job run a turn longer (penalty_turns feeds _need() and
the HUD torque arc).
The gearbox is the hero job. Without a hoist you drag the 60 kg box out
by hand and hold the steady ring so it doesn't land on you; with the
Cashies crane it's the ceremony -- hook the chain, pump it out -- and
the tier-3 gantry winch hooks itself up. The gasket job is the full
rite: bonnet, head bolts, old gasket out (mind the mayonnaise), seat
the new one square, torque the head IN the window, coolant back in.
Tool tiers: every line goes Cashies -> Supercheap -> SNAP-OFF
PROFESSIONAL (buy again to upgrade, garage shows [T2] UP $N). Tiers are
felt, not read: sockets scale turns 1.6x down to 0.5x and widen the
torque window 0.8 to 1.3 rad; the tier-2 compressor's RATTLE GUN
converts non-torque nut twists into BRRRT clicks (windows keep the hand
skill -- guns strip); ramps cut jacking 10 clicks to 3; the fluids
shelf speeds pours; the SNAP-OFF tablet skips diagnosis outright.
Pre-tier saves read as tier 1.
Workmanship reaches the strip: an 85+ gearbox rebuild raises box_limit
to 4 blown shifts, an 85+ gasket job lifts the overheat ceiling to
1.18. Both wired into the drag setup and asserted in the suite, which
now also covers tier upgrade/cap flow, wrong-then-right diagnosis, the
toolless 60 kg gearbox path, and a clean gasket run -- via one shared
driver that handles every verb including diagnose.
The := inference trap, visit four: indexing untyped const Arrays.
Typed the tables (Array[float]/Array[int]) so every index site infers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a5158bf6ec
commit
89b9106f88
25
README.md
25
README.md
@ -164,11 +164,26 @@ Faults have gameplay sources: a red light at the tree flat-spots the fronts,
|
||||
every 4th wreck rattles the sump loose (oil light), and getting busted kills
|
||||
the battery, because the divvy van jump-starts it wrong out of spite. The
|
||||
whole thing routes through an API (`twist/pump/grab/drop_at/yank_ok/
|
||||
steady_tick/clamp_on`), so the smoke suite plays a clean flat-tyre job
|
||||
(quality 90+, buff verified on next boot) and a sabotaged oil job (stripped
|
||||
thread, extraction step) headless. Gearbox and gasket still use the 2D panel
|
||||
(`FAULTS[id].scene` routes each fault to its minigame) -- porting them to 3D
|
||||
plus tool tiers and driveway customers is phases 2 and 3.
|
||||
steady_tick/clamp_on/pick_light`), so the smoke suite plays a clean flat-tyre
|
||||
job (quality 90+, buff verified on next boot) and a sabotaged oil job
|
||||
(stripped thread, extraction step) headless.
|
||||
|
||||
Phase 2 finishes the port: **gearbox and gasket are wrench jobs now** (the 2D
|
||||
panel is gone) and both open with **diagnosis** -- three lit warning lights,
|
||||
one honest, the code reader pointing at it through its cracked screen; a wrong
|
||||
pick makes every nut in the job run a turn longer. The gearbox job is the
|
||||
hero: no hoist means dragging 60 kg out by hand and holding the steady ring so
|
||||
it doesn't land on you, while owning the crane turns it into the ceremony
|
||||
(hook the chain, pump it out -- and at tier 3 the winch hooks itself). Every
|
||||
tool line now has **three tiers** (Cashies -> Supercheap -> SNAP-OFF
|
||||
PROFESSIONAL, buy again to upgrade): better sockets mean fewer turns and a
|
||||
wider torque window, the tier-2 compressor's **rattle gun turns wheel nuts
|
||||
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
|
||||
skips diagnosis entirely. Workmanship reaches the strip: an 85+ gearbox
|
||||
rebuild takes an **extra blown shift** before grenading, an 85+ gasket job
|
||||
**raises the overheat ceiling** -- both asserted in the suite. Phase 3 is
|
||||
driveway customers.
|
||||
|
||||
**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,
|
||||
|
||||
10
push16.log
10
push16.log
@ -1,10 +0,0 @@
|
||||
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
|
||||
10
push17.log
10
push17.log
@ -1,10 +0,0 @@
|
||||
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
|
||||
10
push18.log
10
push18.log
@ -1,10 +0,0 @@
|
||||
Enumerating objects: 21, done.
|
||||
Counting objects: 4% (1/21)
Counting objects: 9% (2/21)
Counting objects: 14% (3/21)
Counting objects: 19% (4/21)
Counting objects: 23% (5/21)
Counting objects: 28% (6/21)
Counting objects: 33% (7/21)
Counting objects: 38% (8/21)
Counting objects: 42% (9/21)
Counting objects: 47% (10/21)
Counting objects: 52% (11/21)
Counting objects: 57% (12/21)
Counting objects: 61% (13/21)
Counting objects: 66% (14/21)
Counting objects: 71% (15/21)
Counting objects: 76% (16/21)
Counting objects: 80% (17/21)
Counting objects: 85% (18/21)
Counting objects: 90% (19/21)
Counting objects: 95% (20/21)
Counting objects: 100% (21/21)
Counting objects: 100% (21/21), done.
|
||||
Delta compression using up to 32 threads
|
||||
Compressing objects: 8% (1/12)
Compressing objects: 16% (2/12)
Compressing objects: 25% (3/12)
Compressing objects: 33% (4/12)
Compressing objects: 41% (5/12)
Compressing objects: 50% (6/12)
Compressing objects: 58% (7/12)
Compressing objects: 66% (8/12)
Compressing objects: 75% (9/12)
Compressing objects: 83% (10/12)
Compressing objects: 91% (11/12)
Compressing objects: 100% (12/12)
Compressing objects: 100% (12/12), done.
|
||||
Writing objects: 7% (1/13)
Writing objects: 15% (2/13)
Writing objects: 23% (3/13)
Writing objects: 30% (4/13)
Writing objects: 38% (5/13)
Writing objects: 46% (6/13)
Writing objects: 53% (7/13)
Writing objects: 61% (8/13)
Writing objects: 69% (9/13)
Writing objects: 76% (10/13)
Writing objects: 84% (11/13)
Writing objects: 92% (12/13)
Writing objects: 100% (13/13)
Writing objects: 100% (13/13), 10.48 KiB | 10.48 MiB/s, done.
|
||||
Total 13 (delta 8), 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
|
||||
927b3a9..4f91b34 main -> main
|
||||
11
push19.log
11
push19.log
File diff suppressed because one or more lines are too long
10
push20.log
10
push20.log
@ -1,10 +0,0 @@
|
||||
Enumerating objects: 38, done.
|
||||
Counting objects: 2% (1/38)
Counting objects: 5% (2/38)
Counting objects: 7% (3/38)
Counting objects: 10% (4/38)
Counting objects: 13% (5/38)
Counting objects: 15% (6/38)
Counting objects: 18% (7/38)
Counting objects: 21% (8/38)
Counting objects: 23% (9/38)
Counting objects: 26% (10/38)
Counting objects: 28% (11/38)
Counting objects: 31% (12/38)
Counting objects: 34% (13/38)
Counting objects: 36% (14/38)
Counting objects: 39% (15/38)
Counting objects: 42% (16/38)
Counting objects: 44% (17/38)
Counting objects: 47% (18/38)
Counting objects: 50% (19/38)
Counting objects: 52% (20/38)
Counting objects: 55% (21/38)
Counting objects: 57% (22/38)
Counting objects: 60% (23/38)
Counting objects: 63% (24/38)
Counting objects: 65% (25/38)
Counting objects: 68% (26/38)
Counting objects: 71% (27/38)
Counting objects: 73% (28/38)
Counting objects: 76% (29/38)
Counting objects: 78% (30/38)
Counting objects: 81% (31/38)
Counting objects: 84% (32/38)
Counting objects: 86% (33/38)
Counting objects: 89% (34/38)
Counting objects: 92% (35/38)
Counting objects: 94% (36/38)
Counting objects: 97% (37/38)
Counting objects: 100% (38/38)
Counting objects: 100% (38/38), done.
|
||||
Delta compression using up to 32 threads
|
||||
Compressing objects: 4% (1/23)
Compressing objects: 8% (2/23)
Compressing objects: 13% (3/23)
Compressing objects: 17% (4/23)
Compressing objects: 21% (5/23)
Compressing objects: 26% (6/23)
Compressing objects: 30% (7/23)
Compressing objects: 34% (8/23)
Compressing objects: 39% (9/23)
Compressing objects: 43% (10/23)
Compressing objects: 47% (11/23)
Compressing objects: 52% (12/23)
Compressing objects: 56% (13/23)
Compressing objects: 60% (14/23)
Compressing objects: 65% (15/23)
Compressing objects: 69% (16/23)
Compressing objects: 73% (17/23)
Compressing objects: 78% (18/23)
Compressing objects: 82% (19/23)
Compressing objects: 86% (20/23)
Compressing objects: 91% (21/23)
Compressing objects: 95% (22/23)
Compressing objects: 100% (23/23)
Compressing objects: 100% (23/23), done.
|
||||
Writing objects: 4% (1/25)
Writing objects: 8% (2/25)
Writing objects: 12% (3/25)
Writing objects: 16% (4/25)
Writing objects: 20% (5/25)
Writing objects: 24% (6/25)
Writing objects: 32% (8/25)
Writing objects: 36% (9/25)
Writing objects: 40% (10/25)
Writing objects: 44% (11/25)
Writing objects: 48% (12/25)
Writing objects: 52% (13/25)
Writing objects: 56% (14/25)
Writing objects: 60% (15/25)
Writing objects: 64% (16/25)
Writing objects: 68% (17/25)
Writing objects: 72% (18/25)
Writing objects: 76% (19/25)
Writing objects: 80% (20/25)
Writing objects: 84% (21/25)
Writing objects: 88% (22/25)
Writing objects: 92% (23/25)
Writing objects: 96% (24/25)
Writing objects: 100% (25/25)
Writing objects: 100% (25/25), 211.82 KiB | 30.26 MiB/s, done.
|
||||
Total 25 (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
|
||||
8967972..6d740ad main -> main
|
||||
15
src/game.gd
15
src/game.gd
@ -117,9 +117,11 @@ 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_strikes := 0 # blown shifts this run; box_limit grenades it
|
||||
var box_limit := 3 # 4 if your last gearbox rebuild scored 85+
|
||||
var box_blown := false
|
||||
var gasket_blown := false
|
||||
var gasket_cap := 1.0 # 1.18 if your last gasket job scored 85+
|
||||
|
||||
var derby: Array[Cop] = [] # arena opponents; Cop AI already hunts a target
|
||||
var derby_centre: Vector3
|
||||
@ -290,6 +292,11 @@ func _ready() -> void:
|
||||
sc.add_point(drag_end + Vector3.UP * (drag_end_y + 0.7) + lane + fwd * 120.0)
|
||||
strip.curve = sc
|
||||
add_child(strip)
|
||||
# workmanship pays here: an 85+ gearbox rebuild takes an extra blown
|
||||
# shift, an 85+ gasket job raises the overheat ceiling
|
||||
var wcid := Garage.car_id_of(car_path)
|
||||
box_limit = 3 + (1 if Garage.quality(wcid, "gearbox") >= 85.0 else 0)
|
||||
gasket_cap = 1.0 + (0.18 if Garage.quality(wcid, "gasket") >= 85.0 else 0.0)
|
||||
# the ladder: your current rung decides who's in the other lane
|
||||
drag_rung_now = mini(Garage.drag_rung(), DRAG_LADDER.size() - 1)
|
||||
var rung: Dictionary = DRAG_LADDER[drag_rung_now]
|
||||
@ -826,7 +833,7 @@ func _drag_tick(delta: float) -> void:
|
||||
box_strikes += 1
|
||||
bog = 0.9
|
||||
Sfx.shot(car, "bog", -2.0)
|
||||
if box_strikes >= 3:
|
||||
if box_strikes >= box_limit:
|
||||
# third mistreated shift: the box keeps what's in it
|
||||
box_blown = true
|
||||
car.mech_pop()
|
||||
@ -843,10 +850,10 @@ func _drag_tick(delta: float) -> void:
|
||||
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)
|
||||
eng_temp = minf(eng_temp + delta * 0.06, gasket_cap - 0.01 if not gasket_blown else gasket_cap)
|
||||
else:
|
||||
eng_temp = maxf(eng_temp - delta * 0.04, 0.0)
|
||||
if eng_temp >= 1.0 and not gasket_blown:
|
||||
if eng_temp >= gasket_cap and not gasket_blown:
|
||||
gasket_blown = true
|
||||
car.set_steam(true)
|
||||
Sfx.shot(car, "bog", -2.0, 0.55)
|
||||
|
||||
@ -61,19 +61,65 @@ const CATALOGUE := {
|
||||
## THE SHED: workshop tools are global, bought once, and each unlocks a
|
||||
## per-car TUNE-UP. Parts buy bling; the shed buys labour. Provenance of the
|
||||
## hoist is not to be discussed.
|
||||
## Each tool line has THREE tiers: Cashies-grade, Supercheap-grade, and
|
||||
## SNAP-OFF PROFESSIONAL. Higher tiers change how jobs FEEL in the wrench
|
||||
## minigame (fewer turns, wider torque windows, the rattle gun, a crane that
|
||||
## does the lifting). Top-level name/price/desc mirror tier 1 for old code.
|
||||
const TOOLS := {
|
||||
"ramps": {"name": "SUPERCHEAP WHEEL RAMPS", "price": 800,
|
||||
"desc": "Plastic. Rated to \"most cars, probably\"."},
|
||||
"desc": "Plastic. Rated to \"most cars, probably\".",
|
||||
"tiers": [
|
||||
{"name": "SUPERCHEAP WHEEL RAMPS", "price": 800,
|
||||
"desc": "Plastic. Rated to \"most cars, probably\". Jacking: slow."},
|
||||
{"name": "STEEL RAMPS + CHOCKS", "price": 2000,
|
||||
"desc": "Actual metal. The car goes up like it means it."},
|
||||
{"name": "QUICK-JACK (KNOCKOFF)", "price": 4000,
|
||||
"desc": "Whoosh. Three clicks and it's in the air."}]},
|
||||
"reader": {"name": "CASHIES CODE READER", "price": 1200,
|
||||
"desc": "One previous owner. Screen cracked. Works if you squint."},
|
||||
"desc": "One previous owner. Screen cracked. Works if you squint.",
|
||||
"tiers": [
|
||||
{"name": "CASHIES CODE READER", "price": 1200,
|
||||
"desc": "Screen cracked. Points at the right light, roughly."},
|
||||
{"name": "SUPERCHEAP SCAN TOOL", "price": 3000,
|
||||
"desc": "Whole screen works. Names the fault outright."},
|
||||
{"name": "SNAP-OFF DIAGNOSTICS TABLET", "price": 6000,
|
||||
"desc": "Knows what's wrong before you park. Skips diagnosis."}]},
|
||||
"sockets": {"name": "ALDI SOCKET SET", "price": 600,
|
||||
"desc": "Sunday special. Metric-ish."},
|
||||
"desc": "Sunday special. Metric-ish.",
|
||||
"tiers": [
|
||||
{"name": "ALDI SOCKET SET", "price": 600,
|
||||
"desc": "Sunday special. Metric-ish."},
|
||||
{"name": "SUPERCHEAP 200-PIECE", "price": 1500,
|
||||
"desc": "A socket for everything, 30% fewer turns."},
|
||||
{"name": "SNAP-OFF PROFESSIONAL", "price": 3000,
|
||||
"desc": "Fell off a van. Half the turns, forgiving torque."}]},
|
||||
"compressor": {"name": "BUNNINGS AIR COMPRESSOR", "price": 1500,
|
||||
"desc": "Came with three fittings, none of them the right one."},
|
||||
"desc": "Came with three fittings, none of them the right one.",
|
||||
"tiers": [
|
||||
{"name": "BUNNINGS AIR COMPRESSOR", "price": 1500,
|
||||
"desc": "Came with three fittings, none of them the right one."},
|
||||
{"name": "COMPRESSOR + RATTLE GUN", "price": 3500,
|
||||
"desc": "BRRRT. Wheel nuts become a clicking matter."},
|
||||
{"name": "SHOP AIR, PLUMBED", "price": 7000,
|
||||
"desc": "BRRRT but faster, and the neighbours hate you."}]},
|
||||
"hoist": {"name": "CASHIES CHAIN HOIST (ON WHEELS)", "price": 2500,
|
||||
"desc": "Definitely not stolen. Rolls beautifully."},
|
||||
"desc": "Definitely not stolen. Rolls beautifully.",
|
||||
"tiers": [
|
||||
{"name": "CASHIES CHAIN HOIST (ON WHEELS)", "price": 2500,
|
||||
"desc": "Definitely not stolen. Does the 60 kg lifting."},
|
||||
{"name": "2-TONNE CRANE, NEW CHAINS", "price": 5000,
|
||||
"desc": "Half the pumping, none of the creaking."},
|
||||
{"name": "GANTRY + ELECTRIC WINCH", "price": 9000,
|
||||
"desc": "Hooks itself up. You just point."}]},
|
||||
"additives": {"name": "SERVO ADDITIVE CRATE", "price": 400,
|
||||
"desc": "A crate of bottles that all claim +30 horsepower."},
|
||||
"desc": "A crate of bottles that all claim +30 horsepower.",
|
||||
"tiers": [
|
||||
{"name": "SERVO ADDITIVE CRATE", "price": 400,
|
||||
"desc": "A crate of bottles that all claim +30 horsepower."},
|
||||
{"name": "PROPER FLUIDS SHELF", "price": 1000,
|
||||
"desc": "Funnels that fit. Pours nearly pour themselves."},
|
||||
{"name": "BULK DRUMS + PUMP", "price": 2000,
|
||||
"desc": "Tap the drum, walk away, come back to full."}]},
|
||||
}
|
||||
## tune id -> {name, desc, price (labour), tool (required)}
|
||||
const TUNES := {
|
||||
@ -147,17 +193,39 @@ static func tools() -> Array:
|
||||
static func has_tool(tool: String) -> bool:
|
||||
return tool in tools()
|
||||
|
||||
static func buy_tool(tool: String) -> bool:
|
||||
static func tool_tier(tool: String) -> int:
|
||||
## 0 = not owned; 1..3 = owned tier. Pre-tier saves count as tier 1.
|
||||
_load()
|
||||
if not TOOLS.has(tool) or has_tool(tool):
|
||||
if not has_tool(tool):
|
||||
return 0
|
||||
return int(_data.get("tool_tiers", {}).get(tool, 1))
|
||||
|
||||
static func tool_upgrade_price(tool: String) -> int:
|
||||
## Price of the NEXT tier, or 0 when maxed.
|
||||
var t := tool_tier(tool)
|
||||
var tiers: Array = TOOLS[tool]["tiers"]
|
||||
return int(tiers[t]["price"]) if t < tiers.size() else 0
|
||||
|
||||
static func buy_tool(tool: String) -> bool:
|
||||
## First purchase = tier 1; buying again upgrades a tier, up to 3.
|
||||
_load()
|
||||
if not TOOLS.has(tool):
|
||||
return false
|
||||
var price: int = TOOLS[tool]["price"]
|
||||
var t := tool_tier(tool)
|
||||
var tiers: Array = TOOLS[tool]["tiers"]
|
||||
if t >= tiers.size():
|
||||
return false
|
||||
var price: int = tiers[t]["price"]
|
||||
if int(_data["bucks"]) < price:
|
||||
return false
|
||||
_data["bucks"] = int(_data["bucks"]) - price
|
||||
var lst: Array = _data.get("tools", [])
|
||||
lst.append(tool)
|
||||
_data["tools"] = lst
|
||||
if t == 0:
|
||||
var lst: Array = _data.get("tools", [])
|
||||
lst.append(tool)
|
||||
_data["tools"] = lst
|
||||
var tt: Dictionary = _data.get("tool_tiers", {})
|
||||
tt[tool] = t + 1
|
||||
_data["tool_tiers"] = tt
|
||||
_flush()
|
||||
return true
|
||||
|
||||
@ -197,10 +265,10 @@ static func buy_tune(car_id: String, tune: String) -> bool:
|
||||
## scene: which minigame fixes it -- "repair" (2D panel) or "wrench" (3D shed)
|
||||
const FAULTS := {
|
||||
"gearbox": {"name": "GRENADED GEARBOX", "light": "DRIVETRAIN", "shop": 900,
|
||||
"tool": "hoist", "scene": "repair",
|
||||
"tool": "hoist", "scene": "wrench",
|
||||
"desc": "Third gear is a maraca now. Leaks on everything it loves."},
|
||||
"gasket": {"name": "BLOWN HEAD GASKET", "light": "ENGINE TEMP", "shop": 700,
|
||||
"tool": "sockets", "scene": "repair",
|
||||
"tool": "sockets", "scene": "wrench",
|
||||
"desc": "Mayonnaise under the oil cap. Not the good kind."},
|
||||
"flat": {"name": "FLAT-SPOTTED TYRES", "light": "TYRE PRESSURE", "shop": 250,
|
||||
"tool": "sockets", "scene": "wrench",
|
||||
|
||||
@ -102,11 +102,19 @@ func _refresh() -> void:
|
||||
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 once, unlock tune-ups) ---")
|
||||
_header("--- THE SHED (tools; buy again to upgrade the tier) ---")
|
||||
for id in Garage.TOOLS:
|
||||
var c: Dictionary = Garage.TOOLS[id]
|
||||
var tag := "[OWNED]" if Garage.has_tool(id) else "$%d" % c["price"]
|
||||
list.add_item("%-28s %s" % [c["name"], tag])
|
||||
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:
|
||||
@ -140,8 +148,12 @@ func _entry_info(i: int) -> Dictionary:
|
||||
"done": Garage.has(car_id, e["id"]), "locked": false}
|
||||
"tool":
|
||||
var c: Dictionary = Garage.TOOLS[e["id"]]
|
||||
return {"desc": c["desc"], "price": c["price"],
|
||||
"done": Garage.has_tool(e["id"]), "locked": false}
|
||||
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"],
|
||||
@ -177,11 +189,10 @@ func _on_buy() -> void:
|
||||
"tune": ok = Garage.buy_tune(car_id, e["id"])
|
||||
"shopfix": ok = Garage.shop_fix(car_id, e["id"])
|
||||
"fault":
|
||||
# DIY: off to whichever minigame owns this fault (repair = 2D panel,
|
||||
# wrench = 3D shed); it clears the fault on completion
|
||||
# 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", "repair")))
|
||||
% String(Garage.FAULTS[e["id"]].get("scene", "wrench")))
|
||||
return
|
||||
if ok:
|
||||
Sfx.shot_2d("clunk")
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
[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
369
src/repair_ui.gd
@ -1,369 +0,0 @@
|
||||
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 +0,0 @@
|
||||
uid://bray7xq7dem20
|
||||
242
src/wrench.gd
242
src/wrench.gd
@ -14,8 +14,16 @@ extends Node3D
|
||||
## grab, drop, yank_ok, steady_tick, clamp, twist_release, skip_wait) so the
|
||||
## smoke suite plays entire jobs headless.
|
||||
|
||||
const WINDOW := 0.8 # rad of "seated" travel after the turns are in
|
||||
const CAR_AT := Vector3(12, 0.0, -14.2) # the shed's painted bay
|
||||
## Tool tiers change how jobs FEEL. Tables indexed by Garage.tool_tier() 0..3.
|
||||
## typed so indexing them stays inference-friendly (the := trap, 4th visit)
|
||||
const TURN_MULT: Array[float] = [1.6, 1.0, 0.7, 0.5] # sockets tier
|
||||
const WINDOW_W: Array[float] = [0.8, 0.8, 1.0, 1.3] # torque forgiveness, rad
|
||||
const JACK_CLICKS: Array[int] = [10, 8, 5, 3] # ramps
|
||||
const POUR_MULT: Array[float] = [1.3, 1.0, 0.6, 0.4] # additives
|
||||
const RATTLE_CLICKS: Array[int] = [0, 0, 4, 2] # compressor >=2: BRRRT
|
||||
const CRANE_CLICKS: Array[int] = [0, 10, 5, 4] # hoist: crane pumping
|
||||
const LIGHT_POOL := ["ENGINE TEMP", "DRIVETRAIN", "OIL PRESSURE", "BATTERY", "AIRBAG (ALWAYS ON)"]
|
||||
|
||||
var car_id := ""
|
||||
var fault := ""
|
||||
@ -31,6 +39,10 @@ var pump_count := 0
|
||||
var steady_need := 0.0
|
||||
var wait_left := 0.0
|
||||
var clamps_done: Array = []
|
||||
var penalty_turns := 0.0 # wrong diagnosis: every nut gets this much longer
|
||||
var light_options: Array = []
|
||||
var _light_btns: Array = []
|
||||
var _diag_layer: CanvasLayer = null
|
||||
var props := {} # name -> Node3D (spare wheel, battery, tray...)
|
||||
var model: Node3D
|
||||
var cam: Camera3D
|
||||
@ -134,48 +146,77 @@ func _anchor_engine() -> Vector3:
|
||||
func _anchor_sump() -> Vector3:
|
||||
return CAR_AT + Vector3(0.2, 0.28, -stats.length * 0.18)
|
||||
|
||||
func _t(base: float) -> float:
|
||||
return base * TURN_MULT[Garage.tool_tier("sockets")]
|
||||
|
||||
func _win() -> float:
|
||||
return WINDOW_W[Garage.tool_tier("sockets")]
|
||||
|
||||
func _sockets_note() -> String:
|
||||
return ["VICE GRIPS: this will end badly", "ALDI SOCKETS: fits, mostly",
|
||||
"SUPERCHEAP 200-PIECE: there IS a socket for that",
|
||||
"SNAP-OFF PROFESSIONAL: it just... undoes"][Garage.tool_tier("sockets")]
|
||||
|
||||
func _apply_tiers(sheet: Array) -> Array:
|
||||
## Post-pass: the rattle gun (compressor t2+) turns non-torque nut twists
|
||||
## into BRRRT clicks. Torque windows keep the hand skill -- guns strip.
|
||||
var rattle := RATTLE_CLICKS[Garage.tool_tier("compressor")]
|
||||
if rattle > 0:
|
||||
for i in sheet.size():
|
||||
var st: Dictionary = sheet[i]
|
||||
if st["verb"] == "twist" and bool(st.get("nut", false)) and not bool(st.get("window", false)):
|
||||
sheet[i] = {"verb": "pump", "clicks": rattle, "cam": st["cam"],
|
||||
"label": st["label"] + " (BRRRT)", "note": "the rattle gun eats this step"}
|
||||
# gearbox/gasket aren't obvious from the driver's seat: diagnose first,
|
||||
# unless the SNAP-OFF tablet already told you on the way in
|
||||
if fault in ["gearbox", "gasket"] and Garage.tool_tier("reader") < 3:
|
||||
sheet.push_front({"verb": "diagnose", "cam": "bonnet",
|
||||
"label": "WHAT'S THAT LIGHT, THEN?", "note": ""})
|
||||
return sheet
|
||||
|
||||
func _job() -> Array:
|
||||
var have_sockets := Garage.has_tool("sockets")
|
||||
var t := 3.0 if have_sockets else 6.0
|
||||
var pour := POUR_MULT[Garage.tool_tier("additives")]
|
||||
var jack := JACK_CLICKS[Garage.tool_tier("ramps")]
|
||||
var hoist := Garage.tool_tier("hoist")
|
||||
match fault:
|
||||
"flat":
|
||||
return [
|
||||
{"verb": "pump", "clicks": 8, "cam": "wheel", "label": "JACK IT UP",
|
||||
return _apply_tiers([
|
||||
{"verb": "pump", "clicks": jack, "cam": "wheel", "label": "JACK IT UP",
|
||||
"note": "click it like you mean it"},
|
||||
{"verb": "twist", "dir": -1.0, "turns": t, "window": false, "cam": "wheel",
|
||||
"label": "CRACK THE NUTS OFF",
|
||||
"note": "ALDI SOCKETS: fits, mostly" if have_sockets else "VICE GRIPS: this will end badly"},
|
||||
{"verb": "twist", "dir": -1.0, "turns": _t(3.0), "window": false, "nut": true,
|
||||
"cam": "wheel", "label": "CRACK THE NUTS OFF", "note": _sockets_note()},
|
||||
{"verb": "yank", "cam": "wheel", "label": "POP THE FLAT OFF",
|
||||
"note": "press on it and FLICK"},
|
||||
{"verb": "grab", "item": "spare", "at": "wheel", "cam": "wheel",
|
||||
"label": "SPARE ON", "note": "grab it, hang it on the studs"},
|
||||
{"verb": "steady", "secs": 2.0, "cam": "wheel", "label": "LINE UP THE STUDS",
|
||||
"note": "keep the cursor in the ring"},
|
||||
{"verb": "twist", "dir": 1.0, "turns": t, "window": true, "cam": "wheel",
|
||||
"label": "TORQUE THE NUTS", "note": "release IN the green -- past it strips"},
|
||||
{"verb": "twist", "dir": 1.0, "turns": _t(3.0), "window": true, "nut": true,
|
||||
"cam": "wheel", "label": "TORQUE THE NUTS",
|
||||
"note": "release IN the green -- past it strips"},
|
||||
{"verb": "pump", "clicks": 4, "cam": "wheel", "label": "LET IT DOWN",
|
||||
"note": "gently. gently."},
|
||||
]
|
||||
])
|
||||
"oil":
|
||||
return [
|
||||
return _apply_tiers([
|
||||
{"verb": "grab", "item": "tray", "at": "sump", "cam": "under",
|
||||
"label": "TRAY UNDER THE SUMP", "note": "future-you says thanks"},
|
||||
{"verb": "twist", "dir": -1.0, "turns": 1.5, "window": false, "cam": "under",
|
||||
{"verb": "twist", "dir": -1.0, "turns": _t(1.5), "window": false, "cam": "under",
|
||||
"label": "CRACK THE DRAIN PLUG", "note": "it's tighter than it needs to be"},
|
||||
{"verb": "yank", "cam": "under", "label": "PLUG OUT", "note": "flick -- and stand back"},
|
||||
{"verb": "wait", "secs": 2.0, "cam": "under", "label": "LET HER DRAIN",
|
||||
"note": "black gold. worthless black gold."},
|
||||
{"verb": "twist", "dir": 1.0, "turns": 1.5, "window": true, "cam": "under",
|
||||
{"verb": "twist", "dir": 1.0, "turns": _t(1.5), "window": true, "cam": "under",
|
||||
"label": "PLUG BACK IN", "note": "release in the green: sump threads are soft"},
|
||||
{"verb": "grab", "item": "bottle", "at": "engine", "cam": "bonnet",
|
||||
"label": "FRESH OIL UP TOP", "note": "the good stuff ($12/5L)"},
|
||||
{"verb": "steady", "secs": 2.5, "cam": "bonnet", "label": "POUR IT IN, STEADY",
|
||||
{"verb": "steady", "secs": 2.5 * pour, "cam": "bonnet", "label": "POUR IT IN, STEADY",
|
||||
"note": "outside the ring = on the manifold"},
|
||||
]
|
||||
])
|
||||
"battery":
|
||||
return [
|
||||
return _apply_tiers([
|
||||
{"verb": "yank", "cam": "bonnet", "label": "BONNET UP", "note": "flick it open"},
|
||||
{"verb": "twist", "dir": -1.0, "turns": 1.0, "window": false, "cam": "bonnet",
|
||||
{"verb": "twist", "dir": -1.0, "turns": _t(1.0), "window": false, "cam": "bonnet",
|
||||
"label": "CLAMPS OFF", "note": "10mm. it's always 10mm. where's the 10mm"},
|
||||
{"verb": "grab", "item": "old_batt", "at": "bench", "cam": "bonnet",
|
||||
"label": "DEAD ONE OUT", "note": "heavier than it looks"},
|
||||
@ -185,7 +226,52 @@ func _job() -> Array:
|
||||
{"verb": "clamps", "cam": "bonnet", "label": "CLAMP IT UP: RED THEN BLACK",
|
||||
"note": "get it backwards and you'll know"},
|
||||
{"verb": "yank", "cam": "bonnet", "label": "BONNET DOWN", "note": "one dignified slam"},
|
||||
])
|
||||
"gasket":
|
||||
return _apply_tiers([
|
||||
{"verb": "yank", "cam": "bonnet", "label": "BONNET UP", "note": "flick it open"},
|
||||
{"verb": "twist", "dir": -1.0, "turns": _t(4.0), "window": false, "nut": true,
|
||||
"cam": "bonnet", "label": "UNDO THE HEAD BOLTS", "note": _sockets_note()},
|
||||
{"verb": "yank", "cam": "bonnet", "label": "OLD GASKET OUT",
|
||||
"note": "flick it. mind the mayonnaise."},
|
||||
{"verb": "grab", "item": "gasket", "at": "engine", "cam": "bonnet",
|
||||
"label": "NEW GASKET ON", "note": "EBAY, \"GENUINE\". sure."},
|
||||
{"verb": "steady", "secs": 2.0, "cam": "bonnet", "label": "SEAT IT SQUARE",
|
||||
"note": "keep it in the ring or it weeps forever"},
|
||||
{"verb": "twist", "dir": 1.0, "turns": _t(4.0), "window": true, "nut": true,
|
||||
"cam": "bonnet", "label": "TORQUE THE HEAD DOWN",
|
||||
"note": "release IN the green -- head bolts do not forgive"},
|
||||
{"verb": "steady", "secs": 2.5 * pour, "cam": "bonnet", "label": "COOLANT BACK IN",
|
||||
"note": "steady pour. the radiator's been through enough"},
|
||||
{"verb": "yank", "cam": "bonnet", "label": "BONNET DOWN", "note": "one dignified slam"},
|
||||
])
|
||||
"gearbox":
|
||||
var sheet: Array = [
|
||||
{"verb": "pump", "clicks": jack, "cam": "under", "label": "JACK IT UP",
|
||||
"note": "click it like you mean it"},
|
||||
{"verb": "twist", "dir": -1.0, "turns": _t(5.0), "window": false, "nut": true,
|
||||
"cam": "under", "label": "UNDO THE BELLHOUSING BOLTS", "note": _sockets_note()},
|
||||
]
|
||||
if hoist == 0:
|
||||
sheet.append({"verb": "grab", "item": "dead_box", "at": "bench", "cam": "under",
|
||||
"label": "DRAG THE DEAD BOX OUT (it's 60 kg)", "note": "no crane. just you and regret"})
|
||||
sheet.append({"verb": "steady", "secs": 2.0, "cam": "under",
|
||||
"label": "DON'T DROP IT ON YOURSELF", "note": "hold the ring. HOLD IT."})
|
||||
else:
|
||||
if hoist < 3:
|
||||
sheet.append({"verb": "grab", "item": "chain", "at": "box", "cam": "under",
|
||||
"label": "HOOK THE CRANE UP", "note": "chain to the box. the crane does the rest"})
|
||||
sheet.append({"verb": "pump", "clicks": CRANE_CLICKS[hoist], "cam": "under",
|
||||
"label": "CRANE IT OUT", "note": ["", "the Cashies crane creaks but lifts",
|
||||
"new chains. barely any creak", "the winch just... does it"][hoist]})
|
||||
sheet.append({"verb": "grab", "item": "cashies_box", "at": "tunnel", "cam": "under",
|
||||
"label": "CASHIES BOX IN", "note": "GEARBOX -- ONE OWNER, HONEST"})
|
||||
sheet.append({"verb": "twist", "dir": 1.0, "turns": _t(5.0), "window": true, "nut": true,
|
||||
"cam": "under", "label": "BOLT IT ALL BACK UP",
|
||||
"note": "release IN the green -- you KNOW what happens"})
|
||||
sheet.append({"verb": "pump", "clicks": 4, "cam": "under", "label": "LET IT DOWN",
|
||||
"note": "and breathe"})
|
||||
return _apply_tiers(sheet)
|
||||
return []
|
||||
|
||||
## ---- step lifecycle --------------------------------------------------------
|
||||
@ -200,6 +286,9 @@ func _show_step() -> void:
|
||||
if step_index >= steps.size():
|
||||
_finish()
|
||||
return
|
||||
if _diag_layer:
|
||||
_diag_layer.queue_free()
|
||||
_diag_layer = null
|
||||
var st: Dictionary = steps[step_index]
|
||||
step_label.text = "%s (%d/%d)" % [st["label"], step_index + 1, steps.size()]
|
||||
note_label.text = String(st.get("note", ""))
|
||||
@ -213,13 +302,65 @@ func _show_step() -> void:
|
||||
steady_need = float(st["secs"])
|
||||
"grab":
|
||||
_spawn_prop(String(st["item"]))
|
||||
"yank":
|
||||
if fault == "battery" and step_index == 0:
|
||||
pass # bonnet handled on success
|
||||
"diagnose":
|
||||
_show_diagnose()
|
||||
ring.anchor_world = _step_anchor()
|
||||
ring.cam = cam
|
||||
ring.mode = String(st["verb"])
|
||||
|
||||
func _show_diagnose() -> void:
|
||||
## Three lit warning lights, one honest. The reader points (t1 garbled,
|
||||
## t2 outright); no reader means vibes. Wrong pick = every nut runs longer.
|
||||
var correct: String = Garage.FAULTS[fault]["light"]
|
||||
var pool := LIGHT_POOL.filter(func(l): return l != correct)
|
||||
pool.shuffle()
|
||||
light_options = [correct, pool[0], pool[1]]
|
||||
light_options.shuffle()
|
||||
var rt := Garage.tool_tier("reader")
|
||||
if rt >= 2:
|
||||
note_label.text = "SCAN TOOL SAYS: \"%s\". no drama." % correct
|
||||
elif rt == 1:
|
||||
note_label.text = "CASHIES READER SAYS: \"%s\" (screen cracked)" % _garble(correct)
|
||||
else:
|
||||
note_label.text = "no reader. vibes only."
|
||||
_diag_layer = CanvasLayer.new()
|
||||
add_child(_diag_layer)
|
||||
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
|
||||
_diag_layer.add_child(row)
|
||||
_light_btns.clear()
|
||||
for i in light_options.size():
|
||||
var b := Button.new()
|
||||
b.text = "(!) %s" % light_options[i]
|
||||
b.add_theme_font_size_override("font_size", 24)
|
||||
if rt >= 1 and light_options[i] == correct:
|
||||
b.modulate = Color(1.0, 0.75, 0.35)
|
||||
var idx := i
|
||||
b.pressed.connect(func() -> void: pick_light(idx))
|
||||
row.add_child(b)
|
||||
_light_btns.append(b)
|
||||
|
||||
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 pick_light(i: int) -> void:
|
||||
if done or steps[step_index]["verb"] != "diagnose" or i >= light_options.size():
|
||||
return
|
||||
if light_options[i] == Garage.FAULTS[fault]["light"]:
|
||||
Sfx.shot_2d("clunk", -6.0)
|
||||
_advance()
|
||||
else:
|
||||
penalty_turns += 1.0
|
||||
quality -= 5.0
|
||||
Sfx.shot_2d("bog", -6.0)
|
||||
note_label.text = "nope. pulled the wrong thing apart first -- every nut runs a turn longer now."
|
||||
qual_label.text = "QUALITY %d%%" % int(quality)
|
||||
|
||||
func _step_anchor() -> Vector3:
|
||||
var st: Dictionary = steps[step_index]
|
||||
match String(st.get("cam", "wheel")):
|
||||
@ -275,6 +416,25 @@ func _spawn_prop(item: String) -> void:
|
||||
bm.material = _dark(Color(0.08, 0.08, 0.09) if item == "old_batt" else Color(0.12, 0.3, 0.12))
|
||||
mi.mesh = bm
|
||||
n.position = _anchor_engine() if item == "old_batt" else Vector3(10.5, 1.08, -19.8)
|
||||
"gasket":
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.45, 0.02, 0.3)
|
||||
bm.material = _dark(Color(0.55, 0.5, 0.4))
|
||||
mi.mesh = bm
|
||||
n.position = Vector3(10.5, 1.0, -19.8) # on the bench
|
||||
"dead_box", "cashies_box":
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.35, 0.4, 0.7)
|
||||
bm.material = _dark(Color(0.10, 0.10, 0.11) if item == "dead_box" else Color(0.16, 0.28, 0.16))
|
||||
mi.mesh = bm
|
||||
n.position = _anchor_sump() - Vector3(0, 0.15, 0) if item == "dead_box" \
|
||||
else Vector3(10.5, 1.15, -19.8)
|
||||
"chain":
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.05, 0.9, 0.05)
|
||||
bm.material = _dark(Color(0.35, 0.36, 0.38))
|
||||
mi.mesh = bm
|
||||
n.position = Vector3(15.2, 1.4, -17.0) # hanging off the crane
|
||||
n.add_child(mi)
|
||||
n.name = item
|
||||
add_child(n)
|
||||
@ -305,6 +465,10 @@ func _process(delta: float) -> void:
|
||||
|
||||
## ---- the verbs (test API) --------------------------------------------------
|
||||
|
||||
func _need() -> float:
|
||||
# wrong diagnosis makes every nut longer; better sockets never change THAT
|
||||
return (float(steps[step_index]["turns"]) + penalty_turns) * TAU
|
||||
|
||||
func twist(rad: float) -> void:
|
||||
if done or steps[step_index]["verb"] != "twist":
|
||||
return
|
||||
@ -313,14 +477,14 @@ func twist(rad: float) -> void:
|
||||
if toward <= 0.0:
|
||||
return
|
||||
twist_prog += toward
|
||||
var need: float = float(st["turns"]) * TAU
|
||||
var need := _need()
|
||||
if not bool(st["window"]):
|
||||
if twist_prog >= need:
|
||||
Sfx.shot_2d("clunk", -6.0)
|
||||
_advance()
|
||||
return
|
||||
# torque window: overshoot past the seat and the thread strips
|
||||
if twist_prog > need + WINDOW and not _stripped:
|
||||
if twist_prog > need + _win() and not _stripped:
|
||||
_stripped = true
|
||||
quality -= 10.0
|
||||
Sfx.shot_2d("bog", -4.0)
|
||||
@ -334,8 +498,8 @@ func twist_release() -> void:
|
||||
## releasing the mouse ends a torque attempt: clean if inside the window
|
||||
if done or steps[step_index]["verb"] != "twist" or not bool(steps[step_index]["window"]):
|
||||
return
|
||||
var need: float = float(steps[step_index]["turns"]) * TAU
|
||||
if twist_prog >= need and twist_prog <= need + WINDOW:
|
||||
var need := _need()
|
||||
if twist_prog >= need and twist_prog <= need + _win():
|
||||
Sfx.shot_2d("clunk", -4.0)
|
||||
_advance()
|
||||
elif twist_prog > 0.5:
|
||||
@ -354,11 +518,11 @@ func yank_ok() -> void:
|
||||
if done or steps[step_index]["verb"] != "yank":
|
||||
return
|
||||
Sfx.shot_2d("clunk", -6.0)
|
||||
var label := String(steps[step_index]["label"])
|
||||
if fault == "flat" and _wheel_mesh:
|
||||
_wheel_mesh.visible = false
|
||||
if fault == "battery" and _bonnet:
|
||||
# explicit bool: a Dictionary index is Variant and := can't see through it
|
||||
var opening: bool = steps[step_index]["label"] == "BONNET UP"
|
||||
if _bonnet and label.begins_with("BONNET"):
|
||||
var opening: bool = label == "BONNET UP"
|
||||
var tw := create_tween()
|
||||
tw.tween_property(_bonnet, "rotation:x", -0.9 if opening else 0.0, 0.4)
|
||||
_advance()
|
||||
@ -369,17 +533,23 @@ func grab(item: String) -> void:
|
||||
if steps[step_index]["item"] == item:
|
||||
carried = item
|
||||
|
||||
func _drop_target(at: String) -> Vector3:
|
||||
match at:
|
||||
"wheel": return _anchor_wheel()
|
||||
"sump": return _anchor_sump() - Vector3(0, 0.2, 0)
|
||||
"engine": return _anchor_engine()
|
||||
"bench": return Vector3(11.5, 1.0, -19.8)
|
||||
"box": # the dead gearbox under the car, waiting for the chain
|
||||
return _anchor_sump() - Vector3(0, 0.15, 0)
|
||||
"tunnel":
|
||||
return _anchor_sump() - Vector3(0, 0.1, 0)
|
||||
return _anchor_wheel()
|
||||
|
||||
func drop_at(world: Vector3) -> void:
|
||||
if done or carried == "" or steps[step_index]["verb"] != "grab":
|
||||
return
|
||||
var st: Dictionary = steps[step_index]
|
||||
var target: Vector3
|
||||
match String(st["at"]):
|
||||
"wheel": target = _anchor_wheel()
|
||||
"sump": target = _anchor_sump() - Vector3(0, 0.2, 0)
|
||||
"engine": target = _anchor_engine()
|
||||
"bench": target = Vector3(11.5, 1.0, -19.8)
|
||||
_: target = _anchor_wheel()
|
||||
var target := _drop_target(String(st["at"]))
|
||||
if world.distance_to(target) < 1.2:
|
||||
var p: Node3D = props.get(carried)
|
||||
if p:
|
||||
@ -558,7 +728,7 @@ class RingDraw extends Control:
|
||||
draw_arc(p, 42.0, 0, TAU, 40, Color(1, 0.82, 0.35, 0.8), 3.0)
|
||||
var w = parent.get("steps")[parent.get("step_index")]
|
||||
if bool(w.get("window", false)):
|
||||
var need: float = float(w["turns"]) * TAU
|
||||
var need: float = (float(w["turns"]) + float(parent.get("penalty_turns"))) * TAU
|
||||
var frac: float = clampf(float(parent.get("twist_prog")) / need, 0.0, 1.3)
|
||||
draw_arc(p, 52.0, -PI / 2, -PI / 2 + TAU * minf(frac, 1.0), 40,
|
||||
Color(0.3, 0.9, 0.4, 0.9) if frac >= 1.0 else Color(0.9, 0.9, 0.9, 0.7), 4.0)
|
||||
|
||||
@ -59,7 +59,15 @@ 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
|
||||
# tool tiers: buying again upgrades; effects are exercised by wrench jobs
|
||||
Garage.earn(1500 + 3000)
|
||||
_check(Garage.tool_tier("sockets") == 1, "owned sockets not tier 1")
|
||||
_check(Garage.buy_tool("sockets"), "tier 2 upgrade refused")
|
||||
_check(Garage.buy_tool("sockets"), "tier 3 upgrade refused")
|
||||
_check(Garage.tool_tier("sockets") == 3 and not Garage.buy_tool("sockets"),
|
||||
"tier cap broken (tier %d)" % Garage.tool_tier("sockets"))
|
||||
_check(Garage.has_tool("sockets"), "tiered tool lost has_tool")
|
||||
# faults: break it, boot wounded, pay the shop for one, wrench the other
|
||||
print(" .. repair")
|
||||
Garage.add_fault("datto_120y", "gearbox")
|
||||
Garage.add_fault("datto_120y", "gasket")
|
||||
@ -71,36 +79,45 @@ func _run() -> void:
|
||||
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
|
||||
# the gearbox goes through the 3D wrench: diagnose (wrong first, penalty),
|
||||
# then the toolless 60 kg drag path -- no hoist owned on this save
|
||||
Garage.ui_stats_path = "res://cars/datto_120y/stats.tres"
|
||||
Garage.ui_fault = "gearbox"
|
||||
var ui = (load("res://src/repair.tscn") as PackedScene).instantiate()
|
||||
var ui = (load("res://src/wrench.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())
|
||||
_check(ui.steps[0]["verb"] == "diagnose", "gearbox job skipped diagnosis")
|
||||
_check(ui.steps.size() == 8, "toolless gearbox sheet wrong (%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"]:
|
||||
for i in ui.light_options.size():
|
||||
if ui.light_options[i] == Garage.FAULTS["gearbox"]["light"]:
|
||||
right = i
|
||||
ui.pick_light((right + 1) % 3)
|
||||
_check(ui.penalty_turns > 0.0, "wrong diagnosis cost nothing")
|
||||
_check(ui.penalty_turns > 0.0 and ui.quality < 100.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")
|
||||
await _drive_wrench(ui, false)
|
||||
_check(ui.done, "gearbox job never finished (step %d)" % ui.step_index)
|
||||
_check(not Garage.faults("datto_120y").has("gearbox"), "wrench fix didn't clear the fault")
|
||||
_check(Garage.quality("datto_120y", "gearbox") >= 85.0, "clean-ish gearbox scored %d"
|
||||
% int(ui.quality))
|
||||
ui.free()
|
||||
# a gasket job at 85+ raises the overheat ceiling; gearbox 85+ adds a strike
|
||||
Garage.add_fault("datto_120y", "gasket")
|
||||
Garage.ui_fault = "gasket"
|
||||
ui = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
||||
root.add_child(ui)
|
||||
await process_frame
|
||||
await _drive_wrench(ui, false)
|
||||
_check(ui.done and Garage.quality("datto_120y", "gasket") >= 85.0,
|
||||
"gasket job didn't finish clean (%d)" % int(ui.quality))
|
||||
ui.free()
|
||||
Game.level_path = levels["willowbank_dragway"]
|
||||
gg = await _boot("drag")
|
||||
_check(gg.box_limit == 4, "85+ gearbox rebuild paid no extra strike (%d)" % gg.box_limit)
|
||||
_check(gg.gasket_cap > 1.1, "85+ gasket job raised no ceiling (%.2f)" % gg.gasket_cap)
|
||||
gg.free()
|
||||
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
|
||||
# THE SHED home base: owned tools materialise (sockets were bought above,
|
||||
# ramps were not), and rolling into the bay and holding still opens the garage
|
||||
print(" .. shed")
|
||||
@ -428,7 +445,13 @@ func _drive_wrench(wr, sabotage: bool) -> void:
|
||||
while not wr.done and guard < 30:
|
||||
guard += 1
|
||||
var st: Dictionary = wr.steps[wr.step_index]
|
||||
var need: float = (float(st.get("turns", 0.0)) + wr.penalty_turns) * TAU
|
||||
match String(st["verb"]):
|
||||
"diagnose":
|
||||
for i in wr.light_options.size():
|
||||
if wr.light_options[i] == Garage.FAULTS[wr.fault]["light"]:
|
||||
wr.pick_light(i)
|
||||
break
|
||||
"pump":
|
||||
for i in int(st["clicks"]):
|
||||
wr.pump()
|
||||
@ -436,18 +459,17 @@ func _drive_wrench(wr, sabotage: bool) -> void:
|
||||
if bool(st.get("window", false)):
|
||||
if sabotage and not sabotaged:
|
||||
sabotaged = true
|
||||
wr.twist(float(st["dir"]) * (float(st["turns"]) * TAU + 2.0))
|
||||
wr.twist(float(st["dir"]) * (need + 4.0))
|
||||
else:
|
||||
wr.twist(float(st["dir"]) * (float(st["turns"]) * TAU + 0.3))
|
||||
wr.twist(float(st["dir"]) * (need + 0.3))
|
||||
wr.twist_release()
|
||||
else:
|
||||
wr.twist(float(st["dir"]) * (float(st["turns"]) * TAU + 0.2))
|
||||
wr.twist(float(st["dir"]) * (need + 0.2))
|
||||
"yank":
|
||||
wr.yank_ok()
|
||||
"grab":
|
||||
wr.grab(String(st["item"]))
|
||||
wr.drop_at(Vector3(11.5, 1.0, -19.8) if String(st["at"]) == "bench"
|
||||
else wr._step_anchor())
|
||||
wr.drop_at(wr._drop_target(String(st["at"])))
|
||||
"steady":
|
||||
while not wr.done and wr.steps[wr.step_index]["verb"] == "steady":
|
||||
wr.steady_tick(true, 0.5)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user