class_name Game extends Node3D ## Loads the chosen level, spawns cars, runs the chosen mode: ## cruise -- free roam with traffic + roaming rivals (drive up close to one ## at speed to trigger an OUTRUN: first to gap the other wins) ## crash -- 60s damage-dollar rampage (Crash Mode v0) ## race -- 3 laps vs rivals on the level's RacePath, rubber-banded ## Takedowns refill the boost meter and grow it (up to 4x, per Burnout 3). static var car_path := "" static var level_path := "" static var mode := "cruise" static var time_of_day := "day" # day | dusk | night static var junction: Dictionary = {} const LAPS := 3 const CRASH_TIME := 60.0 const RIVAL_COUNT := 3 ## drag and derby are explicitly 0: both fall through to the RacePath traffic ## branch otherwise (every level GLB has a RacePath), littering a drag strip and ## a derby ring with loop traffic that belongs to neither event. const TRAFFIC_COUNT := {"cruise": 8, "crash": 12, "race": 4, "drag": 0, "derby": 0} const PARKED_MAX := 50 # PP_ markers filled with shuntable fleet cars ## Street furniture that topples. Marker prefix -> size, mass, colour. ## Deliberately physics, not scenery: clipping a lamp post at 120 and watching ## it go over is the whole point of the genre. const SMASHABLE := { "WB": {"size": Vector3(0.55, 1.0, 0.6), "mass": 35.0, "col": Color(0.14, 0.30, 0.13), "max": 45}, "BO": {"size": Vector3(0.22, 0.95, 0.22), "mass": 45.0, "col": Color(0.20, 0.21, 0.23), "max": 60}, "SL": {"size": Vector3(0.26, 7.4, 0.26), "mass": 190.0, "col": Color(0.24, 0.25, 0.27), "max": 60}, } const JCT_RUN_TIME := 30.0 # crash into something before this or the run ends const JCT_SETTLE := 9.0 # aftertouch/crashbreaker window after the crash const ROAM_RIVALS := 2 # challengers circling the RacePath in cruise ## Pursuit (Simpsons Hit & Run's meter, our code): wreck enough street furniture ## and the law turns up. Heat decays if you behave, so it's a choice not a timer. const HEAT_PER_SMASH := 0.14 const HEAT_DECAY := 0.055 # per second while not smashing anything const HEAT_COOLDOWN := 20.0 # grace after an escape before heat can build again const COPS_MIN := 2 const COPS_MAX := 4 const BUST_RANGE := 7.0 # a cop this close, this long, and you're done const BUST_TIME := 4.0 const ESCAPE_RANGE := 220.0 const ESCAPE_TIME := 6.0 ## Drag (NFSU2's, our code): the whole event is the gearbox. Revs climb, you ## shift in the window, and a blown shift costs you the race in a straight line ## where there's nothing else to get wrong. const DRAG_GEARS := 5 const SHIFT_LO := 0.74 # revs below this = shifted early, lose drive const REDLINE := 0.99 # hit it without shifting and the engine bogs const DRAG_REV_RATE := [0.0, 0.62, 0.50, 0.40, 0.32, 0.26] # per gear, revs/sec ## The Willowbank ladder: preset rivals, weakest first. power is their ## power_scale once they launch, reaction is how long after the green they sit ## there blinking, purse is what beating them pays. Beat the rung you're on to ## climb; the top dog repeats and keeps paying. const DRAG_LADDER := [ {"name": "NANNA'S SHOPPING RUN", "car": "morry", "power": 0.55, "reaction": 0.85, "purse": 250}, {"name": "P-PLATE PEPPER", "car": "excel_x3", "power": 0.78, "reaction": 0.55, "purse": 400}, {"name": "MULLET MERV", "car": "kingswood_hq", "power": 0.98, "reaction": 0.40, "purse": 600}, {"name": "TORQUE'N TEZZA", "car": "vl_terbo", "power": 1.12, "reaction": 0.30, "purse": 850}, {"name": "SELF-DESCRIBED BATHURST LEGEND", "car": "floored_xd", "power": 1.26, "reaction": 0.22, "purse": 1200}, {"name": "THE WILLOWBANK WIDOWMAKER", "car": "falcodore_gtho", "power": 1.45, "reaction": 0.15, "purse": 1600}, ] const TREE_AMBERS := [1.2, 1.7, 2.2] # seconds into staging, sportsman tree const TREE_GREEN := 2.7 const DRAG_FINISH_PAY := 50 # crossing the line at all const DRAG_PB_PAY := 150 # beating your own best const DERBY_CARS := 5 # opponents in the arena, last one running wins const DERBY_ARENA := 90.0 # leave the ring this long and you forfeit const DERBY_OUT := 8.0 const OUTRUN_GAP := 250.0 # escape distance that decides an outrun (NFSU2 rule) const OUTRUN_TIME := 90.0 const OUTRUN_START_D := 12.0 # pull alongside within this, at speed, to trigger ## Road Rage (Burnout 3's headline mode): 90 s, wreck as many rivals as you can. ## They recover and keep driving, so the supply never dries up. const RAGE_TIME := 90.0 const RAGE_RIVALS := 4 const RAGE_TARGETS := [4, 8, 12] # bronze / silver / gold takedowns const RAGE_LEASH := 220.0 # rivals further than this teleport back into play ## Oncoming: driving against the traffic flow on the road earns boost -- the ## canon Burnout risk/reward loop. const ONCOMING_EARN := 0.22 # boost per second while in the oncoming lane const ONCOMING_LANE_D := 7.0 # how close to the road line counts as "on the road" const ONCOMING_SPEED := 12.0 var jstate := "" # junction: "run" -> "settle" -> "done" var breaker_used := false var spinners: Array[Node3D] = [] # ferris wheels etc, turned in _process var heat := 0.0 var heat_cd := 0.0 var cops: Array[Cop] = [] var bust_t := 0.0 var escape_t := 0.0 var busts := 0 var escapes := 0 var drag_end: Vector3 var drag_end_y := 0.0 var drag_len := 0.0 var gear := 1 var revs := 0.0 var bog := 0.0 # seconds of lost drive from a blown shift var clean_shifts := 0 var blown_shifts := 0 var drag_t := 0.0 # elapsed time, counted from the GREEN var drag_state := "" # "stage" (at the tree) -> "run" (green light) var stage_t := 0.0 var jumped := false # rolled out of the beams before the green var drag_rung_now := 0 # index into DRAG_LADDER for this event var rival_reaction := 0.0 var rival_et := 0.0 # rival's ET once it crosses; 0 = hasn't yet var rival_wait := 0.0 # grace timer after the rival crosses first var _tree_bulbs := {} # "TreeGreen" etc -> MeshInstance3D (dragway only) 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_blown := false var gasket_blown := false var derby: Array[Cop] = [] # arena opponents; Cop AI already hunts a target var derby_centre: Vector3 var derby_out_t := 0.0 var derby_kills := 0 var outrun_rival: Rival = null var outrun_t := 0.0 var outrun_cd := 0.0 # cooldown so a finished outrun doesn't instantly re-arm var outruns_won := 0 var rage_downs := 0 var _road_off := 0.0 # player's tracked offset along the road line var _oncoming_run := 0.0 var _oncoming_toast := 0.0 var car: Car var cam: Camera3D var race_path: Path3D var rivals: Array[Rival] = [] var traffic: Array[TrafficCar] = [] var score := 0.0 var time_left := CRASH_TIME var over := false var laps := {} # racer -> lap count var prev_off := {} # racer -> last path offset var near_cd := {} # traffic -> near-miss cooldown var speed_label: Label var boost_bar: ProgressBar var heat_bar: ProgressBar var info_label: Label var msg_label: Label var msg_timer: SceneTreeTimer var _sticky := false # a result screen is up; transient messages must not clobber it var _shake := 0.0 # camera jolt on impacts, decays exponentially func _ready() -> void: var level: Node = (load(level_path) as PackedScene).instantiate() add_child(level) if mode != "junction": # junctions are deterministic puzzles: keep them clear _spawn_smashables(level) for w in level.find_children("wheel_of_brisbane*", "MeshInstance3D", true, false): # The GLB bakes world coords into the mesh, so the node origin is the # level's, not the wheel's. Hang it off a pivot at its own AABB centre. var mi := w as MeshInstance3D var pivot := Node3D.new() level.add_child(pivot) pivot.global_position = mi.global_transform * mi.get_aabb().get_center() var keep := mi.global_transform mi.reparent(pivot, false) mi.global_transform = keep spinners.append(pivot) for p in level.find_children("*", "Path3D", true, false): race_path = p if String(p.name).begins_with("Race"): break if race_path == null: # GLB levels can't carry Path3D: build one from a "RacePath" node of empties var rp := level.find_child("RacePath", true, false) if rp != null and rp.get_child_count() >= 3: var path := Path3D.new() var curve := Curve3D.new() for ch in rp.get_children(): curve.add_point((ch as Node3D).global_position) curve.add_point((rp.get_child(0) as Node3D).global_position) path.curve = curve level.add_child(path) race_path = path car = (load("res://src/car.tscn") as PackedScene).instantiate() car.stats = load(car_path) # place BEFORE add_child: teleporting a continuous_cd body after it enters # the physics world sweeps the jump as motion and hits everything en route if mode == "junction": var sp: Dictionary = junction["spawn"] var jf := Vector3(sp["dx"], 0, sp["dz"]).normalized() car.transform = Transform3D(Basis.looking_at(jf, Vector3.UP), _clear_spawn(Vector3(sp["x"], float(sp.get("y", 0.0)) + 0.7, sp["z"]), jf)) car.hold_wreck = true # The marker test lives in the branch condition, not inside it: as an `elif` # with the test inside, a level without a drag strip (the code-built carpark) # matched the branch, set no transform, and left the car at the world origin # half-sunk in the ground slab with no way to end the run. elif mode == "drag" and _drag_markers(level).size() == 2: var mk := _drag_markers(level) drag_end = Vector3(mk[1].global_position.x, 0.0, mk[1].global_position.z) drag_end_y = mk[1].global_position.y var from := Vector3(mk[0].global_position.x, mk[0].global_position.y + 0.7, mk[0].global_position.z) var dir := (drag_end - Vector3(from.x, 0, from.z)).normalized() drag_len = Vector3(from.x, 0, from.z).distance_to(drag_end) car.transform = Transform3D(Basis.looking_at(dir, Vector3.UP), _clear_spawn(from, dir) - dir.cross(Vector3.UP) * 2.0) elif mode == "race" and race_path: car.transform = _grid_xform(0.0) else: var spawn := level.find_child("Spawn", true, false) if spawn is Node3D: # trust only origin + yaw: DCC-exported empties arrive with mangled bases var t: Transform3D = (spawn as Node3D).global_transform var f := -t.basis.z f.y = 0.0 f = f.normalized() if f.length_squared() > 0.001 else Vector3.FORWARD car.transform = Transform3D(Basis.looking_at(f, Vector3.UP), Vector3(t.origin.x, t.origin.y + 0.7, t.origin.z)) else: car.position = Vector3.UP * 0.6 add_child(car) car.crashed.connect(_player_crash) car.wreck_started.connect(_player_wreck) if mode == "race" and race_path: laps[car] = 0 prev_off[car] = 0.0 var pool := Registry.cars().values() for i in RIVAL_COUNT: var r: Rival = (load("res://src/rival.tscn") as PackedScene).instantiate() r.stats = load(pool[randi() % pool.size()]) r.path = race_path var off := race_path.curve.get_baked_length() - 8.0 * (i + 1) r.transform = _grid_xform(off) r.progress = off add_child(r) laps[r] = 0 prev_off[r] = off r.wreck_started.connect(_rival_wrecked.bind(r)) rivals.append(r) elif mode == "derby": # Cop AI is exactly what a derby wants: hunts a target, reverses out of # wedges. Each one chases a different car so it's a brawl, not a mob. derby_centre = Vector3(car.global_position.x, 0.0, car.global_position.z) var pool := Registry.cars().values() for i in DERBY_CARS: var d: Cop = (load("res://src/cop.tscn") as PackedScene).instantiate() d.stats = load(pool[randi() % pool.size()]) d.power_boost = 1.0 d.siren = false # brawlers, not police # a wreck is an elimination: without this car.gd's 2.2 s _recover # timer stands them back up and the field never empties d.hold_wreck = true add_child(d) var a := TAU * i / DERBY_CARS var at := derby_centre + Vector3(cos(a), 0, sin(a)) * 26.0 + Vector3.UP * 0.75 d.global_transform = Transform3D( Basis.looking_at((derby_centre - at).normalized() * Vector3(1, 0, 1), Vector3.UP), at) d.wreck_started.connect(_derby_down.bind(d)) derby.append(d) for i in derby.size(): derby[i].target = car if i % 2 == 0 else derby[(i + 1) % derby.size()] elif mode == "drag" and drag_len > 0.0: # one opponent, one straight -- a Path3D of two points is all the AI needs # Run the rival down its OWN lane. Anchoring the strip on the player put # the AI's racing line on top of the player and they simply collided. var fwd := -car.global_basis.z fwd = Vector3(fwd.x, 0.0, fwd.z).normalized() var lane := fwd.cross(Vector3.UP).normalized() * 4.0 var strip := Path3D.new() var sc := Curve3D.new() sc.add_point(car.global_position + lane) sc.add_point(drag_end + Vector3.UP * (drag_end_y + 0.7) + lane) # a 2-point curve makes rival.gd's fmod() lookahead wrap to the start once # it nears the end, spinning the car around -- extend past the line instead sc.add_point(drag_end + Vector3.UP * (drag_end_y + 0.7) + lane + fwd * 120.0) strip.curve = sc add_child(strip) # 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] rival_reaction = float(rung["reaction"]) var cars_dict := Registry.cars() var r: Rival = (load("res://src/rival.tscn") as PackedScene).instantiate() r.stats = load(cars_dict[rung["car"]]) if cars_dict.has(rung["car"]) \ else load(cars_dict.values()[randi() % cars_dict.size()]) r.path = strip r.power_scale = 0.0 # held at the tree until green + reaction time add_child(r) r.global_transform = Transform3D(Basis.looking_at(fwd, Vector3.UP), car.global_position + lane) r.progress = 0.0 rivals.append(r) drag_state = "stage" _launch_pos = car.global_position # the Willowbank tree, if this level has one: bulbs are MeshInstance3Ds # with an emission material_override that _bulb() switches on and off for bn in ["TreeStage", "TreeAmber1", "TreeAmber2", "TreeAmber3", "TreeGreen", "TreeRed"]: var b := level.find_child(bn, true, false) if b is MeshInstance3D: _tree_bulbs[bn] = b elif mode == "rage" and race_path: time_left = RAGE_TIME var pool := Registry.cars().values() for i in RAGE_RIVALS: var r: Rival = (load("res://src/rival.tscn") as PackedScene).instantiate() r.stats = load(pool[randi() % pool.size()]) r.path = race_path var off := race_path.curve.get_baked_length() * (i + 1) / float(RAGE_RIVALS + 1) r.transform = _grid_xform(off) r.progress = off add_child(r) r.wreck_started.connect(_rival_wrecked.bind(r)) rivals.append(r) elif mode == "cruise" and race_path: # roaming challengers: they lap the circuit until you pick a fight var pool := Registry.cars().values() var L := race_path.curve.get_baked_length() for i in ROAM_RIVALS: var r: Rival = (load("res://src/rival.tscn") as PackedScene).instantiate() r.stats = load(pool[randi() % pool.size()]) r.path = race_path var off := L * (i + 1) / float(ROAM_RIVALS + 1) r.transform = _grid_xform(off) r.progress = off add_child(r) r.wreck_started.connect(_rival_wrecked.bind(r)) rivals.append(r) car.spawn_xform = car.global_transform if mode == "junction": jstate = "run" time_left = JCT_RUN_TIME var midx := 0 for stream in junction["streams"]: var path := Path3D.new() var curve := Curve3D.new() for p in stream["pts"]: # third component (when baked) is terrain height curve.add_point(Vector3(p[0], float(p[2]) if p.size() > 2 else 0.0, p[1])) path.curve = curve add_child(path) var L := curve.get_baked_length() var headway: float = stream["speed"] * stream["interval"] # At least 3 per stream, spaced to fit. int(L / headway) floors to 1 # on a short clipped stream, which is how Grey x Glenelg ended up # with 9 convoy cars against the usual 20-25 and gaps wide enough to # drive clean through -- it scored nothing in 12 of 12 tuning runs. var n: int = clampi(int(L / headway), 3, 14) var spacing := L / float(n) for i in n: var t := TrafficCar.new() t.path = path t.offset = fposmod(i * spacing, L) t.speed = stream["speed"] t.model_idx = midx midx += 1 add_child(t) t.smashed.connect(_traffic_smashed.bind(t)) traffic.append(t) elif race_path: var n: int = TRAFFIC_COUNT.get(mode, 4) for i in n: var t := TrafficCar.new() t.path = race_path t.offset = race_path.curve.get_baked_length() * (i + 0.5) / n t.speed = randf_range(9.0, 14.0) add_child(t) t.smashed.connect(_traffic_smashed.bind(t)) traffic.append(t) cam = Camera3D.new() cam.fov = 72.0 add_child(cam) cam.global_position = car.global_position + Vector3(0, 3, 8) _build_environment() var hud := CanvasLayer.new() add_child(hud) speed_label = Label.new() speed_label.position = Vector2(24, 24) speed_label.add_theme_font_size_override("font_size", 32) hud.add_child(speed_label) boost_bar = ProgressBar.new() boost_bar.position = Vector2(24, 72) boost_bar.size = Vector2(160, 18) boost_bar.show_percentage = false hud.add_child(boost_bar) heat_bar = ProgressBar.new() heat_bar.position = Vector2(24, 96) heat_bar.size = Vector2(160, 10) heat_bar.show_percentage = false heat_bar.max_value = 1.0 heat_bar.visible = false var fill := StyleBoxFlat.new() fill.bg_color = Color(0.85, 0.16, 0.12) heat_bar.add_theme_stylebox_override("fill", fill) hud.add_child(heat_bar) info_label = Label.new() info_label.anchor_left = 1.0 info_label.anchor_right = 1.0 info_label.offset_left = -420.0 info_label.offset_top = 24.0 info_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT info_label.add_theme_font_size_override("font_size", 30) hud.add_child(info_label) msg_label = Label.new() msg_label.anchor_left = 0.5 msg_label.anchor_right = 0.5 msg_label.anchor_top = 0.25 msg_label.grow_horizontal = Control.GROW_DIRECTION_BOTH msg_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER msg_label.add_theme_font_size_override("font_size", 48) hud.add_child(msg_label) func _drag_markers(level: Node) -> Array[Node3D]: var out: Array[Node3D] = [] var s := level.find_child("DragStart", true, false) as Node3D var e := level.find_child("DragEnd", true, false) as Node3D if s and e: out = [s, e] return out func _clear_spawn(pos: Vector3, fwd: Vector3) -> Vector3: ## The generator places launch points from street centrelines alone, so one ## can land inside a building or a mall canopy leg -- Albert x Adelaide was ## wedged at its start line, 1 m driven in 25 s. Back off along the approach ## until the car actually fits. var space := get_world_3d().direct_space_state var q := PhysicsShapeQueryParameters3D.new() var box := BoxShape3D.new() # must clear the ground slab: its top face is y=0 and the spawn sits at 0.7, # so a 1.4-tall box centred there touches the ground at every candidate and # the whole search silently fails box.size = Vector3(2.2, 0.9, 4.8) q.shape = box var lift := Vector3(0, 0.25, 0) var side := Vector3.UP.cross(fwd).normalized() # nearest-first: try the exact spot, then widening back/side offsets. A clear # box isn't enough -- Albert x Adelaide fitted fine but faced a wall 2.7 m # on, so the launch corridor has to be clear too. var tries: Array[Vector3] = [Vector3.ZERO] # nearest-first, and allow sliding forward as well as back: Albert x Adelaide's # generated launch sits inside a CBD building block, where nothing behind it # is clear either. A shortened run-up beats an unplayable event. for along in [0.0, -6.0, 6.0, -12.0, 12.0, -20.0, 20.0, -30.0, 30.0, 45.0, 60.0, 75.0, 90.0, 105.0]: for lat in [0.0, 4.0, -4.0, 8.0, -8.0, 13.0, -13.0]: tries.append(fwd * along + side * lat) for off in tries: var at := pos + off q.transform = Transform3D(Basis.IDENTITY, at + lift) if not space.intersect_shape(q, 1).is_empty(): continue var ray := PhysicsRayQueryParameters3D.create(at + lift, at + lift + fwd * 22.0) if space.intersect_ray(ray).is_empty(): if off.length() > 0.5: push_warning("junction spawn blocked; moved %.0f m to clear the run-up" % off.length()) return at push_warning("junction spawn has no clear launch corridor") return pos func _spawn_smashables(level: Node) -> void: ## Fill the level's PP_/WB_ markers with shuntable parked cars and wheelie ## bins. Real fleet models, asleep until rammed. var pool: Array[String] = [] var cars_dict := Registry.cars() for id in cars_dict: var st: CarStats = load(cars_dict[id]) if st.model_path != "" and ResourceLoader.exists(st.model_path): pool.append(st.model_path) pool.sort() var n := 0 for m in level.find_children("PP_*", "Node3D", true, false): if n >= PARKED_MAX: break var b := RigidBody3D.new() b.mass = 300.0 var cs := CollisionShape3D.new() var bs := BoxShape3D.new() bs.size = Vector3(1.8, 1.2, 4.3) cs.shape = bs b.add_child(cs) if not pool.is_empty(): b.add_child((load(pool[randi() % pool.size()]) as PackedScene).instantiate()) add_child(b) var t: Transform3D = (m as Node3D).global_transform b.global_transform = Transform3D( Basis(Vector3.UP, t.basis.get_euler().y), Vector3(t.origin.x, t.origin.y + 0.72, t.origin.z)) b.sleeping = true n += 1 for prefix in SMASHABLE: var spec: Dictionary = SMASHABLE[prefix] var size: Vector3 = spec["size"] var mesh := BoxMesh.new() mesh.size = size var mat := StandardMaterial3D.new() mat.albedo_color = spec["col"] mat.roughness = 0.8 mat.metallic = 0.3 mesh.material = mat n = 0 for m in level.find_children("%s_*" % prefix, "Node3D", true, false): if n >= int(spec["max"]): break var b := RigidBody3D.new() b.mass = spec["mass"] # low centre of mass so a clipped lamp post topples rather than # helicoptering off into the skybox b.center_of_mass_mode = RigidBody3D.CENTER_OF_MASS_MODE_CUSTOM b.center_of_mass = Vector3(0, -size.y * 0.3, 0) var cs := CollisionShape3D.new() var bs := BoxShape3D.new() bs.size = size cs.shape = bs b.add_child(cs) var mi := MeshInstance3D.new() mi.mesh = mesh b.add_child(mi) if prefix == "SL": # arm + lantern, visual only -- collision stays the plain pole box var arm := MeshInstance3D.new() var am := BoxMesh.new() am.size = Vector3(1.5, 0.12, 0.12) am.material = mat arm.mesh = am arm.position = Vector3(0.75, size.y * 0.5 - 0.25, 0) b.add_child(arm) var head := MeshInstance3D.new() var hm := BoxMesh.new() hm.size = Vector3(0.8, 0.14, 0.4) var hmat := StandardMaterial3D.new() hmat.albedo_color = Color(0.86, 0.84, 0.72) hmat.emission_enabled = true hmat.emission = Color(0.9, 0.85, 0.6) hmat.emission_energy_multiplier = 0.35 if time_of_day != "night" else 3.5 hm.material = hmat head.mesh = hm head.position = Vector3(1.45, size.y * 0.5 - 0.36, 0) b.add_child(head) if time_of_day == "night": # a real light under the lantern -- and because it's parented # to the physics body, toppling the lamp takes its pool of # light with it, which is deeply satisfying var ol := OmniLight3D.new() ol.light_color = Color(1.0, 0.87, 0.55) ol.light_energy = 1.6 ol.omni_range = 13.0 ol.omni_attenuation = 1.4 ol.position = head.position - Vector3(0, 0.5, 0) b.add_child(ol) add_child(b) var t: Transform3D = (m as Node3D).global_transform b.global_transform = Transform3D( Basis(Vector3.UP, t.basis.get_euler().y), Vector3(t.origin.x, t.origin.y + size.y * 0.5 + 0.02, t.origin.z)) b.sleeping = true n += 1 func _build_environment() -> void: ## Three Brisbanes: midday (hard high sun), dusk (low orange sun under a FLUX ## panorama) and night (moonlight, working street lamps, headlights). var sun := DirectionalLight3D.new() match time_of_day: "dusk": sun.rotation_degrees = Vector3(-11, 55, 0) sun.light_color = Color(1.0, 0.62, 0.32) sun.light_energy = 0.85 "night": sun.rotation_degrees = Vector3(-48, -20, 0) sun.light_color = Color(0.55, 0.65, 0.90) # moon sun.light_energy = 0.14 _: sun.rotation_degrees = Vector3(-62, 38, 0) sun.light_color = Color(1.0, 0.96, 0.88) sun.light_energy = 1.25 sun.shadow_enabled = true sun.directional_shadow_mode = DirectionalLight3D.SHADOW_PARALLEL_4_SPLITS sun.directional_shadow_max_distance = 220.0 sun.directional_shadow_split_1 = 0.05 sun.directional_shadow_split_2 = 0.15 sun.directional_shadow_split_3 = 0.45 sun.directional_shadow_blend_splits = true sun.shadow_bias = 0.035 sun.shadow_normal_bias = 1.4 add_child(sun) # a dim unshadowed fill from the opposite side so shadowed facades keep their # texture instead of crushing to black var fill := DirectionalLight3D.new() fill.rotation_degrees = Vector3(-28, -145, 0) fill.light_color = Color(0.86, 0.89, 0.96) fill.light_energy = 0.22 fill.shadow_enabled = false add_child(fill) # dusk/night ride FLUX panoramas; day keeps the procedural gradient var sky_mat: Material var pano := "res://assets/textures/sky_%s.jpg" % time_of_day if time_of_day != "day" and ResourceLoader.exists(pano): var pm := PanoramaSkyMaterial.new() pm.panorama = load(pano) pm.energy_multiplier = 1.3 if time_of_day == "dusk" else 0.9 sky_mat = pm else: var gm := ProceduralSkyMaterial.new() gm.sky_top_color = Color(0.27, 0.46, 0.74) gm.sky_horizon_color = Color(0.80, 0.86, 0.92) gm.sky_energy_multiplier = 1.0 gm.ground_bottom_color = Color(0.28, 0.28, 0.30) gm.ground_horizon_color = Color(0.66, 0.72, 0.78) gm.sun_angle_max = 4.0 gm.sun_curve = 0.08 sky_mat = gm var e := Environment.new() e.background_mode = Environment.BG_SKY e.sky = Sky.new() e.sky.sky_material = sky_mat e.ambient_light_source = Environment.AMBIENT_SOURCE_SKY # a street canyon is lit almost entirely by ambient, so pure sky ambient # painted the whole city blue -- blend it half-and-half with neutral e.ambient_light_sky_contribution = 0.5 match time_of_day: "dusk": e.ambient_light_color = Color(0.80, 0.62, 0.55) e.ambient_light_energy = 0.38 "night": e.ambient_light_color = Color(0.35, 0.40, 0.55) e.ambient_light_energy = 0.16 _: e.ambient_light_color = Color(0.88, 0.87, 0.84) e.ambient_light_energy = 0.55 e.reflected_light_source = Environment.REFLECTION_SOURCE_SKY e.tonemap_mode = Environment.TONE_MAPPER_ACES e.tonemap_exposure = 0.6 e.tonemap_white = 5.0 e.ssao_enabled = true # contact shade is what makes the massing read e.ssao_radius = 3.0 e.ssao_intensity = 1.6 e.ssao_power = 1.4 e.ssao_light_affect = 0.25 e.glow_enabled = true # sun off glass, and the Crashbreaker fireball e.glow_intensity = 0.5 e.glow_bloom = 0.08 e.glow_hdr_threshold = 1.3 e.fog_enabled = true # hides the level's hard edge, gives the CBD depth e.fog_mode = Environment.FOG_MODE_DEPTH e.fog_light_color = Color(0.74, 0.82, 0.92) e.fog_density = 0.0 e.fog_depth_begin = 190.0 e.fog_depth_end = 900.0 e.fog_depth_curve = 0.7 e.fog_sun_scatter = 0.15 if time_of_day == "night": e.glow_intensity = 0.8 # lamp heads and tail lights bloom e.fog_light_color = Color(0.10, 0.11, 0.16) elif time_of_day == "dusk": e.fog_light_color = Color(0.55, 0.38, 0.30) var env := WorldEnvironment.new() env.environment = e add_child(env) if time_of_day == "night": # headlights on the player: two warm spots raking the road ahead for sx in [-0.55, 0.55]: var hl := SpotLight3D.new() hl.light_color = Color(1.0, 0.94, 0.78) hl.light_energy = 4.0 hl.spot_range = 42.0 hl.spot_angle = 28.0 hl.spot_angle_attenuation = 1.6 hl.position = Vector3(sx, 0.15, -car.stats.length * 0.48) hl.rotation_degrees = Vector3(-4.0, 180.0, 0) car.add_child(hl) func _exit_tree() -> void: Engine.time_scale = 1.0 func _grid_xform(off: float) -> Transform3D: var L := race_path.curve.get_baked_length() var pos := race_path.to_global(race_path.curve.sample_baked(fposmod(off, L))) + Vector3.UP * 0.8 var ahead := race_path.to_global(race_path.curve.sample_baked(fposmod(off + 4.0, L))) + Vector3.UP * 0.8 var dir := (ahead - pos).normalized() return Transform3D(Basis.looking_at(dir if dir.length_squared() > 0.001 else Vector3.FORWARD, Vector3.UP), pos) func _physics_process(delta: float) -> void: # near misses: close pass on live traffic at speed for t in traffic: near_cd[t] = maxf(near_cd.get(t, 0.0) - delta, 0.0) if t.live and near_cd[t] == 0.0 and car.linear_velocity.length() > 15.0 \ and car.global_position.distance_to(t.global_position) < 4.5: near_cd[t] = 4.0 car.earn_boost(0.12) _msg("NEAR MISS", 32) if mode == "race" and race_path and not over: _race_tick() if mode == "cruise" and race_path: _outrun_tick(delta) if mode == "cruise": _pursuit_tick(delta) if mode == "drag" and not over: _drag_tick(delta) if mode == "rage" and not over: _rage_tick() if mode in ["cruise", "race", "rage"] and race_path and not car.wrecked: _oncoming_tick(delta) if mode == "derby" and not over: _derby_tick(delta) if mode == "junction" and jstate == "settle": if Input.is_action_just_pressed("boost") and not breaker_used: _crashbreaker() if car.wrecked: # aftertouch: nudge the tumbling wreck, camera-relative var right := cam.global_basis.x right.y = 0 var fwd := -cam.global_basis.z fwd.y = 0 var push := right.normalized() * Input.get_axis("steer_left", "steer_right") \ + fwd.normalized() * Input.get_axis("brake", "throttle") car.apply_central_force(push * car.mass * 9.0) func _derby_down(d: Cop) -> void: if over: return # no bounties after the result screen is up if not car.wrecked and car.global_position.distance_to(d.global_position) < 22.0: derby_kills += 1 car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) car.boost = car.boost_max Sfx.shot(car, "sting_win", -4.0) Garage.earn(100) _msg("WRECKED 'EM! x%d +$100" % derby_kills, 46) func _derby_tick(delta: float) -> void: var alive: Array[Cop] = [] for d in derby: if is_instance_valid(d) and not d.wrecked: alive.append(d) # retarget anyone hunting a car that's already out, or they circle a corpse for d in alive: if d.target == null or not is_instance_valid(d.target) \ or (d.target is Cop and (d.target as Cop).wrecked): d.target = car if alive.size() < 2 else alive[randi() % alive.size()] if d.target == d: d.target = car var flat := Vector3(car.global_position.x, 0.0, car.global_position.z) var from_centre := flat.distance_to(derby_centre) if from_centre > DERBY_ARENA: derby_out_t += delta else: derby_out_t = maxf(derby_out_t - delta, 0.0) info_label.text = "DERBY %d left %s" % [ alive.size(), "GET BACK IN %ds" % ceili(DERBY_OUT - derby_out_t) if derby_out_t > 0.5 else "%d wrecked" % derby_kills] if car.wrecked: over = true _msg("WRECKED\n%d of %d taken out\nR to retry, Esc for menu" % [derby_kills, DERBY_CARS], 44, true) elif derby_out_t >= DERBY_OUT: over = true _msg("RAN AWAY\nR to retry, Esc for menu", 44, true) elif alive.is_empty(): over = true Garage.earn(800) _msg("LAST ONE RUNNING\n%d wrecked +$800\nR to retry, Esc for menu" % derby_kills, 48, true) func _drag_tick(delta: float) -> void: if drag_len <= 0.0: info_label.text = "no drag strip in this level" return if drag_state == "stage": _stage_tick(delta) return drag_t += delta bog = maxf(bog - delta, 0.0) # the rival launches when its reaction time is up -- Nanna sits at the green # for most of a second; the Widowmaker leaves with you if not rivals.is_empty(): rivals[0].power_scale = 0.0 if drag_t < rival_reaction \ else float(DRAG_LADDER[drag_rung_now]["power"]) if gear <= DRAG_GEARS: revs = minf(revs + DRAG_REV_RATE[gear] * delta * (1.0 if bog == 0.0 else 0.25), 1.0) if Input.is_action_just_pressed("boost") and not box_blown: if revs >= SHIFT_LO and revs < REDLINE: clean_shifts += 1 Sfx.shot(car, "clunk", -2.0) _msg("CLEAN SHIFT", 34) else: blown_shifts += 1 box_strikes += 1 bog = 0.9 Sfx.shot(car, "bog", -2.0) if box_strikes >= 3: # third mistreated shift: the box keeps what's in it box_blown = true car.mech_pop() car.leaking = true _msg("GEARBOX GRENADED!\nSTUCK IN %d, LEAKING" % mini(gear + 1, DRAG_GEARS), 44) else: _msg("MISSED IT", 34) gear += 1 revs = 0.28 if gear <= DRAG_GEARS else 1.0 elif revs >= REDLINE: bog = maxf(bog, 0.35) # sitting on the limiter goes nowhere # coolant: riding the limiter cooks the engine; at 1.0 the head gasket goes 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) else: eng_temp = maxf(eng_temp - delta * 0.04, 0.0) if eng_temp >= 1.0 and not gasket_blown: gasket_blown = true car.set_steam(true) Sfx.shot(car, "bog", -2.0, 0.55) _msg("HEAD GASKET!\nCOOLANT EVERYWHERE", 44) # drive comes from the box, not the throttle: clean shifts pay, bogging hurts var tune := 1.0 + clean_shifts * 0.07 - blown_shifts * 0.05 # launch traction: warm tyres hook up, cold ones spin it away off the line var gone := Vector3(car.global_position.x, 0, car.global_position.z).distance_to( Vector3(_launch_pos.x, 0, _launch_pos.z)) var traction := (0.78 + 0.42 * tyre_temp) if gone < 150.0 else 1.0 # clamp the gear term: gear runs to DRAG_GEARS+1 as the "past top" sentinel, # and un-clamped that paid 6/5 -- more grunt than top gear for shifting into nothing car.power_scale = (0.35 if bog > 0.0 else tune) \ * (0.75 + 0.25 * mini(gear, DRAG_GEARS) / float(DRAG_GEARS)) \ * traction * (0.55 if box_blown else 1.0) * (0.72 if gasket_blown else 1.0) var flat := Vector3(car.global_position.x, 0.0, car.global_position.z) var left := flat.distance_to(drag_end) if rival_et == 0.0 and not rivals.is_empty(): var rf := Vector3(rivals[0].global_position.x, 0.0, rivals[0].global_position.z) if rf.distance_to(drag_end) < 12.0: rival_et = drag_t var pb := Garage.drag_pb(Garage.car_id_of(car_path)) var mech := "" if box_blown: mech += " BOX GRENADED" if gasket_blown: mech += " GASKET STEAMING" elif eng_temp > 0.6: mech += " ENG %d%%!" % int(eng_temp * 100) info_label.text = "VS %s%s\nGEAR %d %s %dm %.1fs%s" % [ DRAG_LADDER[drag_rung_now]["name"], (" | PB %.2f" % pb) if pb > 0.0 else "", mini(gear, DRAG_GEARS), ("BOGGED" if bog > 0.0 else ("SHIFT!" if revs >= SHIFT_LO else "%d%%" % int(revs * 100))), int(left), drag_t, mech] # the event ends when YOU cross: losing to the rival mustn't rob you of your # ET and PB. If the rival's crossed and you dawdle 10 s, it gets called. if rival_et > 0.0: rival_wait += delta if left < 12.0: _drag_finish(true) elif rival_wait > 10.0: _drag_finish(false) func _stage_tick(delta: float) -> void: ## At the tree. The RIVAL is held by the electronics; you're held by nothing ## but your own discipline, which is the whole point of a christmas tree. var was := stage_t stage_t += delta if not rivals.is_empty(): rivals[0].power_scale = 0.0 # the burnout box: DRIFT+GAS lights up the rears -- smoke pours, tyres warm, # and the handbrake holds you off the beams so it can't red-light you var burning := Input.is_action_pressed("drift") \ and Input.get_axis("brake", "throttle") > 0.2 and not car.wrecked car.set_burnout(burning) if burning: car.power_scale = 0.0 tyre_temp = minf(tyre_temp + delta * 0.30, 1.0) else: car.power_scale = 0.8 if was <= 0.0: _bulb("TreeStage", true) for i in TREE_AMBERS.size(): if was < float(TREE_AMBERS[i]) and stage_t >= float(TREE_AMBERS[i]): _bulb("TreeAmber%d" % (i + 1), true) Sfx.shot_2d("clunk", -10.0) if not jumped and car.global_position.distance_to(_launch_pos) > 2.0: jumped = true _bulb("TreeRed", true) Sfx.shot(car, "bog", -2.0) _msg("RED LIGHT", 44) var rung: Dictionary = DRAG_LADDER[drag_rung_now] var pb := Garage.drag_pb(Garage.car_id_of(car_path)) info_label.text = "VS %s%s\nTYRES %d%% (DRIFT+GAS to burn out)\nHOLD FOR THE GREEN..." % [ rung["name"], (" | PB %.2fs" % pb) if pb > 0.0 else "", int(tyre_temp * 100)] if stage_t >= TREE_GREEN: drag_state = "run" car.set_burnout(false) for i in 3: _bulb("TreeAmber%d" % (i + 1), false) _bulb("TreeGreen", true) Sfx.shot_2d("clunk", -2.0) if jumped: bog = 1.6 # you launched at a light that wasn't green _msg("GO! (BOGGED: RED LIGHT)", 44) else: _msg("GO!", 52) func _drag_finish(crossed: bool) -> void: over = true car.power_scale = 1.0 # _drag_tick stops on `over`, so reset it here var rung: Dictionary = DRAG_LADDER[drag_rung_now] var won := crossed and (rival_et == 0.0 or drag_t <= rival_et) var pay := DRAG_FINISH_PAY if crossed else 0 var lines: Array[String] = [] if crossed: lines.append("%s vs %s -- %.2fs" % ["WON" if won else "LOST", rung["name"], drag_t]) if Garage.record_drag_pb(Garage.car_id_of(car_path), drag_t): pay += DRAG_PB_PAY lines.append("NEW PERSONAL BEST +$%d" % DRAG_PB_PAY) else: lines.append("LOST vs %s (never crossed)" % rung["name"]) if won: pay += int(rung["purse"]) lines.append("PURSE +$%d" % int(rung["purse"])) if Garage.drag_rung() == drag_rung_now and drag_rung_now < DRAG_LADDER.size() - 1: Garage.drag_advance() 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.)") if box_blown: lines.append("(the gearbox is on the strip somewhere behind you)") if gasket_blown: lines.append("(and the radiator's boiled itself dry)") Garage.earn(pay) lines.append("+$%d bucks | %d clean / %d blown" % [pay, clean_shifts, blown_shifts]) lines.append("R to retry, Esc for menu") Sfx.shot_2d("sting_win" if won else "bog") _msg("\n".join(lines), 40, true) func _bulb(bn: String, on: bool) -> void: if not _tree_bulbs.has(bn): return # street drags have no tree hardware; the HUD carries the count var m := (_tree_bulbs[bn] as MeshInstance3D).material_override as StandardMaterial3D if m: m.emission_enabled = on func add_heat(amount: float) -> void: if mode != "cruise" or heat_cd > 0.0 or not cops.is_empty(): return heat = minf(heat + amount, 1.0) if heat >= 1.0: _start_pursuit() func _start_pursuit() -> void: var pool := Registry.cars().values() var n := COPS_MIN + (busts + escapes) / 2 # they escalate as the night goes on for i in mini(n, COPS_MAX): var c: Cop = (load("res://src/cop.tscn") as PackedScene).instantiate() c.stats = load(pool[randi() % pool.size()]) c.target = car c.power_boost = 1.12 c.hold_wreck = true # wrecked cop stays out of the chase add_child(c) # Drop them in behind, spread across the road, clear of the player. # Flattened: multiplying by the full basis while the player is pitched # (mid-jump, up a kerb) spawns cops in the air or under the road. var flat_b := Basis.looking_at( Vector3(-car.global_basis.z.x, 0.0, -car.global_basis.z.z).normalized(), Vector3.UP) var back := flat_b * Vector3((i - 0.5) * 4.0, 0.0, 22.0 + i * 5.0) c.global_transform = Transform3D( Basis.looking_at(-back.normalized(), Vector3.UP), car.global_position + back) c.wreck_started.connect(_cop_wrecked.bind(c)) cops.append(c) bust_t = 0.0 escape_t = 0.0 _msg("WANTED!\nlose them", 48) func _cop_wrecked(c: Cop) -> void: # taking one out is worth the same as any takedown if not car.wrecked and car.global_position.distance_to(c.global_position) < 25.0: car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) car.boost = car.boost_max Garage.earn(200) _msg("COP DOWN! +$200", 48) func _end_pursuit(escaped: bool) -> void: for c in cops: if is_instance_valid(c): c.queue_free() cops.clear() heat = 0.0 heat_cd = HEAT_COOLDOWN info_label.text = "" # nothing else writes this in cruise, so "WANTED" would stick if escaped: escapes += 1 car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) car.boost = car.boost_max Sfx.shot(car, "sting_win", -4.0) Garage.earn(500) _msg("LOST 'EM! x%d +$500" % escapes, 52) else: busts += 1 Sfx.shot(car, "sting_lose", -4.0) _msg("BUSTED", 52) func _pursuit_tick(delta: float) -> void: heat_cd = maxf(heat_cd - delta, 0.0) if cops.is_empty(): if heat > 0.0: heat = maxf(heat - HEAT_DECAY * delta, 0.0) return cops = cops.filter(func(c): return is_instance_valid(c)) if cops.is_empty(): _end_pursuit(true) return var nearest := INF for c in cops: if not c.wrecked: nearest = minf(nearest, car.global_position.distance_to(c.global_position)) if nearest == INF: # every cop wrecked = you won _end_pursuit(true) return if nearest < BUST_RANGE and not car.wrecked: bust_t += delta escape_t = 0.0 else: bust_t = maxf(bust_t - delta * 0.5, 0.0) if nearest > ESCAPE_RANGE: escape_t += delta else: escape_t = 0.0 info_label.text = "WANTED %dm %s" % [ int(nearest), ("BUSTED IN %ds" % ceili(BUST_TIME - bust_t)) if bust_t > 1.0 else ("LOSING THEM %ds" % ceili(ESCAPE_TIME - escape_t)) if escape_t > 0.5 else "%d cars" % cops.size()] if car.wrecked and nearest < 40.0: _end_pursuit(false) elif bust_t >= BUST_TIME: _end_pursuit(false) elif escape_t >= ESCAPE_TIME: _end_pursuit(true) func _rage_tick() -> void: # rivals recover on their own (no hold_wreck), so the supply of takedowns # never dries up; the leash keeps the brawl within reach var L := race_path.curve.get_baked_length() for r in rivals: if r.wrecked: continue if car.global_position.distance_to(r.global_position) > RAGE_LEASH: var off := fposmod(_road_off + randf_range(60.0, 120.0), L) r.global_transform = _grid_xform(off) r.progress = off r.linear_velocity = Vector3.ZERO r.angular_velocity = Vector3.ZERO func _oncoming_tick(delta: float) -> void: ## Burnout's oldest deal: drive against the flow, get paid in boost. ## Traffic runs the RacePath forward, so "oncoming" = moving against the ## local tangent while actually on the road. var L := race_path.curve.get_baked_length() _road_off = _nearest_off(car.global_position, _road_off, L) var p := race_path.to_global(race_path.curve.sample_baked(_road_off)) var tangent := (race_path.to_global(race_path.curve.sample_baked(fposmod(_road_off + 2.0, L))) - p) tangent.y = 0.0 var flat_v := car.linear_velocity flat_v.y = 0.0 var on_road := Vector2(car.global_position.x - p.x, car.global_position.z - p.z).length() < ONCOMING_LANE_D if on_road and flat_v.length() > ONCOMING_SPEED \ and tangent.length_squared() > 0.01 \ and flat_v.normalized().dot(tangent.normalized()) < -0.55: car.earn_boost(ONCOMING_EARN * delta) _oncoming_run += delta _oncoming_toast -= delta if _oncoming_toast <= 0.0: _oncoming_toast = 2.0 _msg("ONCOMING +boost", 30) else: _oncoming_run = 0.0 _oncoming_toast = 0.0 func _outrun_tick(delta: float) -> void: ## NFSU2-style impromptu 1v1: pull alongside a roaming rival at speed and it's ## on. Straight-line distance decides WHEN it ends (so ducking down a side ## street is a real escape); path order decides WHO won (so reversing away ## from the circuit is a loss, not a win). outrun_cd = maxf(outrun_cd - delta, 0.0) var L := race_path.curve.get_baked_length() if outrun_rival == null: if outrun_cd > 0.0 or car.wrecked: return for r in rivals: if not r.wrecked and car.linear_velocity.length() > 8.0 \ and car.global_position.distance_to(r.global_position) < OUTRUN_START_D: outrun_rival = r outrun_t = OUTRUN_TIME laps[car] = 0 laps[r] = 0 prev_off[car] = _nearest_off(car.global_position, r.progress, L) prev_off[r] = r.progress _msg("OUTRUN!\ngap them by %dm" % int(OUTRUN_GAP), 44) return return var r := outrun_rival outrun_t -= delta if Input.is_action_just_pressed("reset"): # R teleports home: that's a forfeit _outrun_end(false, "bailed") return prev_off[car] = _lap_step(car, _nearest_off(car.global_position, prev_off[car], L), L) prev_off[r] = _lap_step(r, r.progress, L) var my_total: float = laps[car] * L + prev_off[car] var rt: float = laps[r] * L + prev_off[r] var ahead := my_total >= rt # rubber-band: the rival hunts when losing, coasts a touch when winning r.power_scale = clampf(1.0 + (my_total - rt) / 250.0, 0.85, 1.4) var d := car.global_position.distance_to(r.global_position) info_label.text = "OUTRUN %s %dm / %dm %ds" % ["LEAD" if ahead else "CHASE", int(d), int(OUTRUN_GAP), ceili(outrun_t)] if car.wrecked: _outrun_end(false, "wrecked") elif r.wrecked and d < 60.0: _outrun_end(true, "rival wrecked") elif d > OUTRUN_GAP: _outrun_end(ahead, "") elif outrun_t <= 0.0: _outrun_end(false, "time up") func _outrun_end(won: bool, why: String) -> void: outrun_rival.power_scale = 1.0 outrun_rival = null outrun_cd = 8.0 info_label.text = "" var tail := ("\n(%s)" % why) if why != "" else "" if won: outruns_won += 1 car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) # takedown-tier reward car.boost = car.boost_max Sfx.shot(car, "sting_win", -4.0) Garage.earn(400) _msg("OUTRUN WON! x%d +$400%s" % [outruns_won, tail], 52) else: Sfx.shot(car, "sting_lose", -4.0) _msg("OUTRUN LOST%s" % tail, 44) func _race_tick() -> void: var L := race_path.curve.get_baked_length() prev_off[car] = _lap_step(car, _nearest_off(car.global_position, prev_off[car], L), L) for r in rivals: prev_off[r] = _lap_step(r, r.progress, L) var my_total: float = laps[car] * L + prev_off[car] var pos := 1 for r in rivals: var rt: float = laps[r] * L + prev_off[r] if rt > my_total: pos += 1 r.power_scale = clampf(1.0 + (my_total - rt) / 300.0, 0.8, 1.35) info_label.text = "P%d/%d LAP %d/%d" % [pos, rivals.size() + 1, mini(laps[car] + 1, LAPS), LAPS] if laps[car] >= LAPS: over = true _msg("FINISHED P%d\nEsc for menu" % pos, 48, true) func _lap_step(c: Car, off: float, L: float) -> float: if prev_off[c] > 0.8 * L and off < 0.2 * L: laps[c] += 1 elif off > 0.8 * L and prev_off[c] < 0.2 * L: laps[c] -= 1 return off func _nearest_off(pos: Vector3, seed_off: float, L: float) -> float: var best := seed_off var best_d := INF for i in 25: var o := fposmod(seed_off - 4.0 + i, L) var d := race_path.to_global(race_path.curve.sample_baked(o)).distance_squared_to(pos) if d < best_d: best_d = d best = o return best func _player_crash(dv: float) -> void: _shake = maxf(_shake, clampf(dv / 16.0, 0.12, 0.65)) if mode in ["crash", "junction"] and not over: score += minf(dv, 30.0) * 120.0 if dv > 3.0: add_heat(HEAT_PER_SMASH * minf(dv / 6.0, 2.0)) if mode == "junction" and jstate == "run" and dv > 5.0: jstate = "settle" time_left = JCT_SETTLE _msg("CRASH!\narrows = aftertouch space = crashbreaker", 36) return if dv > 6.0: _msg("SMASH", 40) func _player_wreck() -> void: Engine.time_scale = 0.35 # Impact Time get_tree().create_timer(1.0, true, false, true).timeout.connect( func() -> void: Engine.time_scale = 1.0) _msg("WRECKED", 48) func _rival_wrecked(r: Rival) -> void: if car.wrecked or car.global_position.distance_to(r.global_position) > 8.0: return car.boost_max = minf(car.boost_max + 0.5, Car.BOOST_CAP) car.boost = car.boost_max Sfx.shot(car, "sting_win", -4.0) Garage.earn(150) if mode == "rage" and not over: rage_downs += 1 _msg("TAKEDOWN! x%d +$150" % rage_downs, 56) else: _msg("TAKEDOWN! +$150", 56) func _traffic_smashed(impulse: float, t: TrafficCar) -> void: if is_instance_valid(t): Fx.sparks(t, clampi(int(impulse * 2.0), 12, 40)) if mode in ["crash", "junction"] and not over: score += impulse * 400.0 _msg("$%d" % int(impulse * 400.0), 40) car.earn_boost(0.2) add_heat(HEAT_PER_SMASH) func _crashbreaker() -> void: breaker_used = true score += 2000.0 var origin := car.global_position var q := PhysicsShapeQueryParameters3D.new() var sphere := SphereShape3D.new() sphere.radius = 14.0 q.shape = sphere q.transform = Transform3D(Basis.IDENTITY, origin) var hits := get_world_3d().direct_space_state.intersect_shape(q, 64) for h in hits: var b: Object = h.collider if b == car or not b is RigidBody3D: continue var dir: Vector3 = (b as RigidBody3D).global_position - origin var dist := dir.length() dir = (dir / maxf(dist, 0.5) + Vector3.UP * 0.6).normalized() var strength := 16.0 / (1.0 + dist * 0.25) if b is TrafficCar: (b as TrafficCar).blast(dir * strength) score += 500.0 else: (b as RigidBody3D).apply_central_impulse(dir * strength * (b as RigidBody3D).mass) car.apply_central_impulse(Vector3.UP * car.mass * 7.0) Sfx.shot(car, "boom", 0.0) Fx.explosion(car) _shake = 0.8 _msg("CRASHBREAKER!", 52) var boom := MeshInstance3D.new() var sm := SphereMesh.new() sm.radius = 1.0 sm.height = 2.0 # thin fast shockwave shell -- the fire itself is Fx.explosion's particles, # and the old opaque orange bubble used to drown them out completely var mat := StandardMaterial3D.new() mat.albedo_color = Color(1.0, 0.55, 0.15, 0.20) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.cull_mode = BaseMaterial3D.CULL_FRONT # inside face: reads as a wavefront sm.material = mat boom.mesh = sm add_child(boom) boom.global_position = origin var tw := create_tween() tw.tween_property(boom, "scale", Vector3.ONE * 15.0, 0.30) \ .set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) tw.parallel().tween_property(boom, "transparency", 1.0, 0.30) tw.tween_callback(boom.queue_free) func _jct_tally() -> void: jstate = "done" over = true var t: Array = junction.get("targets", [15000, 35000, 60000]) var medal := "NO MEDAL, MATE" var payout := int(score / 50.0) if score >= float(t[2]): medal = "GOLD!" payout += 1000 elif score >= float(t[1]): medal = "SILVER" payout += 600 elif score >= float(t[0]): medal = "BRONZE" payout += 300 Garage.earn(payout) _msg("DAMAGE: $%d\n%s +$%d bucks\nR to retry, Esc for menu" % [int(score), medal, payout], 48, true) func _msg(text: String, size: int, sticky := false) -> void: # a sticky message is a result screen; don't let a stray "NEAR MISS" from a # still-rolling wreck overwrite it a moment later if _sticky and not sticky: return _sticky = sticky msg_label.text = text msg_label.add_theme_font_size_override("font_size", size) if sticky: msg_timer = null return var t := get_tree().create_timer(1.2, true, false, true) msg_timer = t t.timeout.connect(func() -> void: if msg_timer == t: msg_label.text = "") func _process(delta: float) -> void: if Input.is_action_just_pressed("ui_cancel"): get_tree().change_scene_to_file("res://src/main.tscn") return if mode == "crash" and not over: time_left -= delta info_label.text = "$%d %d s" % [int(score), ceili(time_left)] if time_left <= 0.0: over = true var crash_pay := int(score / 50.0) Garage.earn(crash_pay) _msg("DAMAGE: $%d +$%d bucks\nR to retry, Esc for menu" % [int(score), crash_pay], 48, true) if mode == "junction" and not over: time_left -= delta info_label.text = "$%d %d s" % [int(score), ceili(time_left)] if time_left <= 0.0: if jstate == "run": _msg("Didn't even crash. Cooked.", 40) _jct_tally() if mode == "rage" and not over: time_left -= delta info_label.text = "%d TAKEDOWNS %d s" % [rage_downs, ceili(time_left)] if time_left <= 0.0: over = true var medal := "NO MEDAL, MATE" if rage_downs >= RAGE_TARGETS[2]: medal = "GOLD!" elif rage_downs >= RAGE_TARGETS[1]: medal = "SILVER" elif rage_downs >= RAGE_TARGETS[0]: medal = "BRONZE" Sfx.shot(car, "sting_win" if rage_downs >= RAGE_TARGETS[0] else "sting_lose", -4.0) var rage_pay := 300 if rage_downs >= RAGE_TARGETS[0] else 0 rage_pay = 600 if rage_downs >= RAGE_TARGETS[1] else rage_pay rage_pay = 1000 if rage_downs >= RAGE_TARGETS[2] else rage_pay Garage.earn(rage_pay) _msg("%d TAKEDOWNS\n%s +$%d bucks\nR to retry, Esc for menu" % [rage_downs, medal, rage_pay], 48, true) # drag and derby both tell you "R to retry", so they have to honour it too if mode == "race" and Input.is_action_just_pressed("reset"): # car.gd teleports the player home on R; without resyncing the lap # tracker, _nearest_off's 25 m local window walks the offset around the # loop and silently books phantom laps for c in laps: prev_off[c] = 0.0 if c == car else (c as Rival).progress laps[car] = 0 if over and mode in ["crash", "junction", "drag", "derby", "rage"] \ and Input.is_action_just_pressed("reset"): get_tree().reload_current_scene() return for sp in spinners: sp.rotate_x(delta * 0.09) # a full turn takes ~70 s, same as the real one var target := car.global_position + car.global_basis * Vector3(0, 2.4, 6.0) cam.global_position = cam.global_position.lerp(target, 1.0 - exp(-5.0 * delta)) cam.look_at(car.global_position + Vector3.UP * 1.2) if _shake > 0.005: # post-look_at, so the jolt reads as the camera being knocked, not re-aimed cam.global_position += Vector3(randf_range(-1, 1), randf_range(-1, 1), randf_range(-1, 1)) * _shake * 0.35 cam.rotation.z += randf_range(-1, 1) * _shake * 0.03 _shake *= exp(-6.0 * delta) cam.fov = lerpf(cam.fov, 84.0 if car.boosting else 72.0, 6.0 * delta) speed_label.text = "%d km/h" % roundf(car.speed_kmh()) boost_bar.size.x = 160.0 * car.boost_max boost_bar.max_value = 100.0 * car.boost_max boost_bar.value = car.boost * 100.0 heat_bar.visible = mode == "cruise" and heat > 0.02 and cops.is_empty() heat_bar.value = heat