diff --git a/scenes/worlds/test_level.tscn b/scenes/worlds/test_level.tscn new file mode 100644 index 0000000..9935db9 --- /dev/null +++ b/scenes/worlds/test_level.tscn @@ -0,0 +1,70 @@ +[gd_scene load_steps=1 format=3] + +[node name="TestLevel" type="Node3D"] + +[node name="Ground" type="StaticBody3D" parent="."] + +[node name="GroundMesh" type="MeshInstance3D" parent="Ground"] +mesh = SubResource("mesh_ground") + +[node name="Props" type="Node3D" parent="."] + +[node name="Prop1" type="StaticBody3D" parent="Props"] + +[node name="Prop1Mesh" type="MeshInstance3D" parent="Props/Prop1"] +mesh = SubResource("mesh_box1") + +[node name="Prop2" type="StaticBody3D" parent="Props"] + +[node name="Prop2Mesh" type="MeshInstance3D" parent="Props/Prop2"] +mesh = SubResource("mesh_cylinder") + +[node name="Prop3" type="StaticBody3D" parent="Props"] + +[node name="Prop3Mesh" type="MeshInstance3D" parent="Props/Prop3"] +mesh = SubResource("mesh_sphere") + +[node name="Prop4" type="StaticBody3D" parent="Props"] + +[node name="Prop4Mesh" type="MeshInstance3D" parent="Props/Prop4"] +mesh = SubResource("mesh_box2") + +[node name="Prop5" type="StaticBody3D" parent="Props"] + +[node name="Prop5Mesh" type="MeshInstance3D" parent="Props/Prop5"] +mesh = SubResource("mesh_box3") + +[node name="Sun" type="DirectionalLight3D" parent="."] +rotation_degrees = Vector3(-45.0, 0.0, 0.0) +shadow_enabled = true + +[node name="WorldEnvironment" type="WorldEnvironment" parent="."] +environment = SubResource("env_bg") + +[node name="PlayerSpawn" type="Marker3D" parent="."] +position = Vector3(0, 1.0, 0) + +[sub_resource type="BoxMesh" id="mesh_ground"] +size = Vector3(40, 1, 40) + +[sub_resource type="BoxMesh" id="mesh_box1"] +size = Vector3(1.5, 2.0, 1.5) + +[sub_resource type="CylinderMesh" id="mesh_cylinder"] +top_radius = 0.5 +bottom_radius = 0.5 +height = 2.0 + +[sub_resource type="SphereMesh" id="mesh_sphere"] +radius = 0.75 + +[sub_resource type="BoxMesh" id="mesh_box2"] +size = Vector3(1.0, 3.0, 1.0) + +[sub_resource type="BoxMesh" id="mesh_box3"] +size = Vector3(2.0, 1.0, 2.0) + +[sub_resource type="Environment" id="env_bg"] +background_mode = 1 +background_color = Color(0.4, 0.6, 0.9, 1) +ambient_light_energy = 0.6 diff --git a/scripts/player.gd b/scripts/player.gd new file mode 100644 index 0000000..06a2f78 --- /dev/null +++ b/scripts/player.gd @@ -0,0 +1,111 @@ +class_name Player +extends CharacterBody3D +## The player character controller. +## +## A simple CharacterBody3D with WASD/arrow locomotion, gravity, and a +## first-person Camera3D. 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 + + +# --------------------------------------------------------------------------- +# Input state +# --------------------------------------------------------------------------- + +var _input_direction := Vector2.ZERO + + +# --------------------------------------------------------------------------- +# _ready — spawn at PlayerSpawn marker +# --------------------------------------------------------------------------- + +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. + var spawn := get_tree().current_scene.find_child("PlayerSpawn", true, false) + if spawn is Node3D: + global_position = spawn.global_position + # Align player forward with spawn yaw (optional polish). + var spawn_yaw := spawn.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) + + +# --------------------------------------------------------------------------- +# _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: + _update_input_direction(key) + else: + _clear_input_direction(key) + elif event is InputEventMouseMotion: + _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: + rotation.y -= event.relative.x * 0.002 + + +# --------------------------------------------------------------------------- +# 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 + return + + # Build movement direction from input. + 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()