Engine v0: Godot 4.6 data-driven arcade racer scaffold
Drop-in content: cars/<id>/stats.tres (+optional car.glb), levels/<id>/level.glb or .tscn with a Spawn marker. Raycast arcade car (boost, drift, drift-earns-boost), chase cam, HUD, menu built from registry scan. Placeholder Woolies carpark with shuntable parked cars. Headless smoke test + windowed screenshot harness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
1c0b7efb32
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.godot/
|
||||||
|
.DS_Store
|
||||||
|
*.blend1
|
||||||
|
screenshot.png
|
||||||
54
README.md
Normal file
54
README.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Burnout Infinity: Shitbox
|
||||||
|
|
||||||
|
Burnout 3-style arcade racer, Aussie shitboxes, Woolies carparks. Godot 4.6 + Blender, native Apple Silicon.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
Controls: WASD/arrows drive, Shift drift, Space boost (earn it by drifting), R reset, Esc menu.
|
||||||
|
|
||||||
|
## Add a car
|
||||||
|
|
||||||
|
Make a folder `cars/<id>/` containing `stats.tres`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[gd_resource type="Resource" script_class="CarStats" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://src/car_stats.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
display_name = "Datto 120Y"
|
||||||
|
model_path = "res://cars/datto_120y/car.glb"
|
||||||
|
engine_power = 12.0
|
||||||
|
mass = 900.0
|
||||||
|
grip = 6.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Model is optional — no `car.glb` means you drive the placeholder box. Export the GLB from
|
||||||
|
Blender with the car facing **-Y** (Blender forward), roughly 1.8 x 1.4 x 4.2 m, origin at the
|
||||||
|
centre, wheels-bottom around z = -0.5.
|
||||||
|
|
||||||
|
## Add a level
|
||||||
|
|
||||||
|
Make a folder `levels/<id>/` containing `level.glb` (or `level.tscn`). Blender convention:
|
||||||
|
|
||||||
|
- Objects that should be solid: suffix the object name with `-col` (Godot generates collision on import).
|
||||||
|
- Add an Empty named `Spawn` where the car starts (its -Y/forward axis = drive direction).
|
||||||
|
- Export scene as glTF Binary to `levels/<id>/level.glb`. Done — it appears in the menu.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --headless --path . --import
|
||||||
|
/Applications/Godot.app/Contents/MacOS/Godot --headless --path . --script tests/smoke.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design doc
|
||||||
|
|
||||||
|
`docs/BurnoutInfinityShitbox.pdf` — research report on the original game. The build order the
|
||||||
|
report implies: 1) handling + boost loop (done, tune forever), 2) crash/damage + takedowns,
|
||||||
|
3) traffic, 4) Crash Mode in the carpark, 5) race modes + AI.
|
||||||
11
cars/gemmy_tx/stats.tres
Normal file
11
cars/gemmy_tx/stats.tres
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CarStats" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://src/car_stats.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
display_name = "Gemmy TX"
|
||||||
|
model_path = ""
|
||||||
|
engine_power = 14.0
|
||||||
|
mass = 950.0
|
||||||
|
grip = 7.0
|
||||||
11
cars/vl_terbo/stats.tres
Normal file
11
cars/vl_terbo/stats.tres
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CarStats" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://src/car_stats.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
display_name = "VL Terbo"
|
||||||
|
model_path = ""
|
||||||
|
engine_power = 18.0
|
||||||
|
mass = 1300.0
|
||||||
|
grip = 5.0
|
||||||
BIN
docs/BurnoutInfinityShitbox.pdf
Normal file
BIN
docs/BurnoutInfinityShitbox.pdf
Normal file
Binary file not shown.
39
levels/woolies_carpark/carpark.gd
Normal file
39
levels/woolies_carpark/carpark.gd
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
extends Node3D
|
||||||
|
## ponytail: placeholder carpark built in code -- real levels arrive as level.glb from Blender
|
||||||
|
## and this whole folder becomes just the GLB + nothing else.
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 8
|
||||||
|
_box(Vector3(0, -0.5, 0), Vector3(240, 1, 160), Color(0.35, 0.35, 0.37)) # tarmac
|
||||||
|
for side in [-1, 1]: # kerb walls
|
||||||
|
_box(Vector3(0, 0.4, side * 79.0), Vector3(240, 0.8, 2), Color(0.6, 0.6, 0.6))
|
||||||
|
_box(Vector3(side * 119.0, 0.4, 0), Vector3(2, 0.8, 160), Color(0.6, 0.6, 0.6))
|
||||||
|
for row in 4: # rows of parked shitboxes, shuntable
|
||||||
|
for bay in 10:
|
||||||
|
if rng.randf() < 0.35:
|
||||||
|
continue
|
||||||
|
var pos := Vector3(-27.0 + bay * 6.0, 0.8, -30.0 + row * 18.0)
|
||||||
|
_box(pos, Vector3(1.9, 1.4, 4.4), Color.from_hsv(rng.randf(), 0.5, rng.randf_range(0.4, 0.9)), true)
|
||||||
|
for i in 6: # light poles
|
||||||
|
_box(Vector3(-30.0 + i * 12.0, 3.0, 8.0), Vector3(0.4, 6.0, 0.4), Color(0.3, 0.3, 0.3))
|
||||||
|
|
||||||
|
func _box(pos: Vector3, size: Vector3, color: Color, dynamic := false) -> void:
|
||||||
|
var body: PhysicsBody3D = RigidBody3D.new() if dynamic else StaticBody3D.new()
|
||||||
|
if dynamic:
|
||||||
|
body.mass = 300.0
|
||||||
|
var shape := CollisionShape3D.new()
|
||||||
|
var bs := BoxShape3D.new()
|
||||||
|
bs.size = size
|
||||||
|
shape.shape = bs
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
var bm := BoxMesh.new()
|
||||||
|
bm.size = size
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = color
|
||||||
|
bm.material = mat
|
||||||
|
mi.mesh = bm
|
||||||
|
body.add_child(shape)
|
||||||
|
body.add_child(mi)
|
||||||
|
body.position = pos
|
||||||
|
add_child(body)
|
||||||
1
levels/woolies_carpark/carpark.gd.uid
Normal file
1
levels/woolies_carpark/carpark.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://baogh32opqndt
|
||||||
9
levels/woolies_carpark/level.tscn
Normal file
9
levels/woolies_carpark/level.tscn
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://levels/woolies_carpark/carpark.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="WooliesCarpark" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Spawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 55)
|
||||||
43
project.godot
Normal file
43
project.godot
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="Burnout Infinity: Shitbox"
|
||||||
|
run/main_scene="res://src/main.tscn"
|
||||||
|
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=1600
|
||||||
|
window/size/viewport_height=900
|
||||||
|
|
||||||
|
[input]
|
||||||
|
|
||||||
|
throttle={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"physical_keycode":87,"device":-1), Object(InputEventKey,"physical_keycode":4194320,"device":-1)]
|
||||||
|
}
|
||||||
|
brake={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"physical_keycode":83,"device":-1), Object(InputEventKey,"physical_keycode":4194322,"device":-1)]
|
||||||
|
}
|
||||||
|
steer_left={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"physical_keycode":65,"device":-1), Object(InputEventKey,"physical_keycode":4194319,"device":-1)]
|
||||||
|
}
|
||||||
|
steer_right={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"physical_keycode":68,"device":-1), Object(InputEventKey,"physical_keycode":4194321,"device":-1)]
|
||||||
|
}
|
||||||
|
drift={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"physical_keycode":4194325,"device":-1)]
|
||||||
|
}
|
||||||
|
boost={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"physical_keycode":32,"device":-1)]
|
||||||
|
}
|
||||||
|
reset={
|
||||||
|
"deadzone": 0.2,
|
||||||
|
"events": [Object(InputEventKey,"physical_keycode":82,"device":-1)]
|
||||||
|
}
|
||||||
78
src/car.gd
Normal file
78
src/car.gd
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
class_name Car
|
||||||
|
extends RigidBody3D
|
||||||
|
## Arcade raycast car: 4 suspension rays, body-level tyre model.
|
||||||
|
## ponytail: body-level grip/drive forces -- go per-wheel only if the feel demands it.
|
||||||
|
|
||||||
|
@export var stats: CarStats
|
||||||
|
|
||||||
|
const REST := 0.55 # suspension rest length, m
|
||||||
|
const STEER_ANGLE := 0.6
|
||||||
|
const STEER_SPEED := 10.0
|
||||||
|
const DRIFT_GRIP_MULT := 0.35
|
||||||
|
const BOOST_MULT := 1.8
|
||||||
|
const BOOST_DRAIN := 0.35 # meter/sec while boosting
|
||||||
|
const DRIFT_EARN := 0.3 # meter/sec while sliding
|
||||||
|
const DRAG := 0.012 # quadratic drag -> top speed = sqrt(power/DRAG)
|
||||||
|
|
||||||
|
var steer := 0.0
|
||||||
|
var boost := 0.0 # 0..1
|
||||||
|
var boosting := false
|
||||||
|
var slip := 0.0 # sideways m/s
|
||||||
|
var spawn_xform: Transform3D
|
||||||
|
|
||||||
|
@onready var wheels: Array[Node] = [$FL, $FR, $RL, $RR]
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
mass = stats.mass
|
||||||
|
center_of_mass_mode = RigidBody3D.CENTER_OF_MASS_MODE_CUSTOM
|
||||||
|
center_of_mass = Vector3(0, -0.3, 0)
|
||||||
|
angular_damp = 3.0
|
||||||
|
spawn_xform = global_transform
|
||||||
|
if stats.model_path != "" and ResourceLoader.exists(stats.model_path):
|
||||||
|
$Body.visible = false
|
||||||
|
add_child((load(stats.model_path) as PackedScene).instantiate())
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if Input.is_action_just_pressed("reset"):
|
||||||
|
global_transform = spawn_xform
|
||||||
|
linear_velocity = Vector3.ZERO
|
||||||
|
angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
var throttle := Input.get_axis("brake", "throttle")
|
||||||
|
steer = lerpf(steer, Input.get_axis("steer_right", "steer_left") * STEER_ANGLE, STEER_SPEED * delta)
|
||||||
|
var drifting := Input.is_action_pressed("drift")
|
||||||
|
boosting = Input.is_action_pressed("boost") and boost > 0.0
|
||||||
|
|
||||||
|
var space := get_world_3d().direct_space_state
|
||||||
|
var grounded := 0
|
||||||
|
for w: Node3D in wheels:
|
||||||
|
var from := w.global_position
|
||||||
|
var q := PhysicsRayQueryParameters3D.create(from, from - global_basis.y * REST)
|
||||||
|
q.exclude = [get_rid()]
|
||||||
|
var hit := space.intersect_ray(q)
|
||||||
|
if hit:
|
||||||
|
grounded += 1
|
||||||
|
var offset := REST - from.distance_to(hit.position)
|
||||||
|
var point_vel := linear_velocity + angular_velocity.cross(from - global_position)
|
||||||
|
var spring := offset * mass * 25.0 - global_basis.y.dot(point_vel) * mass * 2.0
|
||||||
|
if spring > 0.0:
|
||||||
|
apply_force(global_basis.y * spring, from - global_position)
|
||||||
|
|
||||||
|
if grounded >= 2:
|
||||||
|
var fwd := -global_basis.z
|
||||||
|
var speed := fwd.dot(linear_velocity)
|
||||||
|
var power := stats.engine_power * (BOOST_MULT if boosting else 1.0)
|
||||||
|
apply_central_force(fwd * throttle * power * mass)
|
||||||
|
apply_torque(global_basis.y * steer * clampf(speed / 12.0, -1.0, 1.0) * mass * 14.0)
|
||||||
|
slip = global_basis.x.dot(linear_velocity)
|
||||||
|
var grip := stats.grip * (DRIFT_GRIP_MULT if drifting else 1.0)
|
||||||
|
apply_central_force(-global_basis.x * slip * grip * mass)
|
||||||
|
apply_central_force(-linear_velocity * linear_velocity.length() * mass * DRAG)
|
||||||
|
if absf(slip) > 4.0 and speed > 8.0:
|
||||||
|
boost = minf(boost + DRIFT_EARN * delta, 1.0)
|
||||||
|
|
||||||
|
if boosting:
|
||||||
|
boost = maxf(boost - BOOST_DRAIN * delta, 0.0)
|
||||||
|
|
||||||
|
func speed_kmh() -> float:
|
||||||
|
return linear_velocity.length() * 3.6
|
||||||
1
src/car.gd.uid
Normal file
1
src/car.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://brt5bxt4u8k23
|
||||||
30
src/car.tscn
Normal file
30
src/car.tscn
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://src/car.gd" id="1"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="shape"]
|
||||||
|
size = Vector3(1.8, 1.0, 4.2)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="mesh"]
|
||||||
|
size = Vector3(1.8, 1.0, 4.2)
|
||||||
|
|
||||||
|
[node name="Car" type="RigidBody3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Shape" type="CollisionShape3D" parent="."]
|
||||||
|
shape = SubResource("shape")
|
||||||
|
|
||||||
|
[node name="Body" type="MeshInstance3D" parent="."]
|
||||||
|
mesh = SubResource("mesh")
|
||||||
|
|
||||||
|
[node name="FL" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.8, -0.3, -1.5)
|
||||||
|
|
||||||
|
[node name="FR" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.8, -0.3, -1.5)
|
||||||
|
|
||||||
|
[node name="RL" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.8, -0.3, 1.5)
|
||||||
|
|
||||||
|
[node name="RR" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.8, -0.3, 1.5)
|
||||||
10
src/car_stats.gd
Normal file
10
src/car_stats.gd
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class_name CarStats
|
||||||
|
extends Resource
|
||||||
|
## One car = one folder under res://cars/ with a stats.tres (this resource).
|
||||||
|
## Optional car.glb next to it, referenced via model_path. No model = placeholder box.
|
||||||
|
|
||||||
|
@export var display_name := "Unnamed Shitbox"
|
||||||
|
@export var model_path := ""
|
||||||
|
@export var engine_power := 14.0 # forward accel, m/s^2-ish
|
||||||
|
@export var mass := 1100.0
|
||||||
|
@export var grip := 6.0 # lateral grip; lower = slidier
|
||||||
1
src/car_stats.gd.uid
Normal file
1
src/car_stats.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dtrpw74k6hge1
|
||||||
64
src/game.gd
Normal file
64
src/game.gd
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
class_name Game
|
||||||
|
extends Node3D
|
||||||
|
## Loads the chosen level, spawns the chosen car at the level's "Spawn" node,
|
||||||
|
## runs chase cam + HUD. Everything built in code -- game.tscn is just this script.
|
||||||
|
|
||||||
|
static var car_path := ""
|
||||||
|
static var level_path := ""
|
||||||
|
|
||||||
|
var car: Car
|
||||||
|
var cam: Camera3D
|
||||||
|
var speed_label: Label
|
||||||
|
var boost_bar: ProgressBar
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var level: Node = (load(level_path) as PackedScene).instantiate()
|
||||||
|
add_child(level)
|
||||||
|
|
||||||
|
car = (load("res://src/car.tscn") as PackedScene).instantiate()
|
||||||
|
car.stats = load(car_path)
|
||||||
|
add_child(car)
|
||||||
|
var spawn := level.find_child("Spawn", true, false)
|
||||||
|
if spawn is Node3D:
|
||||||
|
car.global_transform = spawn.global_transform
|
||||||
|
car.global_position += Vector3.UP * 0.6
|
||||||
|
|
||||||
|
cam = Camera3D.new()
|
||||||
|
cam.fov = 72.0
|
||||||
|
add_child(cam)
|
||||||
|
cam.global_position = car.global_position + Vector3(0, 3, 8)
|
||||||
|
|
||||||
|
var sun := DirectionalLight3D.new()
|
||||||
|
sun.rotation_degrees = Vector3(-55, 30, 0)
|
||||||
|
sun.shadow_enabled = true
|
||||||
|
add_child(sun)
|
||||||
|
var env := WorldEnvironment.new()
|
||||||
|
var e := Environment.new()
|
||||||
|
e.background_mode = Environment.BG_SKY
|
||||||
|
e.sky = Sky.new()
|
||||||
|
e.sky.sky_material = ProceduralSkyMaterial.new()
|
||||||
|
env.environment = e
|
||||||
|
add_child(env)
|
||||||
|
|
||||||
|
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(220, 18)
|
||||||
|
boost_bar.show_percentage = false
|
||||||
|
hud.add_child(boost_bar)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if Input.is_action_just_pressed("ui_cancel"):
|
||||||
|
get_tree().change_scene_to_file("res://src/main.tscn")
|
||||||
|
return
|
||||||
|
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)
|
||||||
|
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.value = car.boost * 100.0
|
||||||
1
src/game.gd.uid
Normal file
1
src/game.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ci8smv5mvb4fb
|
||||||
6
src/game.tscn
Normal file
6
src/game.tscn
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://src/game.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Game" type="Node3D"]
|
||||||
|
script = ExtResource("1")
|
||||||
31
src/main.gd
Normal file
31
src/main.gd
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
extends Control
|
||||||
|
## Menu: pick a car and a level from whatever folders exist, hit RACE.
|
||||||
|
|
||||||
|
@onready var car_list: ItemList = $VBox/HBox/Cars
|
||||||
|
@onready var level_list: ItemList = $VBox/HBox/Levels
|
||||||
|
|
||||||
|
var car_paths: Array = []
|
||||||
|
var level_paths: Array = []
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var cars := Registry.cars()
|
||||||
|
for id in cars:
|
||||||
|
var stats: CarStats = load(cars[id])
|
||||||
|
car_list.add_item(stats.display_name)
|
||||||
|
car_paths.append(cars[id])
|
||||||
|
var levels := Registry.levels()
|
||||||
|
for id in levels:
|
||||||
|
level_list.add_item(String(id).capitalize())
|
||||||
|
level_paths.append(levels[id])
|
||||||
|
if not car_paths.is_empty():
|
||||||
|
car_list.select(0)
|
||||||
|
if not level_paths.is_empty():
|
||||||
|
level_list.select(0)
|
||||||
|
$VBox/Start.pressed.connect(_start)
|
||||||
|
|
||||||
|
func _start() -> void:
|
||||||
|
if car_list.get_selected_items().is_empty() or level_list.get_selected_items().is_empty():
|
||||||
|
return
|
||||||
|
Game.car_path = car_paths[car_list.get_selected_items()[0]]
|
||||||
|
Game.level_path = level_paths[level_list.get_selected_items()[0]]
|
||||||
|
get_tree().change_scene_to_file("res://src/game.tscn")
|
||||||
1
src/main.gd.uid
Normal file
1
src/main.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://fsusisaqg5nu
|
||||||
36
src/main.tscn
Normal file
36
src/main.tscn
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://src/main.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Main" type="Control"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 40.0
|
||||||
|
offset_top = 40.0
|
||||||
|
offset_right = -40.0
|
||||||
|
offset_bottom = -40.0
|
||||||
|
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
|
||||||
|
|
||||||
|
[node name="HBox" type="HBoxContainer" parent="VBox"]
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_constants/separation = 20
|
||||||
|
|
||||||
|
[node name="Cars" type="ItemList" parent="VBox/HBox"]
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Levels" type="ItemList" parent="VBox/HBox"]
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Start" type="Button" parent="VBox"]
|
||||||
|
text = "RACE"
|
||||||
|
theme_override_font_sizes/font_size = 32
|
||||||
21
src/registry.gd
Normal file
21
src/registry.gd
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
class_name Registry
|
||||||
|
## Content discovery: a car is cars/<id>/stats.tres, a level is levels/<id>/level.glb (or .tscn).
|
||||||
|
## ponytail: DirAccess scan works in dev; revisit when we ship an exported .app (pck listing differs).
|
||||||
|
|
||||||
|
static func cars() -> Dictionary:
|
||||||
|
var out := {}
|
||||||
|
for dir in DirAccess.get_directories_at("res://cars"):
|
||||||
|
var p := "res://cars/%s/stats.tres" % dir
|
||||||
|
if ResourceLoader.exists(p):
|
||||||
|
out[dir] = p
|
||||||
|
return out
|
||||||
|
|
||||||
|
static func levels() -> Dictionary:
|
||||||
|
var out := {}
|
||||||
|
for dir in DirAccess.get_directories_at("res://levels"):
|
||||||
|
for f in ["level.tscn", "level.glb"]:
|
||||||
|
var p := "res://levels/%s/%s" % [dir, f]
|
||||||
|
if ResourceLoader.exists(p):
|
||||||
|
out[dir] = p
|
||||||
|
break
|
||||||
|
return out
|
||||||
1
src/registry.gd.uid
Normal file
1
src/registry.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dmhb80pfe8b1e
|
||||||
19
tests/screenshot.gd
Normal file
19
tests/screenshot.gd
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Boot straight into the game and save a viewport screenshot (needs a window, not --headless):
|
||||||
|
## /Applications/Godot.app/Contents/MacOS/Godot --path . --script tests/screenshot.gd
|
||||||
|
## Writes screenshot.png to the project dir. Used to eyeball the build without a human.
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run()
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
Game.car_path = Registry.cars().values()[0]
|
||||||
|
Game.level_path = Registry.levels().values()[0]
|
||||||
|
root.add_child((load("res://src/game.tscn") as PackedScene).instantiate())
|
||||||
|
await process_frame
|
||||||
|
Input.action_press("throttle")
|
||||||
|
for i in 90:
|
||||||
|
await physics_frame
|
||||||
|
root.get_texture().get_image().save_png("res://screenshot.png")
|
||||||
|
print("screenshot saved")
|
||||||
|
quit(0)
|
||||||
29
tests/smoke.gd
Normal file
29
tests/smoke.gd
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Headless smoke test:
|
||||||
|
## /Applications/Godot.app/Contents/MacOS/Godot --headless --path . --script tests/smoke.gd
|
||||||
|
## Registry finds content, game boots, car drives forward under throttle.
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run()
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var cars := Registry.cars()
|
||||||
|
var levels := Registry.levels()
|
||||||
|
assert(not cars.is_empty(), "no cars found")
|
||||||
|
assert(not levels.is_empty(), "no levels found")
|
||||||
|
for p in cars.values():
|
||||||
|
var s: CarStats = load(p)
|
||||||
|
assert(s != null and s.engine_power > 0.0, "bad stats: %s" % p)
|
||||||
|
Game.car_path = cars.values()[0]
|
||||||
|
Game.level_path = levels.values()[0]
|
||||||
|
var game: Game = (load("res://src/game.tscn") as PackedScene).instantiate()
|
||||||
|
root.add_child(game)
|
||||||
|
await process_frame # _ready doesn't run until the main loop starts
|
||||||
|
var start: Vector3 = game.car.global_position
|
||||||
|
Input.action_press("throttle")
|
||||||
|
for i in 180:
|
||||||
|
await physics_frame
|
||||||
|
var dist := start.distance_to(game.car.global_position)
|
||||||
|
assert(dist > 5.0, "car didn't move (%.1f m)" % dist)
|
||||||
|
print("smoke ok: %d cars, %d levels, drove %.1f m, %.0f km/h" % [cars.size(), levels.size(), dist, game.car.speed_kmh()])
|
||||||
|
quit(0)
|
||||||
1
tests/smoke.gd.uid
Normal file
1
tests/smoke.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cgx3wtstk4snq
|
||||||
Loading…
Reference in New Issue
Block a user