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
|
## 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 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
|
- **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)
|
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
|
class_name Game
|
||||||
extends Node3D
|
extends Node3D
|
||||||
## Loads the chosen level, spawns cars, runs the chosen mode:
|
## 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)
|
## crash -- 60s damage-dollar rampage (Crash Mode v0)
|
||||||
## race -- 3 laps vs rivals on the level's RacePath, rubber-banded
|
## 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).
|
## 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 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_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 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 jstate := "" # junction: "run" -> "settle" -> "done"
|
||||||
var breaker_used := false
|
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 car: Car
|
||||||
var cam: Camera3D
|
var cam: Camera3D
|
||||||
var race_path: Path3D
|
var race_path: Path3D
|
||||||
@ -101,6 +111,20 @@ func _ready() -> void:
|
|||||||
prev_off[r] = off
|
prev_off[r] = off
|
||||||
r.wreck_started.connect(_rival_wrecked.bind(r))
|
r.wreck_started.connect(_rival_wrecked.bind(r))
|
||||||
rivals.append(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
|
car.spawn_xform = car.global_transform
|
||||||
|
|
||||||
if mode == "junction":
|
if mode == "junction":
|
||||||
@ -265,6 +289,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
_msg("NEAR MISS", 32)
|
_msg("NEAR MISS", 32)
|
||||||
if mode == "race" and race_path and not over:
|
if mode == "race" and race_path and not over:
|
||||||
_race_tick()
|
_race_tick()
|
||||||
|
if mode == "cruise" and race_path:
|
||||||
|
_outrun_tick(delta)
|
||||||
if mode == "junction" and jstate == "settle":
|
if mode == "junction" and jstate == "settle":
|
||||||
if Input.is_action_just_pressed("boost") and not breaker_used:
|
if Input.is_action_just_pressed("boost") and not breaker_used:
|
||||||
_crashbreaker()
|
_crashbreaker()
|
||||||
@ -277,6 +303,66 @@ func _physics_process(delta: float) -> void:
|
|||||||
+ fwd.normalized() * Input.get_axis("brake", "throttle")
|
+ fwd.normalized() * Input.get_axis("brake", "throttle")
|
||||||
car.apply_central_force(push * car.mass * 9.0)
|
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:
|
func _race_tick() -> void:
|
||||||
var L := race_path.curve.get_baked_length()
|
var L := race_path.curve.get_baked_length()
|
||||||
prev_off[car] = _lap_step(car, _nearest_off(car.global_position, prev_off[car], L), L)
|
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.junction = {}
|
||||||
game.free()
|
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
|
# race: rivals spawn and make progress along the path
|
||||||
game = await _boot("race")
|
game = await _boot("race")
|
||||||
assert(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing")
|
assert(game.rivals.size() == Game.RIVAL_COUNT, "rivals missing")
|
||||||
@ -98,7 +118,7 @@ func _run() -> void:
|
|||||||
assert(moved, "no rival is driving")
|
assert(moved, "no rival is driving")
|
||||||
game.free()
|
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)])
|
[cars.size(), levels.size(), dist, int(crash_score), int(jct_score)])
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user