Road Rage, oncoming boost, a real Crashbreaker fireball, title screen
The last two design-doc gameplay items land: - Road Rage: 90 s, wreck as many rivals as you can. They recover on their own (no hold_wreck) so the supply never dries up, and a 220 m leash teleports strays back to the road ahead of the player. Bronze/silver/gold at 4/8/12. - Oncoming: driving against the RacePath tangent while actually on the road (within 7 m of the line, above 12 m/s, heading dot < -0.55) trickles boost with a periodic toast. Runs in cruise, race and rage. Crashbreaker finally looks the part: layered Fx.explosion (70-particle additive fire bloom, rising smoke, 60-spark shower) inside a thin additive shockwave shell -- the old opaque orange bubble drowned the fire out completely, so it became a fast-fading front-culled dome instead. Plus a 0.8 camera shake. Menu is a title screen: FLUX-generated key art (battered sedan, flying hubcap, bridge, jacarandas -- the hubcap is canon now) behind a scrim, outlined golden title. Art prompt asks for no lettering; the plate gibberish is FLUX being FLUX. Smoke coverage for both mechanics. The oncoming test taught a harness lesson: setting global_position on a continuous_cd body SWEEPS it there, wrecking the car before the test began -- teleport via PhysicsServer3D.body_set_state. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3070a96b41
commit
63c32928ee
10
README.md
10
README.md
@ -59,6 +59,11 @@ Controls: WASD/arrows drive, Shift drift, Space boost, R reset (or retry after C
|
||||
sit on curved approaches where the launch street bends away from the intersection.
|
||||
Worth hand-picking better crossings for those two.
|
||||
- **Race** — 3 laps vs 3 rubber-banded rivals on the level's `RacePath`.
|
||||
- **Road Rage** — Burnout 3's headline mode: 90 seconds, wreck as many rivals
|
||||
as you can. They recover and keep driving so the supply never dries up, and a
|
||||
leash teleports strays back into play. Bronze/silver/gold at 4/8/12.
|
||||
- **Oncoming** — in Cruise, Race and Road Rage, driving against the traffic
|
||||
flow while actually on the road earns boost. The oldest deal in Burnout.
|
||||
- **Drag** — NFSU2's event, our code: a straight-line run down the level's longest
|
||||
genuinely straight stretch of wide road (the builder finds it and drops
|
||||
`DragStart`/`DragEnd` markers, so no level needs hand-authored coordinates).
|
||||
@ -289,5 +294,6 @@ forever and NC-licensed audio doesn't belong in this repo at all.
|
||||
## Design doc
|
||||
|
||||
`docs/BurnoutInfinityShitbox.pdf` — research report on the original game. Still to come:
|
||||
oncoming-lane boost, Road Rage mode, a DEM elevation pass (so the Story Bridge deck
|
||||
actually rises), and hand-picked crossings for the two scenery-crash junctions.
|
||||
a DEM elevation pass (so the Story Bridge deck actually rises), hand-picked crossings
|
||||
for the two scenery-crash junctions, and splitting wheel meshes out of the 13 imported
|
||||
cars so they spin like the procedural fleet's do.
|
||||
|
||||
BIN
assets/ui/title.jpg
Normal file
BIN
assets/ui/title.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 343 KiB |
40
assets/ui/title.jpg.import
Normal file
40
assets/ui/title.jpg.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dfj2nx15yao4b"
|
||||
path="res://.godot/imported/title.jpg-92335cdada3b990384c48ae100e0394f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/ui/title.jpg"
|
||||
dest_files=["res://.godot/imported/title.jpg-92335cdada3b990384c48ae100e0394f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
shot_boom.png.import
Normal file
40
shot_boom.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bsi3itd11kct2"
|
||||
path="res://.godot/imported/shot_boom.png-f4ed7c2b8b65bee8e8d626deb1966c57.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://shot_boom.png"
|
||||
dest_files=["res://.godot/imported/shot_boom.png-f4ed7c2b8b65bee8e8d626deb1966c57.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
shot_menu.png.import
Normal file
40
shot_menu.png.import
Normal file
@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://btgywefwig6wi"
|
||||
path="res://.godot/imported/shot_menu.png-c61344a3ec1e3de2e32ce9670e847ca3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://shot_menu.png"
|
||||
dest_files=["res://.godot/imported/shot_menu.png-c61344a3ec1e3de2e32ce9670e847ca3.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
45
src/fx.gd
45
src/fx.gd
@ -88,6 +88,51 @@ static func _init_shared() -> void:
|
||||
_dust_mat.scale_max = 1.2
|
||||
_dust_mat.color_ramp = _ramp([Color(0.75, 0.73, 0.70, 0.0), Color(0.72, 0.70, 0.66, 0.4), Color(0.7, 0.7, 0.7, 0.0)])
|
||||
|
||||
static var _fire_mat: ParticleProcessMaterial
|
||||
static var _fire_draw: QuadMesh
|
||||
|
||||
static func explosion(parent: Node3D) -> void:
|
||||
## Crashbreaker-grade fireball: additive fire core, rising smoke, spark
|
||||
## shower. Layered one-shots, all self-freeing.
|
||||
_init_shared()
|
||||
if _fire_mat == null:
|
||||
_fire_mat = ParticleProcessMaterial.new()
|
||||
_fire_mat.direction = Vector3(0, 1, 0)
|
||||
_fire_mat.spread = 180.0
|
||||
_fire_mat.initial_velocity_min = 6.0
|
||||
_fire_mat.initial_velocity_max = 17.0
|
||||
_fire_mat.gravity = Vector3(0, 3.0, 0)
|
||||
_fire_mat.scale_min = 0.8
|
||||
_fire_mat.scale_max = 2.2
|
||||
_fire_mat.damping_min = 4.0
|
||||
_fire_mat.damping_max = 7.0
|
||||
_fire_mat.color_ramp = _ramp([Color(1.0, 0.95, 0.6), Color(1.0, 0.55, 0.08),
|
||||
Color(0.85, 0.2, 0.02, 0.6), Color(0.2, 0.05, 0.02, 0.0)])
|
||||
_fire_draw = _draw_quad(2.6, Color(1, 0.6, 0.15), true)
|
||||
var fire := GPUParticles3D.new()
|
||||
fire.process_material = _fire_mat
|
||||
fire.draw_pass_1 = _fire_draw
|
||||
fire.amount = 70
|
||||
fire.lifetime = 0.85
|
||||
fire.one_shot = true
|
||||
fire.explosiveness = 0.95
|
||||
parent.add_child(fire)
|
||||
fire.emitting = true
|
||||
var smoke := GPUParticles3D.new()
|
||||
smoke.process_material = _smoke_mat
|
||||
smoke.draw_pass_1 = _smoke_draw
|
||||
smoke.amount = 30
|
||||
smoke.lifetime = 2.0
|
||||
smoke.one_shot = true
|
||||
smoke.explosiveness = 0.6
|
||||
smoke.position = Vector3(0, 1.0, 0)
|
||||
parent.add_child(smoke)
|
||||
smoke.emitting = true
|
||||
sparks(parent, 60)
|
||||
parent.get_tree().create_timer(3.0).timeout.connect(func() -> void:
|
||||
fire.queue_free()
|
||||
smoke.queue_free())
|
||||
|
||||
static func sparks(parent: Node3D, amount := 24) -> void:
|
||||
## One-shot burst at the parent's position, self-freeing.
|
||||
_init_shared()
|
||||
|
||||
110
src/game.gd
110
src/game.gd
@ -55,6 +55,17 @@ 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
|
||||
@ -86,6 +97,11 @@ 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
|
||||
@ -242,6 +258,19 @@ func _ready() -> void:
|
||||
car.global_position + lane)
|
||||
r.progress = 0.0
|
||||
rivals.append(r)
|
||||
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()
|
||||
@ -575,6 +604,10 @@ func _physics_process(delta: float) -> void:
|
||||
_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":
|
||||
@ -782,6 +815,45 @@ func _pursuit_tick(delta: float) -> void:
|
||||
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
|
||||
@ -905,7 +977,11 @@ func _rival_wrecked(r: Rival) -> void:
|
||||
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)
|
||||
if mode == "rage" and not over:
|
||||
rage_downs += 1
|
||||
_msg("TAKEDOWN! x%d" % rage_downs, 56)
|
||||
else:
|
||||
_msg("TAKEDOWN!", 56)
|
||||
|
||||
func _traffic_smashed(impulse: float, t: TrafficCar) -> void:
|
||||
if is_instance_valid(t):
|
||||
@ -941,23 +1017,29 @@ func _crashbreaker() -> void:
|
||||
(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.5, 0.1, 0.7)
|
||||
mat.albedo_color = Color(1.0, 0.55, 0.15, 0.20)
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.4, 0.05)
|
||||
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 * 14.0, 0.45)
|
||||
tw.parallel().tween_property(boom, "transparency", 1.0, 0.45)
|
||||
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:
|
||||
@ -1007,6 +1089,20 @@ func _process(delta: float) -> void:
|
||||
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)
|
||||
_msg("%d TAKEDOWNS\n%s\nR to retry, Esc for menu" % [rage_downs, medal], 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
|
||||
@ -1015,7 +1111,7 @@ func _process(delta: float) -> void:
|
||||
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"] \
|
||||
if over and mode in ["crash", "junction", "drag", "derby", "rage"] \
|
||||
and Input.is_action_just_pressed("reset"):
|
||||
get_tree().reload_current_scene()
|
||||
return
|
||||
|
||||
@ -2,7 +2,7 @@ extends Control
|
||||
## Menu: pick a car, a level and a mode from whatever exists, hit RACE.
|
||||
|
||||
const MODES := [["cruise", "Cruise"], ["crash", "Crash Mode"], ["race", "Race"],
|
||||
["drag", "Drag"], ["derby", "Derby"]]
|
||||
["rage", "Road Rage"], ["drag", "Drag"], ["derby", "Derby"]]
|
||||
|
||||
@onready var car_list: ItemList = $VBox/HBox/Cars
|
||||
@onready var level_list: ItemList = $VBox/HBox/Levels
|
||||
|
||||
@ -1,12 +1,26 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[gd_scene load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://src/main.gd" id="1"]
|
||||
[ext_resource type="Texture2D" path="res://assets/ui/title.jpg" id="2"]
|
||||
|
||||
[node name="Main" type="Control"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="BG" type="TextureRect" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
texture = ExtResource("2")
|
||||
expand_mode = 1
|
||||
stretch_mode = 6
|
||||
modulate = Color(0.62, 0.62, 0.68, 1)
|
||||
|
||||
[node name="Scrim" type="ColorRect" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0.05, 0.05, 0.09, 0.42)
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
@ -19,7 +33,10 @@ theme_override_constants/separation = 20
|
||||
[node name="Title" type="Label" parent="VBox"]
|
||||
text = "BURNOUT INFINITY: SHITBOX"
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 44
|
||||
theme_override_font_sizes/font_size = 52
|
||||
theme_override_colors/font_color = Color(1, 0.82, 0.35, 1)
|
||||
theme_override_colors/font_outline_color = Color(0.12, 0.05, 0.02, 1)
|
||||
theme_override_constants/outline_size = 10
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="VBox"]
|
||||
size_flags_vertical = 3
|
||||
|
||||
@ -162,6 +162,43 @@ func _run() -> void:
|
||||
_check(d.target != null and d.target != d, "derby car has no/self target")
|
||||
game.free()
|
||||
|
||||
# road rage: rivals spawn, a takedown near the player counts and pays
|
||||
print(" .. rage")
|
||||
game = await _boot("rage")
|
||||
_check(game.rivals.size() == Game.RAGE_RIVALS, "rage field wrong size")
|
||||
var prey: Rival = game.rivals[0]
|
||||
await physics_frame
|
||||
game.car.global_position = prey.global_position + Vector3(3, 0.2, 0)
|
||||
await physics_frame
|
||||
prey.wreck_started.emit()
|
||||
_check(game.rage_downs == 1, "rage takedown didn't count (%d)" % game.rage_downs)
|
||||
game.time_left = 0.05
|
||||
for i in 10:
|
||||
await physics_frame
|
||||
_check(game.over, "rage never tallied")
|
||||
game.free()
|
||||
|
||||
# oncoming: rolling against the road tangent at speed earns boost
|
||||
game = await _boot("cruise")
|
||||
await physics_frame
|
||||
var curve := game.race_path.curve
|
||||
var road_pt := game.race_path.to_global(curve.sample_baked(20.0))
|
||||
var tan := (game.race_path.to_global(curve.sample_baked(24.0)) - road_pt).normalized()
|
||||
# Teleport via the physics server: setting global_position on a
|
||||
# continuous_cd body SWEEPS it there, slamming through everything en route
|
||||
# -- the first version of this test wrecked the car before it could earn.
|
||||
var xf := Transform3D(Basis.looking_at(-tan, Vector3.UP), road_pt + Vector3.UP * 0.75)
|
||||
PhysicsServer3D.body_set_state(game.car.get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, xf)
|
||||
game.car.wrecked = false
|
||||
game.car.boost = 0.0
|
||||
game.car._prev_lv = -tan * 16.0 # don't let the velocity jump read as a crash
|
||||
game.car.linear_velocity = -tan * 16.0
|
||||
for i in 45:
|
||||
await physics_frame
|
||||
game.car.linear_velocity = -tan * 16.0 # hold course against drag
|
||||
_check(game.car.boost > 0.05, "oncoming earned no boost (%.3f)" % game.car.boost)
|
||||
game.free()
|
||||
|
||||
# race: rivals spawn and make progress along the path
|
||||
print(" .. race")
|
||||
game = await _boot("race")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user