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