Wrench phase 1: 3D jobs on your car in your shed -- your hands are the tune
The repair game grows its 3D half (src/wrench.gd): flat tyre, oil
change and dead battery are done on the player's actual car model,
parked in the actual shed level, through five mouse verbs on projected
3D anchor points:
GRAB part follows the cursor on a ray-plane, drop at the target
TWIST circle the mouse around the anchor; torque steps have a seat
window (release inside = clean) and blowing past it STRIPS
the thread -- quality hit plus an extraction step inserted
PUMP rhythm clicks (jack up, jack down)
YANK press and flick (pop the wheel, bonnet up)
STEADY keep the cursor inside a wandering ring (line up the studs,
pour the oil)
Camera presets per step (wheel / under-car / bonnet), a screen-space
ring overlay with a live torque arc, and props spawned per job (spare,
tray, bottle, batteries). The battery job enforces RED THEN BLACK --
backwards earns a spark shower and commentary.
Workmanship is a stat: every job scores 0-100, and an 85+ fix BUFFS the
car through Garage.apply (tyres +0.2 grip, oil +3% power, battery +0.15
boost) until that fault recurs -- add_fault clears its quality entry, so
your last good fix is exactly as durable as your driving.
Faults now have gameplay sources: a red light at the tree flat-spots
the fronts, every 4th wreck rattles the sump loose (oil light, not in
junctions where wrecking is the game), and getting busted kills the
battery. All three are suite-inert by construction. New faults route to
the wrench scene via FAULTS[id].scene; gearbox/gasket keep the 2D panel
until phase 2. Wounds: flat -0.35 grip, oil -7% power, battery -0.3
boost (boost floor clamped at 0.4 -- the old ba>0 guard never subtracted).
Smoke plays a clean flat-tyre job through the API (7 steps, quality
90+, grip buff verified on the next boot) and a sabotaged oil job (blown
torque window -> stripped thread -> extraction step appears). The same
Variant-inference parse trap struck a third time (Dictionary index this
time); the suite caught it before it cost anything.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3e9a8616ab
commit
a5158bf6ec
19
README.md
19
README.md
@ -151,6 +151,25 @@ Purchases are per-car (tools are global) and persist in `user://garage.save`,
|
||||
which also carries the drag **ladder rung**, per-car **quarter-mile PBs** and
|
||||
outstanding **faults**.
|
||||
|
||||
**The wrench minigame** ([src/wrench.gd](src/wrench.gd)) is the 3D half of
|
||||
repairs, phase 1: flat tyre, oil change and dead battery happen on YOUR car,
|
||||
parked in YOUR shed, through five mouse verbs on 3D anchor points -- GRAB
|
||||
(part follows the cursor, drop it at the target), TWIST (circle the mouse;
|
||||
torque steps have a seat window and blowing past it STRIPS the thread, adding
|
||||
an extraction step), PUMP (rhythm clicks), YANK (flick), and STEADY (keep the
|
||||
cursor in a wandering ring). Every job scores 0-100 for workmanship; finish at
|
||||
85+ and the fix is a live buff via Garage.apply (grip for tyres, power for
|
||||
oil, boost for battery) until that fault recurs -- your hands are the tune.
|
||||
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.
|
||||
|
||||
**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,
|
||||
wheelie bins at the gate, and the corrugated double shed on its slab. The tools
|
||||
|
||||
10
push20.log
Normal file
10
push20.log
Normal file
@ -0,0 +1,10 @@
|
||||
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
|
||||
14
src/game.gd
14
src/game.gd
@ -960,10 +960,11 @@ func _drag_finish(crossed: bool) -> void:
|
||||
lines.append("UP THE LADDER: next is %s" % DRAG_LADDER[drag_rung_now + 1]["name"])
|
||||
elif drag_rung_now == DRAG_LADDER.size() - 1:
|
||||
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 jumped:
|
||||
Garage.add_fault(cid, "flat")
|
||||
lines.append("(red light. we saw it. and you've flat-spotted the fronts -- FIX IT)")
|
||||
if box_blown:
|
||||
Garage.add_fault(cid, "gearbox")
|
||||
lines.append("(the gearbox is on the strip somewhere behind you -- FIX IT IN THE GARAGE)")
|
||||
@ -1040,7 +1041,9 @@ func _end_pursuit(escaped: bool) -> void:
|
||||
else:
|
||||
busts += 1
|
||||
Sfx.shot(car, "sting_lose", -4.0)
|
||||
_msg("BUSTED", 52)
|
||||
# impound treatment: the divvy van jump-starts it wrong, out of spite
|
||||
Garage.add_fault(Garage.car_id_of(car_path), "battery")
|
||||
_msg("BUSTED\n(and now the battery's dead. coincidence?)", 48)
|
||||
|
||||
func _pursuit_tick(delta: float) -> void:
|
||||
heat_cd = maxf(heat_cd - delta, 0.0)
|
||||
@ -1241,6 +1244,11 @@ func _player_wreck() -> void:
|
||||
get_tree().create_timer(1.0, true, false, true).timeout.connect(
|
||||
func() -> void: Engine.time_scale = 1.0)
|
||||
_msg("WRECKED", 48)
|
||||
# wrecks wear the car: every 4th rattles the sump loose (not in junctions,
|
||||
# where wrecking IS the game and shouldn't bill you for playing it)
|
||||
if mode != "junction" and Garage.bump_wreck(Garage.car_id_of(car_path)):
|
||||
get_tree().create_timer(2.0, true, false, true).timeout.connect(
|
||||
func() -> void: _msg("SOMETHING'S RATTLED LOOSE -- OIL LIGHT'S ON", 32))
|
||||
|
||||
func _rival_wrecked(r: Rival) -> void:
|
||||
if car.wrecked or car.global_position.distance_to(r.global_position) > 8.0:
|
||||
|
||||
@ -194,13 +194,23 @@ static func buy_tune(car_id: String, tune: String) -> bool:
|
||||
## "tool" is the shed tool that makes the DIY fix civilised; without it the
|
||||
## minigame is longer and the commentary crueller.
|
||||
|
||||
## scene: which minigame fixes it -- "repair" (2D panel) or "wrench" (3D shed)
|
||||
const FAULTS := {
|
||||
"gearbox": {"name": "GRENADED GEARBOX", "light": "DRIVETRAIN", "shop": 900,
|
||||
"tool": "hoist",
|
||||
"tool": "hoist", "scene": "repair",
|
||||
"desc": "Third gear is a maraca now. Leaks on everything it loves."},
|
||||
"gasket": {"name": "BLOWN HEAD GASKET", "light": "ENGINE TEMP", "shop": 700,
|
||||
"tool": "sockets",
|
||||
"tool": "sockets", "scene": "repair",
|
||||
"desc": "Mayonnaise under the oil cap. Not the good kind."},
|
||||
"flat": {"name": "FLAT-SPOTTED TYRES", "light": "TYRE PRESSURE", "shop": 250,
|
||||
"tool": "sockets", "scene": "wrench",
|
||||
"desc": "That red-light launch took the fronts with it. Grip is a memory."},
|
||||
"oil": {"name": "OIL LIGHT'S ON", "light": "OIL PRESSURE", "shop": 180,
|
||||
"tool": "ramps", "scene": "wrench",
|
||||
"desc": "Service due. The sump remembers every wreck you've walked away from."},
|
||||
"battery": {"name": "DEAD BATTERY", "light": "BATTERY", "shop": 220,
|
||||
"tool": "reader", "scene": "wrench",
|
||||
"desc": "The divvy van jump-started it wrong out of spite."},
|
||||
}
|
||||
|
||||
static func faults(car_id: String) -> Array:
|
||||
@ -211,6 +221,11 @@ static func add_fault(car_id: String, fault: String) -> void:
|
||||
_load()
|
||||
if not FAULTS.has(fault):
|
||||
return
|
||||
# the fault recurring takes its quality buff with it: your last good fix
|
||||
# is exactly as durable as your driving
|
||||
var qd: Dictionary = _data.get("quality", {})
|
||||
if qd.has(car_id):
|
||||
qd[car_id].erase(fault)
|
||||
var f: Dictionary = _data.get("faults", {})
|
||||
var lst: Array = f.get(car_id, [])
|
||||
if fault in lst:
|
||||
@ -220,6 +235,33 @@ static func add_fault(car_id: String, fault: String) -> void:
|
||||
_data["faults"] = f
|
||||
_flush()
|
||||
|
||||
## ---- workmanship: 0-100 per fixed system, 85+ is a buff --------------------
|
||||
|
||||
static func quality(car_id: String, sys: String) -> float:
|
||||
_load()
|
||||
return float(_data.get("quality", {}).get(car_id, {}).get(sys, 0.0))
|
||||
|
||||
static func set_quality(car_id: String, sys: String, score: float) -> void:
|
||||
_load()
|
||||
var q: Dictionary = _data.get("quality", {})
|
||||
var c: Dictionary = q.get(car_id, {})
|
||||
c[sys] = score
|
||||
q[car_id] = c
|
||||
_data["quality"] = q
|
||||
_flush()
|
||||
|
||||
static func bump_wreck(car_id: String) -> bool:
|
||||
## Wrecks wear the sump loose: every 4th one turns the oil light on.
|
||||
_load()
|
||||
var w: Dictionary = _data.get("wrecks", {})
|
||||
w[car_id] = int(w.get(car_id, 0)) + 1
|
||||
_data["wrecks"] = w
|
||||
_flush()
|
||||
if int(w[car_id]) % 4 == 0 and not "oil" in faults(car_id):
|
||||
add_fault(car_id, "oil")
|
||||
return true
|
||||
return false
|
||||
|
||||
static func clear_fault(car_id: String, fault: String) -> void:
|
||||
_load()
|
||||
var f: Dictionary = _data.get("faults", {})
|
||||
@ -284,7 +326,9 @@ static func apply(car: Node3D, stats_path: String, is_player: bool) -> void:
|
||||
var ups := owned(cid)
|
||||
var tns := tunes(cid)
|
||||
var flt := faults(cid)
|
||||
if ups.is_empty() and tns.is_empty() and flt.is_empty():
|
||||
_load()
|
||||
var qual: Dictionary = _data.get("quality", {}).get(cid, {})
|
||||
if ups.is_empty() and tns.is_empty() and flt.is_empty() and qual.is_empty():
|
||||
return
|
||||
var st = car.get("stats")
|
||||
if st != null:
|
||||
@ -300,6 +344,20 @@ static func apply(car: Node3D, stats_path: String, is_player: bool) -> void:
|
||||
pm *= 0.85
|
||||
if car.has_method("set_steam"):
|
||||
car.call("set_steam", true)
|
||||
if "flat" in flt:
|
||||
ga -= 0.35
|
||||
if "oil" in flt:
|
||||
pm *= 0.93
|
||||
if "battery" in flt:
|
||||
ba -= 0.3
|
||||
# workmanship buffs: an 85+ fix drives BETTER than stock, until you
|
||||
# break that system again (add_fault clears its quality entry)
|
||||
if float(qual.get("flat", 0.0)) >= 85.0:
|
||||
ga += 0.2
|
||||
if float(qual.get("oil", 0.0)) >= 85.0:
|
||||
pm *= 1.03
|
||||
if float(qual.get("battery", 0.0)) >= 85.0:
|
||||
ba += 0.15
|
||||
for u in ups:
|
||||
match u:
|
||||
"stripes": pm += 0.06
|
||||
@ -323,8 +381,8 @@ static func apply(car: Node3D, stats_path: String, is_player: bool) -> void:
|
||||
st.engine_power *= pm
|
||||
st.grip += ga
|
||||
car.set("stats", st)
|
||||
if ba > 0.0 and car.get("boost_max") != null:
|
||||
car.set("boost_max", float(car.get("boost_max")) + ba)
|
||||
if ba != 0.0 and car.get("boost_max") != null:
|
||||
car.set("boost_max", maxf(float(car.get("boost_max")) + ba, 0.4))
|
||||
_bolt_visuals(car, ups)
|
||||
|
||||
static func _mat(rgba: Color, rough := 0.5, metal := 0.0, emit := Color.BLACK) -> StandardMaterial3D:
|
||||
|
||||
@ -177,9 +177,11 @@ 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 the shed minigame; it clears the fault on completion
|
||||
# DIY: off to whichever minigame owns this fault (repair = 2D panel,
|
||||
# wrench = 3D shed); it clears the fault on completion
|
||||
Garage.ui_fault = e["id"]
|
||||
get_tree().change_scene_to_file("res://src/repair.tscn")
|
||||
get_tree().change_scene_to_file("res://src/%s.tscn"
|
||||
% String(Garage.FAULTS[e["id"]].get("scene", "repair")))
|
||||
return
|
||||
if ok:
|
||||
Sfx.shot_2d("clunk")
|
||||
|
||||
568
src/wrench.gd
Normal file
568
src/wrench.gd
Normal file
@ -0,0 +1,568 @@
|
||||
extends Node3D
|
||||
## The 3D wrenching minigame, Phase 1: flat tyre, oil change, dead battery.
|
||||
## Your ACTUAL car in your ACTUAL shed, fixed by hand with five verbs:
|
||||
## GRAB -- click a loose part/tool, it follows the cursor, drop at the target
|
||||
## TWIST -- hold near the bolt and circle the mouse; torque jobs have a seat
|
||||
## window -- release inside it for a clean torque, blow past it and
|
||||
## the thread STRIPS (quality hit + an extraction step appears)
|
||||
## PUMP -- click the anchor repeatedly (jack, pump)
|
||||
## YANK -- press the anchor and flick the mouse hard
|
||||
## STEADY- keep the cursor inside a wandering ring (lining up studs, pouring)
|
||||
## Every job scores 0-100. Finish at 85+ and the fix is a BUFF (Garage.apply)
|
||||
## until that fault recurs. Which car/fault come via Garage.ui_stats_path /
|
||||
## Garage.ui_fault. All input routes through the API methods (twist, pump,
|
||||
## 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
|
||||
|
||||
var car_id := ""
|
||||
var fault := ""
|
||||
var stats: CarStats
|
||||
var steps: Array = []
|
||||
var step_index := 0
|
||||
var quality := 100.0
|
||||
var done := false
|
||||
var carried := "" # GRAB: name of the prop on the cursor
|
||||
var twist_prog := 0.0
|
||||
var _stripped := false # current twist step already stripped
|
||||
var pump_count := 0
|
||||
var steady_need := 0.0
|
||||
var wait_left := 0.0
|
||||
var clamps_done: Array = []
|
||||
var props := {} # name -> Node3D (spare wheel, battery, tray...)
|
||||
var model: Node3D
|
||||
var cam: Camera3D
|
||||
var step_label: Label
|
||||
var note_label: Label
|
||||
var qual_label: Label
|
||||
var ring: Control # screen-space anchor/steady indicator
|
||||
var _wheel_mesh: MeshInstance3D = null
|
||||
var _bonnet: Node3D = null
|
||||
var _oil_fx: GPUParticles3D = null
|
||||
|
||||
func _ready() -> void:
|
||||
car_id = Garage.car_id_of(Garage.ui_stats_path)
|
||||
fault = Garage.ui_fault
|
||||
stats = load(Garage.ui_stats_path)
|
||||
|
||||
add_child((load("res://levels/the_shed/level.tscn") as PackedScene).instantiate())
|
||||
_environment()
|
||||
# the patient: model only, no physics -- it isn't going anywhere on stands
|
||||
if stats.model_path != "" and ResourceLoader.exists(stats.model_path):
|
||||
model = (load(stats.model_path) as PackedScene).instantiate()
|
||||
else:
|
||||
model = Node3D.new()
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(stats.width, 1.0, stats.length)
|
||||
mi.mesh = bm
|
||||
model.add_child(mi)
|
||||
add_child(model)
|
||||
model.position = CAR_AT + Vector3.UP * 0.72
|
||||
for w in model.find_children("wheel_*", "MeshInstance3D", true, false):
|
||||
var mi := w as MeshInstance3D
|
||||
if mi.position.x < 0.0 and mi.position.z < 0.0:
|
||||
_wheel_mesh = mi # front-left is the patient's patient
|
||||
var b := model.find_children("det_bonnet*", "Node3D", true, false)
|
||||
_bonnet = b[0] if not b.is_empty() else null
|
||||
|
||||
cam = Camera3D.new()
|
||||
cam.fov = 60.0
|
||||
add_child(cam)
|
||||
_hud()
|
||||
steps = _job()
|
||||
_show_step()
|
||||
|
||||
func _environment() -> void:
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-55, 40, 0)
|
||||
sun.light_energy = 1.1
|
||||
sun.shadow_enabled = true
|
||||
add_child(sun)
|
||||
var e := Environment.new()
|
||||
e.background_mode = Environment.BG_SKY
|
||||
e.sky = Sky.new()
|
||||
var sm := ProceduralSkyMaterial.new()
|
||||
sm.sky_horizon_color = Color(0.8, 0.85, 0.9)
|
||||
e.sky.sky_material = sm
|
||||
e.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
|
||||
e.ambient_light_energy = 0.6
|
||||
e.tonemap_mode = Environment.TONE_MAPPER_ACES
|
||||
var env := WorldEnvironment.new()
|
||||
env.environment = e
|
||||
add_child(env)
|
||||
|
||||
func _hud() -> void:
|
||||
var hud := CanvasLayer.new()
|
||||
add_child(hud)
|
||||
step_label = Label.new()
|
||||
step_label.anchor_left = 0.5
|
||||
step_label.anchor_right = 0.5
|
||||
step_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
step_label.anchor_top = 0.04
|
||||
step_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
step_label.add_theme_font_size_override("font_size", 34)
|
||||
hud.add_child(step_label)
|
||||
note_label = Label.new()
|
||||
note_label.anchor_left = 0.5
|
||||
note_label.anchor_right = 0.5
|
||||
note_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
note_label.anchor_top = 0.11
|
||||
note_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
note_label.add_theme_font_size_override("font_size", 19)
|
||||
note_label.add_theme_color_override("font_color", Color(0.7, 0.73, 0.78))
|
||||
hud.add_child(note_label)
|
||||
qual_label = Label.new()
|
||||
qual_label.position = Vector2(24, 24)
|
||||
qual_label.add_theme_font_size_override("font_size", 26)
|
||||
hud.add_child(qual_label)
|
||||
ring = RingDraw.new()
|
||||
ring.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
ring.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
hud.add_child(ring)
|
||||
|
||||
## ---- job sheets ------------------------------------------------------------
|
||||
|
||||
func _anchor_wheel() -> Vector3:
|
||||
return CAR_AT + Vector3(-stats.width * 0.5, 0.45, -(stats.length * 0.5 - 0.55))
|
||||
|
||||
func _anchor_engine() -> Vector3:
|
||||
return CAR_AT + Vector3(0, 1.1, -stats.length * 0.30)
|
||||
|
||||
func _anchor_sump() -> Vector3:
|
||||
return CAR_AT + Vector3(0.2, 0.28, -stats.length * 0.18)
|
||||
|
||||
func _job() -> Array:
|
||||
var have_sockets := Garage.has_tool("sockets")
|
||||
var t := 3.0 if have_sockets else 6.0
|
||||
match fault:
|
||||
"flat":
|
||||
return [
|
||||
{"verb": "pump", "clicks": 8, "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": "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": "pump", "clicks": 4, "cam": "wheel", "label": "LET IT DOWN",
|
||||
"note": "gently. gently."},
|
||||
]
|
||||
"oil":
|
||||
return [
|
||||
{"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",
|
||||
"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",
|
||||
"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",
|
||||
"note": "outside the ring = on the manifold"},
|
||||
]
|
||||
"battery":
|
||||
return [
|
||||
{"verb": "yank", "cam": "bonnet", "label": "BONNET UP", "note": "flick it open"},
|
||||
{"verb": "twist", "dir": -1.0, "turns": 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"},
|
||||
{"verb": "grab", "item": "new_batt", "at": "engine", "cam": "bonnet",
|
||||
"label": "CASHIES BATTERY IN",
|
||||
"note": "charged overnight" if Garage.has_tool("reader") else "\"probably holds charge\" -- the label"},
|
||||
{"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"},
|
||||
]
|
||||
return []
|
||||
|
||||
## ---- step lifecycle --------------------------------------------------------
|
||||
|
||||
func _show_step() -> void:
|
||||
twist_prog = 0.0
|
||||
_stripped = false
|
||||
pump_count = 0
|
||||
steady_need = 0.0
|
||||
clamps_done = []
|
||||
carried = ""
|
||||
if step_index >= steps.size():
|
||||
_finish()
|
||||
return
|
||||
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", ""))
|
||||
qual_label.text = "QUALITY %d%%" % int(quality)
|
||||
_camera(String(st.get("cam", "wheel")))
|
||||
match st["verb"]:
|
||||
"wait":
|
||||
wait_left = float(st["secs"])
|
||||
_oil_start()
|
||||
"steady":
|
||||
steady_need = float(st["secs"])
|
||||
"grab":
|
||||
_spawn_prop(String(st["item"]))
|
||||
"yank":
|
||||
if fault == "battery" and step_index == 0:
|
||||
pass # bonnet handled on success
|
||||
ring.anchor_world = _step_anchor()
|
||||
ring.cam = cam
|
||||
ring.mode = String(st["verb"])
|
||||
|
||||
func _step_anchor() -> Vector3:
|
||||
var st: Dictionary = steps[step_index]
|
||||
match String(st.get("cam", "wheel")):
|
||||
"under":
|
||||
return _anchor_sump()
|
||||
"bonnet":
|
||||
return _anchor_engine()
|
||||
return _anchor_wheel()
|
||||
|
||||
func _camera(preset: String) -> void:
|
||||
match preset:
|
||||
"under":
|
||||
cam.global_position = CAR_AT + Vector3(0.4, 0.5, -stats.length * 0.18 + 2.6)
|
||||
cam.look_at(_anchor_sump())
|
||||
"bonnet":
|
||||
cam.global_position = CAR_AT + Vector3(-1.6, 2.3, -stats.length * 0.30 + 2.4)
|
||||
cam.look_at(_anchor_engine())
|
||||
_:
|
||||
cam.global_position = CAR_AT + Vector3(-stats.width * 0.5 - 2.6, 1.4,
|
||||
-(stats.length * 0.5 - 0.55) + 2.2)
|
||||
cam.look_at(_anchor_wheel())
|
||||
|
||||
func _spawn_prop(item: String) -> void:
|
||||
if props.has(item):
|
||||
return
|
||||
var n := Node3D.new()
|
||||
var mi := MeshInstance3D.new()
|
||||
match item:
|
||||
"spare":
|
||||
var cm := CylinderMesh.new()
|
||||
cm.top_radius = 0.31
|
||||
cm.bottom_radius = 0.31
|
||||
cm.height = 0.22
|
||||
cm.material = _dark(Color(0.09, 0.09, 0.1))
|
||||
mi.mesh = cm
|
||||
mi.rotation_degrees = Vector3(0, 0, 90)
|
||||
n.position = CAR_AT + Vector3(-2.6, 0.31, 2.2)
|
||||
"tray":
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.5, 0.12, 0.5)
|
||||
bm.material = _dark(Color(0.15, 0.15, 0.17))
|
||||
mi.mesh = bm
|
||||
n.position = CAR_AT + Vector3(-2.0, 0.06, 0.5)
|
||||
"bottle":
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.18, 0.32, 0.14)
|
||||
bm.material = _dark(Color(0.75, 0.7, 0.2))
|
||||
mi.mesh = bm
|
||||
n.position = Vector3(11.5, 1.06, -19.8) # on the bench
|
||||
"old_batt", "new_batt":
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = Vector3(0.32, 0.22, 0.19)
|
||||
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)
|
||||
n.add_child(mi)
|
||||
n.name = item
|
||||
add_child(n)
|
||||
props[item] = n
|
||||
|
||||
func _dark(c: Color) -> StandardMaterial3D:
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = c
|
||||
m.roughness = 0.7
|
||||
return m
|
||||
|
||||
func _oil_start() -> void:
|
||||
if _oil_fx == null:
|
||||
_oil_fx = Fx.smoker(self, Vector3.ZERO)
|
||||
_oil_fx.position = _anchor_sump()
|
||||
_oil_fx.amount = 12
|
||||
_oil_fx.emitting = true
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if done:
|
||||
return
|
||||
if step_index < steps.size() and steps[step_index]["verb"] == "wait":
|
||||
wait_left -= delta
|
||||
if wait_left <= 0.0:
|
||||
if _oil_fx:
|
||||
_oil_fx.emitting = false
|
||||
_advance()
|
||||
|
||||
## ---- the verbs (test API) --------------------------------------------------
|
||||
|
||||
func twist(rad: float) -> void:
|
||||
if done or steps[step_index]["verb"] != "twist":
|
||||
return
|
||||
var st: Dictionary = steps[step_index]
|
||||
var toward: float = rad * float(st["dir"])
|
||||
if toward <= 0.0:
|
||||
return
|
||||
twist_prog += toward
|
||||
var need: float = float(st["turns"]) * TAU
|
||||
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:
|
||||
_stripped = true
|
||||
quality -= 10.0
|
||||
Sfx.shot_2d("bog", -4.0)
|
||||
note_label.text = "STRIPPED IT. that's going in the jar."
|
||||
steps.insert(step_index + 1, {"verb": "twist", "dir": -1.0, "turns": 2.0,
|
||||
"window": false, "cam": st.get("cam", "wheel"),
|
||||
"label": "EXTRACT THE STRIPPED BOLT", "note": "(you did this)"})
|
||||
_advance()
|
||||
|
||||
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:
|
||||
Sfx.shot_2d("clunk", -4.0)
|
||||
_advance()
|
||||
elif twist_prog > 0.5:
|
||||
quality -= 1.0 # fumbled attempt, re-grip
|
||||
twist_prog = maxf(twist_prog - 1.0, 0.0)
|
||||
|
||||
func pump() -> void:
|
||||
if done or steps[step_index]["verb"] != "pump":
|
||||
return
|
||||
pump_count += 1
|
||||
Sfx.shot_2d("clunk", -12.0)
|
||||
if pump_count >= int(steps[step_index]["clicks"]):
|
||||
_advance()
|
||||
|
||||
func yank_ok() -> void:
|
||||
if done or steps[step_index]["verb"] != "yank":
|
||||
return
|
||||
Sfx.shot_2d("clunk", -6.0)
|
||||
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"
|
||||
var tw := create_tween()
|
||||
tw.tween_property(_bonnet, "rotation:x", -0.9 if opening else 0.0, 0.4)
|
||||
_advance()
|
||||
|
||||
func grab(item: String) -> void:
|
||||
if done or steps[step_index]["verb"] != "grab":
|
||||
return
|
||||
if steps[step_index]["item"] == item:
|
||||
carried = item
|
||||
|
||||
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()
|
||||
if world.distance_to(target) < 1.2:
|
||||
var p: Node3D = props.get(carried)
|
||||
if p:
|
||||
p.position = target
|
||||
if carried == "spare":
|
||||
p.get_child(0).rotation_degrees = Vector3(0, 0, 90)
|
||||
Sfx.shot_2d("clunk", -6.0)
|
||||
carried = ""
|
||||
_advance()
|
||||
|
||||
func steady_tick(inside: bool, delta: float) -> void:
|
||||
if done or steps[step_index]["verb"] != "steady":
|
||||
return
|
||||
if inside:
|
||||
steady_need -= delta
|
||||
if steady_need <= 0.0:
|
||||
Sfx.shot_2d("clunk", -6.0)
|
||||
if fault == "flat" and props.has("spare") and _wheel_mesh:
|
||||
(props["spare"] as Node3D).visible = false
|
||||
_wheel_mesh.visible = true # the spare becomes the wheel
|
||||
_advance()
|
||||
else:
|
||||
quality -= 2.0 * delta * 10.0 * 0.1 # excursions cost, gently
|
||||
qual_label.text = "QUALITY %d%%" % int(quality)
|
||||
|
||||
func clamp_on(colour: String) -> void:
|
||||
if done or steps[step_index]["verb"] != "clamps":
|
||||
return
|
||||
if colour in clamps_done:
|
||||
return
|
||||
if clamps_done.is_empty() and colour != "red":
|
||||
quality -= 10.0
|
||||
Fx.sparks(self, 30)
|
||||
Sfx.shot_2d("glass", -6.0)
|
||||
note_label.text = "WRONG ORDER. smell that? that's the smell of learning."
|
||||
clamps_done.append(colour)
|
||||
Sfx.shot_2d("clunk", -8.0)
|
||||
if clamps_done.size() >= 2:
|
||||
_advance()
|
||||
|
||||
func skip_wait() -> void:
|
||||
if not done and step_index < steps.size() and steps[step_index]["verb"] == "wait":
|
||||
wait_left = 0.0
|
||||
|
||||
func _advance() -> void:
|
||||
step_index += 1
|
||||
_show_step()
|
||||
|
||||
func _finish() -> void:
|
||||
done = true
|
||||
quality = clampf(quality, 0.0, 100.0)
|
||||
Garage.clear_fault(car_id, fault)
|
||||
Garage.set_quality(car_id, fault, quality)
|
||||
step_label.text = "DONE. QUALITY %d%%" % int(quality)
|
||||
note_label.text = ("that's a PROPER fix -- it'll drive better than stock" if quality >= 85.0
|
||||
else "it'll hold. probably. no buff at that standard, mate")
|
||||
Sfx.shot_2d("sting_win" if quality >= 85.0 else "clunk", -4.0)
|
||||
var b := Button.new()
|
||||
b.text = "BACK TO THE GARAGE"
|
||||
b.add_theme_font_size_override("font_size", 26)
|
||||
b.anchor_left = 0.5
|
||||
b.anchor_right = 0.5
|
||||
b.anchor_top = 0.8
|
||||
b.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
b.pressed.connect(func() -> void:
|
||||
get_tree().change_scene_to_file("res://src/garage.tscn"))
|
||||
var hud := CanvasLayer.new()
|
||||
add_child(hud)
|
||||
hud.add_child(b)
|
||||
|
||||
## ---- mouse plumbing --------------------------------------------------------
|
||||
|
||||
var _pressing := false
|
||||
var _prev_angle := 0.0
|
||||
var _press_pos := Vector2.ZERO
|
||||
var _press_t := 0.0
|
||||
|
||||
func _anchor_screen() -> Vector2:
|
||||
return cam.unproject_position(_step_anchor())
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if done or step_index >= steps.size():
|
||||
return
|
||||
var st: Dictionary = steps[step_index]
|
||||
var verb := String(st["verb"])
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.pressed:
|
||||
_pressing = true
|
||||
_press_pos = event.position
|
||||
_press_t = Time.get_ticks_msec() / 1000.0
|
||||
var d: float = event.position.distance_to(_anchor_screen())
|
||||
match verb:
|
||||
"twist":
|
||||
_prev_angle = (event.position - _anchor_screen()).angle()
|
||||
"pump":
|
||||
if d < 160.0:
|
||||
pump()
|
||||
"grab":
|
||||
if carried == "":
|
||||
# grab the prop if clicked near it, else it's a miss
|
||||
var p: Node3D = props.get(String(st["item"]))
|
||||
if p and event.position.distance_to(cam.unproject_position(p.global_position)) < 120.0:
|
||||
grab(String(st["item"]))
|
||||
"clamps":
|
||||
# left half of the anchor = red clamp, right half = black
|
||||
clamp_on("red" if event.position.x < _anchor_screen().x else "black")
|
||||
else:
|
||||
_pressing = false
|
||||
match verb:
|
||||
"twist":
|
||||
twist_release()
|
||||
"yank":
|
||||
var dt := Time.get_ticks_msec() / 1000.0 - _press_t
|
||||
if dt < 0.35 and event.position.distance_to(_press_pos) > 120.0:
|
||||
yank_ok()
|
||||
"grab":
|
||||
if carried != "":
|
||||
# drop: project the cursor onto the anchor's height plane
|
||||
var from := cam.project_ray_origin(event.position)
|
||||
var dir := cam.project_ray_normal(event.position)
|
||||
var plane := Plane(Vector3.UP, _step_anchor().y)
|
||||
var hit = plane.intersects_ray(from, dir)
|
||||
if hit != null:
|
||||
drop_at(hit)
|
||||
elif event is InputEventMouseMotion:
|
||||
if _pressing and verb == "twist":
|
||||
var a: float = (event.position - _anchor_screen()).angle()
|
||||
twist(wrapf(a - _prev_angle, -PI, PI))
|
||||
_prev_angle = a
|
||||
elif carried != "":
|
||||
var p: Node3D = props.get(carried)
|
||||
if p:
|
||||
var from := cam.project_ray_origin(event.position)
|
||||
var dir := cam.project_ray_normal(event.position)
|
||||
var plane := Plane(Vector3.UP, _step_anchor().y)
|
||||
var hit = plane.intersects_ray(from, dir)
|
||||
if hit != null:
|
||||
p.global_position = hit
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if done or step_index >= steps.size():
|
||||
return
|
||||
if steps[step_index]["verb"] == "steady":
|
||||
var inside: bool = get_viewport().get_mouse_position().distance_to(
|
||||
ring.steady_centre()) < 46.0
|
||||
steady_tick(inside, delta)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
get_tree().change_scene_to_file("res://src/garage.tscn")
|
||||
|
||||
|
||||
class RingDraw extends Control:
|
||||
## Screen-space indicator: anchor ring, twist progress arc, wandering
|
||||
## steady target. Pure decoration -- the verbs live on the parent.
|
||||
var anchor_world := Vector3.ZERO
|
||||
var cam: Camera3D = null
|
||||
var mode := ""
|
||||
var _t := 0.0
|
||||
|
||||
func steady_centre() -> Vector2:
|
||||
var base := cam.unproject_position(anchor_world) if cam else size * 0.5
|
||||
return base + Vector2(sin(_t * 1.3), cos(_t * 0.9)) * 34.0
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_t += delta
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
if cam == null:
|
||||
return
|
||||
var p := cam.unproject_position(anchor_world)
|
||||
var parent := get_parent().get_parent() as Node3D # CanvasLayer -> wrench
|
||||
match mode:
|
||||
"twist":
|
||||
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 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)
|
||||
"steady":
|
||||
draw_arc(steady_centre(), 46.0, 0, TAU, 40, Color(0.3, 0.9, 0.4, 0.85), 3.0)
|
||||
"pump", "yank", "grab", "clamps":
|
||||
draw_arc(p, 40.0 + sin(_t * 5.0) * 5.0, 0, TAU, 40, Color(1, 0.82, 0.35, 0.7), 3.0)
|
||||
1
src/wrench.gd.uid
Normal file
1
src/wrench.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dfyx45imad8y6
|
||||
6
src/wrench.tscn
Normal file
6
src/wrench.tscn
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://src/wrench.gd" id="1"]
|
||||
|
||||
[node name="Wrench" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
@ -124,6 +124,37 @@ func _run() -> void:
|
||||
_check(gg.garage_requested, "rolling into the bay never opened the garage")
|
||||
gg.free()
|
||||
Game.level_path = levels.get("woolies_carpark", levels.values()[0])
|
||||
# wrench phase 1: the 3D shed jobs. A clean flat-tyre run scores 90+ and
|
||||
# buffs the car; a sabotaged torque window strips a thread on the oil job.
|
||||
print(" .. wrench")
|
||||
Garage.add_fault("datto_120y", "flat")
|
||||
Garage.ui_stats_path = "res://cars/datto_120y/stats.tres"
|
||||
Garage.ui_fault = "flat"
|
||||
var wr = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
||||
root.add_child(wr)
|
||||
await process_frame
|
||||
_check(wr.steps.size() == 7, "flat-tyre job sheet wrong (%d steps)" % wr.steps.size())
|
||||
await _drive_wrench(wr, false)
|
||||
_check(wr.done, "flat-tyre job never finished (step %d)" % wr.step_index)
|
||||
_check(wr.quality >= 90.0, "clean flat run scored %d" % int(wr.quality))
|
||||
_check(not Garage.faults("datto_120y").has("flat"), "flat fault not cleared")
|
||||
_check(Garage.quality("datto_120y", "flat") >= 85.0, "workmanship not recorded")
|
||||
wr.free()
|
||||
gg = await _boot("cruise") # 85+ fix = grip buff on the next boot
|
||||
var dstats: CarStats = load("res://cars/datto_120y/stats.tres")
|
||||
_check(gg.car.stats.grip > dstats.grip + 0.7, "85+ fix paid no grip buff (%.2f vs %.2f)"
|
||||
% [gg.car.stats.grip, dstats.grip])
|
||||
gg.free()
|
||||
Garage.add_fault("datto_120y", "oil")
|
||||
Garage.ui_fault = "oil"
|
||||
wr = (load("res://src/wrench.tscn") as PackedScene).instantiate()
|
||||
root.add_child(wr)
|
||||
await process_frame
|
||||
await _drive_wrench(wr, true)
|
||||
_check(wr.done, "oil job never finished (step %d)" % wr.step_index)
|
||||
_check(wr.quality <= 90.0, "stripped thread cost nothing (%d)" % int(wr.quality))
|
||||
_check(wr.steps.size() == 8, "extraction step never appeared (%d steps)" % wr.steps.size())
|
||||
wr.free()
|
||||
Garage._reset_for_test() # later stages must run stock
|
||||
|
||||
# cruise: throttle 3s, car must move
|
||||
@ -389,6 +420,45 @@ func _run() -> void:
|
||||
[cars.size(), levels.size(), dist, int(crash_score), int(jct_score), Game.DERBY_CARS])
|
||||
quit(0)
|
||||
|
||||
func _drive_wrench(wr, sabotage: bool) -> void:
|
||||
## Plays any wrench job through its API. sabotage blows the first torque
|
||||
## window wide open to prove that threads strip.
|
||||
var guard := 0
|
||||
var sabotaged := false
|
||||
while not wr.done and guard < 30:
|
||||
guard += 1
|
||||
var st: Dictionary = wr.steps[wr.step_index]
|
||||
match String(st["verb"]):
|
||||
"pump":
|
||||
for i in int(st["clicks"]):
|
||||
wr.pump()
|
||||
"twist":
|
||||
if bool(st.get("window", false)):
|
||||
if sabotage and not sabotaged:
|
||||
sabotaged = true
|
||||
wr.twist(float(st["dir"]) * (float(st["turns"]) * TAU + 2.0))
|
||||
else:
|
||||
wr.twist(float(st["dir"]) * (float(st["turns"]) * TAU + 0.3))
|
||||
wr.twist_release()
|
||||
else:
|
||||
wr.twist(float(st["dir"]) * (float(st["turns"]) * TAU + 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())
|
||||
"steady":
|
||||
while not wr.done and wr.steps[wr.step_index]["verb"] == "steady":
|
||||
wr.steady_tick(true, 0.5)
|
||||
"clamps":
|
||||
wr.clamp_on("red")
|
||||
wr.clamp_on("black")
|
||||
"wait":
|
||||
wr.skip_wait()
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
|
||||
func _check(cond: bool, msg: String) -> void:
|
||||
## NOT assert(): a failed assert in a headless SceneTree script halts the
|
||||
## script but leaves the tree running, so the process spins forever instead
|
||||
|
||||
Loading…
Reference in New Issue
Block a user