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 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 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 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_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 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 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"], 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) var from := Vector3(mk[0].global_position.x, 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, 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 * 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 * 0.7 + lane + fwd * 120.0) strip.curve = sc add_child(strip) var pool := Registry.cars().values() var r: Rival = (load("res://src/rival.tscn") as PackedScene).instantiate() r.stats = load(pool[randi() % pool.size()]) r.path = strip add_child(r) r.global_transform = Transform3D(Basis.looking_at(fwd, Vector3.UP), car.global_position + lane) r.progress = 0.0 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"]: curve.add_point(Vector3(p[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, 0.75, 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 hm.material = hmat head.mesh = hm head.position = Vector3(1.45, size.y * 0.5 - 0.36, 0) b.add_child(head) 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, size.y * 0.5 + 0.02, t.origin.z)) b.sleeping = true n += 1 func _build_environment() -> void: ## Brisbane midday: hard high sun, deep blue zenith, everything a bit blown out. ## The bare default (no tonemap, grey ambient) rendered the city flat and dead. var sun := DirectionalLight3D.new() 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) var sky_mat := ProceduralSkyMaterial.new() sky_mat.sky_top_color = Color(0.27, 0.46, 0.74) sky_mat.sky_horizon_color = Color(0.80, 0.86, 0.92) sky_mat.sky_energy_multiplier = 1.0 sky_mat.ground_bottom_color = Color(0.28, 0.28, 0.30) sky_mat.ground_horizon_color = Color(0.66, 0.72, 0.78) sky_mat.sun_angle_max = 4.0 sky_mat.sun_curve = 0.08 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 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 var env := WorldEnvironment.new() env.environment = e add_child(env) 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 == "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) _msg("WRECKED 'EM! x%d" % 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 _msg("LAST ONE RUNNING\n%d wrecked\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 drag_t += delta bog = maxf(bog - delta, 0.0) 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"): if revs >= SHIFT_LO and revs < REDLINE: clean_shifts += 1 Sfx.shot(car, "clunk", -2.0) _msg("CLEAN SHIFT", 34) else: blown_shifts += 1 bog = 0.9 Sfx.shot(car, "bog", -2.0) _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 # 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 # 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)) var flat := Vector3(car.global_position.x, 0.0, car.global_position.z) var left := flat.distance_to(drag_end) var rival_left := INF if not rivals.is_empty(): var rf := Vector3(rivals[0].global_position.x, 0.0, rivals[0].global_position.z) rival_left = rf.distance_to(drag_end) info_label.text = "GEAR %d %s %dm %.1fs" % [ mini(gear, DRAG_GEARS), ("BOGGED" if bog > 0.0 else ("SHIFT!" if revs >= SHIFT_LO else "%d%%" % int(revs * 100))), int(left), drag_t] # the rival crossing has to end it too, or losing means the run never stops if left < 12.0 or rival_left < 12.0: over = true car.power_scale = 1.0 # _drag_tick stops on `over`, so reset it here var won := left <= rival_left _msg("%s %.2fs\n%d clean / %d blown\nR to retry, Esc for menu" % ["WON" if won else "LOST", drag_t, clean_shifts, blown_shifts], 44, true) 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 _msg("COP DOWN!", 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) _msg("LOST 'EM! x%d" % 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 _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) _msg("OUTRUN WON! x%d%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) _msg("TAKEDOWN!", 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) _msg("CRASHBREAKER!", 52) var boom := MeshInstance3D.new() var sm := SphereMesh.new() sm.radius = 1.0 sm.height = 2.0 var mat := StandardMaterial3D.new() mat.albedo_color = Color(1.0, 0.5, 0.1, 0.7) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.emission_enabled = true mat.emission = Color(1.0, 0.4, 0.05) sm.material = mat boom.mesh = sm add_child(boom) boom.global_position = origin var tw := create_tween() tw.tween_property(boom, "scale", Vector3.ONE * 14.0, 0.45) tw.parallel().tween_property(boom, "transparency", 1.0, 0.45) 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" if score >= float(t[2]): medal = "GOLD!" elif score >= float(t[1]): medal = "SILVER" elif score >= float(t[0]): medal = "BRONZE" _msg("DAMAGE: $%d\n%s\nR to retry, Esc for menu" % [int(score), medal], 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 _msg("DAMAGE: $%d\nR to retry, Esc for menu" % int(score), 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() # 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"] \ 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