Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8be962248d | |||
| 7790670559 | |||
| e2a77b055f | |||
| 5966fa9044 | |||
| 945c498a31 | |||
| be0acb1862 | |||
| 8118f3074b | |||
| 5803f47069 | |||
| e33b91d976 | |||
| fdc43bd5f1 | |||
| 406ff3457e | |||
| 3f7acf857c | |||
| 6648b2bba5 | |||
| f70b5b2ee5 | |||
| 2fa7b1f475 | |||
| 7f753cb1b8 |
@@ -0,0 +1,10 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://game_scene_pc2"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://c49a36fc7db_test_level" path="res://scenes/worlds/test_level.tscn" id="1_level"]
|
||||
[ext_resource type="PackedScene" uid="uid://player_scene_001" path="res://scenes/player.tscn" id="2_player"]
|
||||
|
||||
[node name="Game" type="Node3D"]
|
||||
|
||||
[node name="Level" parent="." instance=ExtResource("1_level")]
|
||||
|
||||
[node name="Player" parent="." instance=ExtResource("2_player")]
|
||||
@@ -0,0 +1,24 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://player_scene_001"]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/player.gd" id="1_player"]
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh1"]
|
||||
radius = 0.35
|
||||
height = 1.6
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape1"]
|
||||
radius = 0.35
|
||||
height = 1.6
|
||||
|
||||
[node name="Player" type="CharacterBody3D"]
|
||||
script = ExtResource("1_player")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("CapsuleShape1")
|
||||
|
||||
[node name="CapsuleMesh" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("CapsuleMesh1")
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||
current = true
|
||||
@@ -0,0 +1,120 @@
|
||||
[gd_scene load_steps=1 format=3 uid="uid://c49a36fc7db_test_level"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ground"]
|
||||
size = Vector3(40, 1, 40)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ground"]
|
||||
size = Vector3(40, 1, 40)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_prop1"]
|
||||
size = Vector3(2, 3, 2)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_prop1"]
|
||||
size = Vector3(2, 3, 2)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_prop2"]
|
||||
size = Vector3(3, 1, 3)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_prop2"]
|
||||
size = Vector3(3, 1, 3)
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_prop3"]
|
||||
top_radius = 1.0
|
||||
height = 4.0
|
||||
|
||||
[sub_resource type="CylinderMesh" id="CylinderMesh_prop3"]
|
||||
top_radius = 1.0
|
||||
bottom_radius = 1.0
|
||||
height = 4.0
|
||||
subdivisions_top_radius = 8
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_prop4"]
|
||||
radius = 1.0
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_prop4"]
|
||||
radius = 1.0
|
||||
subdivisions_ring = 12
|
||||
subdivisions_height = 12
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_prop5"]
|
||||
radius = 0.5
|
||||
|
||||
[sub_resource type="SphereMesh" id="SphereMesh_prop5"]
|
||||
radius = 0.5
|
||||
subdivisions_ring = 8
|
||||
subdivisions_height = 8
|
||||
|
||||
[sub_resource type="Environment" id="WorldEnvironment_env"]
|
||||
background_mode = 1
|
||||
background_color = Color(0.4, 0.6, 0.8, 1)
|
||||
ambient_light_source = 1
|
||||
ambient_light_energy = 0.4
|
||||
ambient_light_sky_energy = 1.0
|
||||
|
||||
[node name="TestLevel" type="Node3D"]
|
||||
|
||||
[node name="Ground" type="StaticBody3D" parent="."]
|
||||
position = Vector3(0, -0.5, 0)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ground"]
|
||||
shape = SubResource("BoxShape3D_ground")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Ground"]
|
||||
mesh = SubResource("BoxMesh_ground")
|
||||
|
||||
[node name="Props" type="Node3D" parent="."]
|
||||
|
||||
[node name="Prop1_Box" type="StaticBody3D" parent="Props"]
|
||||
position = Vector3(8, 1.5, 8)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop1_Box"]
|
||||
shape = SubResource("BoxShape3D_prop1")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop1_Box"]
|
||||
mesh = SubResource("BoxMesh_prop1")
|
||||
|
||||
[node name="Prop2_Pedestal" type="StaticBody3D" parent="Props"]
|
||||
position = Vector3(-8, 0.5, 6)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop2_Pedestal"]
|
||||
shape = SubResource("BoxShape3D_prop2")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop2_Pedestal"]
|
||||
mesh = SubResource("BoxMesh_prop2")
|
||||
|
||||
[node name="Prop3_Column" type="StaticBody3D" parent="Props"]
|
||||
position = Vector3(6, 2.0, -8)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop3_Column"]
|
||||
shape = SubResource("CylinderShape3D_prop3")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop3_Column"]
|
||||
mesh = SubResource("CylinderMesh_prop3")
|
||||
|
||||
[node name="Prop4_Sphere" type="StaticBody3D" parent="Props"]
|
||||
position = Vector3(-6, 1.0, -6)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop4_Sphere"]
|
||||
shape = SubResource("SphereShape3D_prop4")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop4_Sphere"]
|
||||
mesh = SubResource("SphereMesh_prop4")
|
||||
|
||||
[node name="Prop5_SmallSphere" type="StaticBody3D" parent="Props"]
|
||||
position = Vector3(0, 0.5, 12)
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Props/Prop5_SmallSphere"]
|
||||
shape = SubResource("SphereShape3D_prop5")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Props/Prop5_SmallSphere"]
|
||||
mesh = SubResource("SphereMesh_prop5")
|
||||
|
||||
[node name="Sun" type="DirectionalLight3D" parent="."]
|
||||
rotation = Vector3(-0.785398, 0, 0)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("WorldEnvironment_env")
|
||||
|
||||
[node name="PlayerSpawn" type="Marker3D" parent="."]
|
||||
position = Vector3(0, 1.5, 0)
|
||||
@@ -0,0 +1,148 @@
|
||||
class_name Player
|
||||
extends CharacterBody3D
|
||||
## The player character controller.
|
||||
##
|
||||
## A simple CharacterBody3D with WASD/arrow locomotion, gravity, jump, and a
|
||||
## first-person Camera3D with mouse-look. Reads the spawn point from the
|
||||
## current scene's `PlayerSpawn` Marker3D (see docs/architecture/pc2_test_level.md §3).
|
||||
##
|
||||
## No networking, no chunk code, no avatar UI — PC2 is local-only.
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Movement constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const WALK_SPEED: float = 5.0
|
||||
const JUMP_VELOCITY: float = 4.5
|
||||
const GRAVITY: float = 9.8
|
||||
|
||||
## Mouse-look sensitivity (radians per pixel of relative motion).
|
||||
const MOUSE_SENSITIVITY: float = 0.003
|
||||
|
||||
## Camera pitch is clamped so the player can't flip past looking straight
|
||||
## up/down (which would otherwise let mouse-look invert on itself).
|
||||
const PITCH_LIMIT: float = deg_to_rad(89.0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Input state
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var _input_direction := Vector2.ZERO
|
||||
var _camera_pitch: float = 0.0
|
||||
|
||||
@onready var _camera: Camera3D = $Camera3D
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _ready — spawn at PlayerSpawn marker, capture the mouse for look
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
# Per §3 of pc2_test_level.md: look up PlayerSpawn from the current
|
||||
# scene root. The lookup walks the full subtree (recursive=true) so
|
||||
# it finds the marker whether it is a direct child of the level or
|
||||
# nested deeper.
|
||||
#
|
||||
# current_scene is null in headless --script mode and in any scene
|
||||
# before the main scene is fully swapped in, so guard it rather than
|
||||
# blindly dereferencing.
|
||||
var current_scene: Node = get_tree().current_scene
|
||||
var spawn: Node = null
|
||||
if current_scene != null:
|
||||
spawn = current_scene.find_child("PlayerSpawn", true, false)
|
||||
|
||||
if spawn is Node3D:
|
||||
global_position = (spawn as Node3D).global_position
|
||||
# Align player forward with spawn yaw (optional polish).
|
||||
var spawn_yaw := (spawn as Node3D).rotation.y
|
||||
rotation.y = spawn_yaw
|
||||
else:
|
||||
push_warning("player.gd: no PlayerSpawn marker found; spawning at origin")
|
||||
global_position = Vector3(0, 1.0, 0)
|
||||
|
||||
# Capture the mouse for FPS-style look. Headless/test runs have no real
|
||||
# window to capture input for, so guard against that rather than error.
|
||||
if DisplayServer.get_name() != "headless":
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _unhandled_input — capture keyboard / mouse for movement
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event is InputEventKey:
|
||||
var key := event as InputEventKey
|
||||
if key.pressed and not key.echo:
|
||||
_update_input_direction(key)
|
||||
elif not key.pressed:
|
||||
_clear_input_direction(key)
|
||||
# Release the mouse so the player can get back to a menu/cursor.
|
||||
if key.pressed and key.keycode == KEY_ESCAPE:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
elif event is InputEventMouseButton and event.pressed:
|
||||
# Clicking back into the viewport re-captures the mouse.
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE and DisplayServer.get_name() != "headless":
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
elif event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
_handle_mouse_look(event as InputEventMouseMotion)
|
||||
|
||||
|
||||
func _update_input_direction(key: InputEventKey) -> void:
|
||||
match key.keycode:
|
||||
KEY_W, KEY_UP:
|
||||
_input_direction.y += 1.0
|
||||
KEY_S, KEY_DOWN:
|
||||
_input_direction.y -= 1.0
|
||||
KEY_A, KEY_LEFT:
|
||||
_input_direction.x -= 1.0
|
||||
KEY_D, KEY_RIGHT:
|
||||
_input_direction.x += 1.0
|
||||
|
||||
|
||||
func _clear_input_direction(key: InputEventKey) -> void:
|
||||
match key.keycode:
|
||||
KEY_W, KEY_UP:
|
||||
_input_direction.y -= 1.0
|
||||
KEY_S, KEY_DOWN:
|
||||
_input_direction.y += 1.0
|
||||
KEY_A, KEY_LEFT:
|
||||
_input_direction.x += 1.0
|
||||
KEY_D, KEY_RIGHT:
|
||||
_input_direction.x -= 1.0
|
||||
|
||||
|
||||
func _handle_mouse_look(event: InputEventMouseMotion) -> void:
|
||||
# Yaw rotates the whole body (so movement direction turns with the
|
||||
# look direction); pitch only tilts the camera, so the capsule itself
|
||||
# never tips over.
|
||||
rotation.y -= event.relative.x * MOUSE_SENSITIVITY
|
||||
_camera_pitch = clampf(_camera_pitch - event.relative.y * MOUSE_SENSITIVITY, -PITCH_LIMIT, PITCH_LIMIT)
|
||||
_camera.rotation.x = _camera_pitch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Physics — gravity, jump, move_and_slide
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# Apply gravity.
|
||||
if not is_on_floor():
|
||||
velocity.y -= GRAVITY * delta
|
||||
|
||||
# Jump.
|
||||
if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
|
||||
velocity.y = JUMP_VELOCITY
|
||||
|
||||
# Build movement direction from input. Godot's local "forward" is -Z, not
|
||||
# +Z, so W (input.y = +1) must use -basis.z -- using +basis.z here would
|
||||
# move the player backward when pressing forward (confirmed by reading
|
||||
# Camera3D/Node3D's own convention: a node looks down its local -Z).
|
||||
var direction := (_input_direction.x * transform.basis.x -
|
||||
_input_direction.y * transform.basis.z).normalized()
|
||||
velocity.x = direction.x * WALK_SPEED
|
||||
velocity.z = direction.z * WALK_SPEED
|
||||
|
||||
move_and_slide()
|
||||
@@ -0,0 +1,93 @@
|
||||
extends SceneTree
|
||||
## Smoke test for the player character scene and its spawn behavior.
|
||||
##
|
||||
## Run with:
|
||||
## godot --headless --path . --script res://scripts/smoke_test_player.gd
|
||||
##
|
||||
## Loads the test level, verifies the player is present, spawned at the
|
||||
## PlayerSpawn marker, and has the expected CharacterBody3D structure.
|
||||
|
||||
var _passed := 0
|
||||
var _failed := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
print("SMOKE: starting player smoke test")
|
||||
_run()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
await process_frame
|
||||
print("SMOKE: loading game scene")
|
||||
|
||||
# Load scenes/game.tscn -- the real PC2 composition point (Level + Player
|
||||
# as siblings, per docs/architecture/pc2_test_level.md §4), not the level
|
||||
# scene directly. Use the same SceneTree API as the real game so
|
||||
# `current_scene` is populated before the scene is added and its
|
||||
# _ready() callbacks fire. That ordering is what the player's spawn
|
||||
# lookup (get_tree().current_scene) depends on.
|
||||
var packed: PackedScene = load("res://scenes/game.tscn")
|
||||
var result := change_scene_to_packed(packed)
|
||||
if result != OK:
|
||||
printerr("SMOKE: failed to change scene, error ", result)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
await scene_changed
|
||||
var game: Node3D = current_scene as Node3D
|
||||
print("SMOKE: game scene ready")
|
||||
|
||||
# 1. Verify the player node exists in the scene tree.
|
||||
var player: Node = game.get_node("Player")
|
||||
_check("Player node exists in test level", player != null)
|
||||
_check("Player is a CharacterBody3D", player is CharacterBody3D)
|
||||
|
||||
# 2. Verify the player has the expected child structure.
|
||||
var collision: Node = player.get_node("CollisionShape3D")
|
||||
_check("Player has CollisionShape3D", collision != null)
|
||||
_check("CollisionShape3D has a shape",
|
||||
collision is CollisionShape3D and (collision as CollisionShape3D).shape != null)
|
||||
|
||||
var mesh: Node = player.get_node("CapsuleMesh")
|
||||
_check("Player has CapsuleMesh", mesh != null)
|
||||
_check("CapsuleMesh has a mesh",
|
||||
mesh is MeshInstance3D and (mesh as MeshInstance3D).mesh != null)
|
||||
|
||||
# 3. Verify the player spawned at the PlayerSpawn marker position.
|
||||
var spawn_marker: Marker3D = game.get_node("Level/PlayerSpawn")
|
||||
_check("PlayerSpawn marker exists", spawn_marker != null)
|
||||
|
||||
# The player's _ready() should have moved it to the spawn marker's
|
||||
# global position. Compare global positions directly (not local) since
|
||||
# Player and Level sit at different depths under Game.
|
||||
var expected_pos: Vector3 = spawn_marker.global_position
|
||||
_check("Player spawned at PlayerSpawn position",
|
||||
player.global_position == expected_pos)
|
||||
|
||||
# 4. Verify the player script class_name is correct and has the
|
||||
# movement constants defined.
|
||||
var script: GDScript = player.get_script() as GDScript
|
||||
_check("Player has a script attached", script != null)
|
||||
var constants: Dictionary = script.get_script_constant_map()
|
||||
_check("Player has WALK_SPEED constant", constants.has("WALK_SPEED"))
|
||||
_check("Player has GRAVITY constant", constants.has("GRAVITY"))
|
||||
_check("Player has JUMP_VELOCITY constant", constants.has("JUMP_VELOCITY"))
|
||||
_check("JUMP_VELOCITY is positive (upward)", float(constants.get("JUMP_VELOCITY", -1.0)) > 0.0)
|
||||
|
||||
# 5. Verify the player has a Camera3D so the game is actually viewable
|
||||
# from the player's perspective, not just a walking capsule with no view.
|
||||
var camera: Node = player.get_node_or_null("Camera3D")
|
||||
_check("Player has a Camera3D", camera is Camera3D)
|
||||
_check("Camera3D is current", camera is Camera3D and (camera as Camera3D).current)
|
||||
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [_passed, _passed + _failed])
|
||||
quit(0 if _failed == 0 else 1)
|
||||
|
||||
|
||||
func _check(description: String, condition: bool) -> void:
|
||||
if condition:
|
||||
print(" PASS: ", description)
|
||||
_passed += 1
|
||||
else:
|
||||
printerr(" FAIL: ", description)
|
||||
_failed += 1
|
||||
@@ -0,0 +1,125 @@
|
||||
extends SceneTree
|
||||
## Headless smoke test for the PC2 test level scene.
|
||||
##
|
||||
## Run with:
|
||||
## godot --headless --path . --script res://scripts/smoke_test_test_level.gd
|
||||
##
|
||||
## Loads the test level scene, asserts:
|
||||
## (a) it loads without error,
|
||||
## (b) it contains a Marker3D named "PlayerSpawn",
|
||||
## (c) PlayerSpawn is positioned >= 1.0 m above the ground top surface.
|
||||
## Exits 0 on success, 1 on failure.
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var level_path: String = "res://scenes/worlds/test_level.tscn"
|
||||
|
||||
# Load the scene file
|
||||
var level_resource: PackedScene = load(level_path)
|
||||
if level_resource == null:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 could not load ", level_path)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("SMOKE: test_level.tscn loaded successfully")
|
||||
|
||||
# Change the running scene to our test level
|
||||
var err: int = change_scene_to_packed(level_resource)
|
||||
if err != OK:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 change_scene_to_packed returned ", err)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Wait for the scene_changed signal so current_scene is set
|
||||
await scene_changed
|
||||
|
||||
var level_instance: Node3D = current_scene as Node3D
|
||||
if level_instance == null:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 current_scene is not Node3D")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Give the tree one frame to fully initialize
|
||||
await process_frame
|
||||
|
||||
# (b) Find the PlayerSpawn marker
|
||||
var spawn_marker: Node3D = level_instance.find_child("PlayerSpawn", true, false) as Node3D
|
||||
if spawn_marker == null:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 no Node3D named 'PlayerSpawn' found in test level")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("SMOKE: found PlayerSpawn at ", spawn_marker.global_position)
|
||||
|
||||
# (c) Verify PlayerSpawn is >= 1.0 m above ground top
|
||||
# The ground top surface is at y = 0 (Ground StaticBody3D centered at y = -0.5
|
||||
# with height 1.0, so top face is at y = 0).
|
||||
var spawn_y: float = spawn_marker.global_position.y
|
||||
if spawn_y < 1.0:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 PlayerSpawn y = ", spawn_y,
|
||||
" is below the required minimum of 1.0 m above ground top (y=0)")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("SMOKE: PlayerSpawn y = ", spawn_y, " >= 1.0 m \u2014 PASS")
|
||||
|
||||
# Additional check: verify Ground StaticBody3D exists
|
||||
var ground: StaticBody3D = level_instance.find_child("Ground", true, false) as StaticBody3D
|
||||
if ground == null:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 no StaticBody3D named 'Ground' found")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("SMOKE: found Ground StaticBody3D")
|
||||
|
||||
# Verify Ground top face is at y = 0
|
||||
var ground_shape: CollisionShape3D = ground.find_child("CollisionShape3D", true, false) as CollisionShape3D
|
||||
if ground_shape != null and ground_shape.shape is BoxShape3D:
|
||||
var box_shape: BoxShape3D = ground_shape.shape as BoxShape3D
|
||||
var ground_top: float = ground.global_position.y + box_shape.size.y / 2.0
|
||||
if abs(ground_top - 0.0) > 0.01:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 Ground top face at y = ", ground_top, " expected ~0.0")
|
||||
quit(1)
|
||||
return
|
||||
print("SMOKE: Ground top face at y = ", ground_top, " \u2014 PASS")
|
||||
|
||||
# Verify props exist (3-5 StaticBody3D children under Props node)
|
||||
var props_node: Node3D = level_instance.find_child("Props", true, false) as Node3D
|
||||
if props_node != null:
|
||||
var prop_count: int = 0
|
||||
for child in props_node.get_children():
|
||||
if child is StaticBody3D:
|
||||
prop_count += 1
|
||||
if prop_count < 3:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 only ", prop_count, " props found, need >= 3")
|
||||
quit(1)
|
||||
return
|
||||
print("SMOKE: found ", prop_count, " props \u2014 PASS")
|
||||
else:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 no 'Props' Node3D found")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# Verify lighting exists
|
||||
var sun: DirectionalLight3D = level_instance.find_child("Sun", true, false) as DirectionalLight3D
|
||||
if sun == null:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 no DirectionalLight3D named 'Sun' found")
|
||||
quit(1)
|
||||
return
|
||||
print("SMOKE: found Sun DirectionalLight3D")
|
||||
|
||||
# Verify WorldEnvironment exists
|
||||
var env: WorldEnvironment = level_instance.find_child("WorldEnvironment", true, false) as WorldEnvironment
|
||||
if env == null:
|
||||
printerr("SMOKE RESULT: FAIL \u2014 no WorldEnvironment named 'WorldEnvironment' found")
|
||||
quit(1)
|
||||
return
|
||||
print("SMOKE: found WorldEnvironment")
|
||||
|
||||
# All checks passed
|
||||
print("SMOKE RESULT: PASS")
|
||||
quit(0)
|
||||
Reference in New Issue
Block a user