Outrun mode: NFSU2-style roaming 1v1 duels in cruise
Two rivals now lap the RacePath in free roam. Pull alongside one at speed and the duel arms: first to gap the other by 250 m wins. Straight-line distance decides when it ends (side-street escapes are real); path order decides who won (reversing away is a loss). Rubber-banded chase, takedown-tier boost payout, R mid-duel forfeits, 8 s re-arm cooldown. Mechanics only -- no assets from anywhere near an EA disk. Smoke test arms a duel and settles the win path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
82dae637a1
commit
da63829e9e
@ -12,7 +12,12 @@ Controls: WASD/arrows drive, Shift drift, Space boost, R reset (or retry after C
|
||||
|
||||
## Modes
|
||||
|
||||
- **Cruise** — free roam with traffic.
|
||||
- **Cruise** — free roam with traffic, plus two rivals lapping the circuit.
|
||||
Pull alongside one at speed and it's an **Outrun** (NFSU2 rules, our code):
|
||||
first to gap the other by 250 m wins. Straight-line distance decides *when*
|
||||
it ends — so ducking down a side street is a real escape — but path order
|
||||
decides *who* won, so reversing away from the circuit is a loss. Winning
|
||||
pays takedown-tier boost. R mid-duel is a forfeit.
|
||||
- **Crash Mode** — 60 seconds, rack up damage dollars ramming traffic and parked cars.
|
||||
- **Crash Junctions** — pick a "CRASH JCT" entry in the level list: launch into scripted
|
||||
cross-traffic convoys at a real intersection, then Aftertouch-steer your wreck (arrows)
|
||||
|
||||
88
src/game.gd
88
src/game.gd
@ -1,7 +1,8 @@
|
||||
class_name Game
|
||||
extends Node3D
|
||||
## Loads the chosen level, spawns cars, runs the chosen mode:
|
||||
## cruise -- free roam with traffic
|
||||
## 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).
|
||||
@ -17,10 +18,19 @@ const RIVAL_COUNT := 3
|
||||
const TRAFFIC_COUNT := {"cruise": 6, "crash": 10, "race": 4}
|
||||
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
|
||||
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 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
|
||||
@ -101,6 +111,20 @@ func _ready() -> void:
|
||||
prev_off[r] = off
|
||||
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":
|
||||
@ -265,6 +289,8 @@ func _physics_process(delta: float) -> void:
|
||||
_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 == "junction" and jstate == "settle":
|
||||
if Input.is_action_just_pressed("boost") and not breaker_used:
|
||||
_crashbreaker()
|
||||
@ -277,6 +303,66 @@ func _physics_process(delta: float) -> void:
|
||||
+ fwd.normalized() * Input.get_axis("brake", "throttle")
|
||||
car.apply_central_force(push * car.mass * 9.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
|
||||
_msg("OUTRUN WON! x%d%s" % [outruns_won, tail], 52)
|
||||
else:
|
||||
_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)
|
||||
|
||||
@ -85,6 +85,26 @@ func _run() -> void:
|
||||
Game.junction = {}
|
||||
game.free()
|
||||
|
||||
# outrun: roaming rivals exist in cruise; pulling alongside at speed arms the
|
||||
# duel, and the win path pays out and clears state
|
||||
game = await _boot("cruise")
|
||||
assert(game.rivals.size() == Game.ROAM_RIVALS, "no roaming rivals in cruise")
|
||||
var rr: Rival = game.rivals[0]
|
||||
await physics_frame
|
||||
game.car.global_position = rr.global_position + Vector3(3, 0.3, 0)
|
||||
game.car.linear_velocity = Vector3(0, 0, -12) # above the 8 m/s arming speed
|
||||
for i in 5:
|
||||
await physics_frame
|
||||
if game.outrun_rival != null:
|
||||
break
|
||||
assert(game.outrun_rival == rr, "outrun didn't arm on proximity")
|
||||
var bm0: float = game.car.boost_max
|
||||
game._outrun_end(true, "test")
|
||||
assert(game.outrun_rival == null and game.outruns_won == 1, "outrun win didn't settle")
|
||||
assert(game.car.boost_max > bm0, "outrun win paid no boost")
|
||||
assert(game.outrun_cd > 0.0, "no re-arm cooldown after outrun")
|
||||
game.free()
|
||||
|
||||
# race: rivals spawn and make progress along the path
|
||||
game = await _boot("race")
|
||||
assert(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing")
|
||||
@ -98,7 +118,7 @@ func _run() -> void:
|
||||
assert(moved, "no rival is driving")
|
||||
game.free()
|
||||
|
||||
print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | race AI driving" %
|
||||
print("smoke ok: %d cars, %d levels | cruise %.1f m | crash $%d | junction $%d | outrun arms+pays | race AI driving" %
|
||||
[cars.size(), levels.size(), dist, int(crash_score), int(jct_score)])
|
||||
quit(0)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user