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.find_child("Camera3D", true, false) _check("Player has a Camera3D", camera is Camera3D) _check("Camera3D is current", camera is Camera3D and (camera as Camera3D).current) # Third-person rig: the camera hangs off a SpringArm3D (so it pulls in # when a wall is behind the player) with real length. var arm: Node = player.find_child("SpringArm3D", true, false) _check("Camera is on a SpringArm3D (third-person)", arm is SpringArm3D) _check("SpringArm has real length", arm is SpringArm3D and (arm as SpringArm3D).spring_length >= 2.0) _check("Camera is a descendant of the SpringArm", camera != null and arm != null and arm.is_ancestor_of(camera)) 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