diff --git a/game/scenes/Characters/repo_bot.gd b/game/scenes/Characters/repo_bot.gd new file mode 100644 index 0000000..7c77eb7 --- /dev/null +++ b/game/scenes/Characters/repo_bot.gd @@ -0,0 +1,156 @@ +extends Node3D + +@export var left_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeLeft/Pupil") +@export var right_pupil_path: NodePath = NodePath("Body/HeadPivot/EyeRight/Pupil") +@export var camera_path: NodePath +@export var look_origin_path: NodePath = NodePath("Body/HeadPivot") +@export var look_reference_path: NodePath = NodePath("Body") +@export var lock_vertical: bool = true +@export var vertical_unlock_height: float = 0.6 +@export var vertical_lock_smooth_speed: float = 6.0 +@export var vertical_lock_hold_time: float = 0.3 +@export var max_look_angle_deg: float = 90.0 +@export var eye_return_speed: float = 0.2 +@export var max_offset: float = 0.08 +@export var side_eye_boost: float = 1.4 +@export var head_path: NodePath = NodePath("Body/HeadPivot") +@export var head_turn_speed: float = 16.0 +@export var head_max_yaw_deg: float = 55.0 +@export var head_max_pitch_deg: float = 22.0 + +var _left_pupil: Node3D +var _right_pupil: Node3D +var _left_base: Vector3 +var _right_base: Vector3 +var _camera: Camera3D +var _look_origin: Node3D +var _head: Node3D +var _head_base_rot: Vector3 +var _vertical_lock_factor: float = 1.0 +var _vertical_hold_timer: float = 0.0 +var _look_reference: Node3D + + +func _ready() -> void: + _left_pupil = get_node_or_null(left_pupil_path) as Node3D + _right_pupil = get_node_or_null(right_pupil_path) as Node3D + if _left_pupil: + _left_base = _left_pupil.position + if _right_pupil: + _right_base = _right_pupil.position + _camera = _resolve_camera() + _look_origin = get_node_or_null(look_origin_path) as Node3D + _look_reference = get_node_or_null(look_reference_path) as Node3D + _head = get_node_or_null(head_path) as Node3D + if _head: + _head_base_rot = _head.rotation + + +func _physics_process(_delta: float) -> void: + _update_pupils() + + +func _process(_delta: float) -> void: + _update_pupils() + + +func _update_pupils() -> void: + if _camera == null or not _camera.is_inside_tree(): + _camera = _resolve_camera() + if _camera == null: + return + var origin := _look_origin + if origin == null: + origin = self + var target := _camera.global_position + var dir_world := target - origin.global_position + if dir_world.length_squared() <= 0.0001: + return + dir_world = dir_world.normalized() + var reference := _look_reference if _look_reference != null else origin + var forward := -reference.global_basis.z + var min_dot := cos(deg_to_rad(max_look_angle_deg)) + var can_look := dir_world.dot(forward) >= min_dot + if not can_look: + var delta := get_process_delta_time() + if _left_pupil: + var target_left := Vector3(_left_base.x, _left_base.y, _left_base.z) + var pos_left := _left_pupil.position + pos_left.x = move_toward(pos_left.x, target_left.x, eye_return_speed * delta) + pos_left.y = target_left.y + pos_left.z = move_toward(pos_left.z, target_left.z, eye_return_speed * delta) + _left_pupil.position = pos_left + if _right_pupil: + var target_right := Vector3(_right_base.x, _right_base.y, _right_base.z) + var pos_right := _right_pupil.position + pos_right.x = move_toward(pos_right.x, target_right.x, eye_return_speed * delta) + pos_right.y = target_right.y + pos_right.z = move_toward(pos_right.z, target_right.z, eye_return_speed * delta) + _right_pupil.position = pos_right + if _head: + _head.rotation.x = _head_base_rot.x + _head.rotation.y = lerp_angle(_head.rotation.y, _head_base_rot.y, head_turn_speed * delta) + return + if lock_vertical: + var origin_y := origin.global_position.y + var target_offset := target.y - origin_y + var is_above := target_offset > vertical_unlock_height + if is_above: + _vertical_hold_timer = vertical_lock_hold_time + else: + _vertical_hold_timer = max(0.0, _vertical_hold_timer - get_process_delta_time()) + if is_above: + _vertical_lock_factor = 1.0 + else: + var unlock := 1.0 if _vertical_hold_timer > 0.0 else 0.0 + _vertical_lock_factor = move_toward(_vertical_lock_factor, unlock, vertical_lock_smooth_speed * get_process_delta_time()) + target.y = lerp(origin_y, target.y, _vertical_lock_factor) + dir_world = target - origin.global_position + if dir_world.length_squared() <= 0.0001: + return + dir_world = dir_world.normalized() + if _left_pupil: + _update_eye(_left_pupil, _left_base, dir_world) + if _right_pupil: + _update_eye(_right_pupil, _right_base, dir_world) + if _head: + _update_head(dir_world) + + +func _resolve_camera() -> Camera3D: + if camera_path != NodePath(""): + var from_path := get_node_or_null(camera_path) as Camera3D + if from_path: + return from_path + var viewport_cam := get_viewport().get_camera_3d() + if viewport_cam: + return viewport_cam + var by_name := get_tree().get_root().find_child("Camera3D", true, false) as Camera3D + return by_name + + +func _update_eye(eye: Node3D, base_pos: Vector3, dir_world: Vector3) -> void: + var parent := eye.get_parent() as Node3D + if parent == null: + return + var dir_local := parent.global_basis.inverse() * dir_world + var flat := Vector2(dir_local.x, dir_local.y) + flat.x *= side_eye_boost + if flat.length() > 1.0: + flat = flat.normalized() + var offset := Vector3(flat.x, flat.y, 0.0) * max_offset + eye.position = base_pos + offset + + +func _update_head(dir_world: Vector3) -> void: + var parent := _head.get_parent() as Node3D + if parent == null: + return + var dir_local := parent.global_basis.inverse() * dir_world + var yaw := atan2(-dir_local.x, -dir_local.z) + var pitch := atan2(dir_local.y, -dir_local.z) + yaw = clamp(yaw, deg_to_rad(-head_max_yaw_deg), deg_to_rad(head_max_yaw_deg)) + pitch = clamp(pitch, deg_to_rad(-head_max_pitch_deg), deg_to_rad(head_max_pitch_deg)) + var target := Vector3(_head_base_rot.x + pitch, _head_base_rot.y + yaw, _head_base_rot.z) + _head.rotation.x = lerp_angle(_head.rotation.x, target.x, head_turn_speed * get_process_delta_time()) + _head.rotation.y = lerp_angle(_head.rotation.y, target.y, head_turn_speed * get_process_delta_time()) diff --git a/game/scenes/Characters/repo_bot.gd.uid b/game/scenes/Characters/repo_bot.gd.uid new file mode 100644 index 0000000..a21e00c --- /dev/null +++ b/game/scenes/Characters/repo_bot.gd.uid @@ -0,0 +1 @@ +uid://bs3eqqujhetsm diff --git a/game/scenes/Characters/repo_bot.tscn b/game/scenes/Characters/repo_bot.tscn new file mode 100644 index 0000000..edd725d --- /dev/null +++ b/game/scenes/Characters/repo_bot.tscn @@ -0,0 +1,111 @@ +[gd_scene load_steps=14 format=3] + +[ext_resource type="Script" path="res://scenes/Characters/repo_bot.gd" id="1_repo_bot"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_body"] +albedo_color = Color(0.78, 0.8, 0.82, 1) +metallic = 0.2 +roughness = 0.35 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_accent"] +albedo_color = Color(0.25, 0.82, 0.55, 1) +emission_enabled = true +emission = Color(0.25, 0.82, 0.55, 1) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_eye_white"] +albedo_color = Color(0.95, 0.95, 0.95, 1) +roughness = 0.2 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pupil"] +albedo_color = Color(0.02, 0.02, 0.02, 1) +roughness = 0.8 + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_body"] +radius = 0.25 +height = 0.6 +material = SubResource("StandardMaterial3D_body") + +[sub_resource type="SphereMesh" id="SphereMesh_head"] +radius = 0.22 +height = 0.44 +material = SubResource("StandardMaterial3D_body") + +[sub_resource type="SphereMesh" id="SphereMesh_eye_white"] +radius = 0.075 +height = 0.15 +material = SubResource("StandardMaterial3D_eye_white") + +[sub_resource type="SphereMesh" id="SphereMesh_pupil"] +radius = 0.028 +height = 0.056 +material = SubResource("StandardMaterial3D_pupil") + +[sub_resource type="CylinderMesh" id="CylinderMesh_limb"] +top_radius = 0.06 +bottom_radius = 0.06 +height = 0.35 +material = SubResource("StandardMaterial3D_body") + +[sub_resource type="BoxMesh" id="BoxMesh_pack"] +size = Vector3(0.26, 0.3, 0.12) +material = SubResource("StandardMaterial3D_accent") + +[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"] +radius = 0.3 +height = 1.1 + +[node name="RepoBot" type="Node3D"] +script = ExtResource("1_repo_bot") + +[node name="Body" type="StaticBody3D" parent="."] + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.55, 0) +shape = SubResource("CapsuleShape3D_body") + +[node name="Torso" type="MeshInstance3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) +mesh = SubResource("CapsuleMesh_body") + +[node name="HeadPivot" type="Node3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.95, 0) + +[node name="Head" type="MeshInstance3D" parent="Body/HeadPivot"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) +mesh = SubResource("SphereMesh_head") + +[node name="EyeLeft" type="MeshInstance3D" parent="Body/HeadPivot"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.09, 0, -0.205) +mesh = SubResource("SphereMesh_eye_white") + +[node name="Pupil" type="MeshInstance3D" parent="Body/HeadPivot/EyeLeft"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.06) +mesh = SubResource("SphereMesh_pupil") + +[node name="EyeRight" type="MeshInstance3D" parent="Body/HeadPivot"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.09, 0, -0.205) +mesh = SubResource("SphereMesh_eye_white") + +[node name="Pupil" type="MeshInstance3D" parent="Body/HeadPivot/EyeRight"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.06) +mesh = SubResource("SphereMesh_pupil") + +[node name="ArmLeft" type="MeshInstance3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.32, 0.55, 0) +mesh = SubResource("CylinderMesh_limb") + +[node name="ArmRight" type="MeshInstance3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.32, 0.55, 0) +mesh = SubResource("CylinderMesh_limb") + +[node name="LegLeft" type="MeshInstance3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.12, 0.15, 0) +mesh = SubResource("CylinderMesh_limb") + +[node name="LegRight" type="MeshInstance3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.12, 0.15, 0) +mesh = SubResource("CylinderMesh_limb") + +[node name="Backpack" type="MeshInstance3D" parent="Body"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0.22) +mesh = SubResource("BoxMesh_pack") diff --git a/game/scenes/Levels/level.tscn b/game/scenes/Levels/level.tscn index 661efa5..f4e511c 100644 --- a/game/scenes/Levels/level.tscn +++ b/game/scenes/Levels/level.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=12 format=3 uid="uid://dchj6g2i8ebph"] +[gd_scene load_steps=13 format=3 uid="uid://dchj6g2i8ebph"] [ext_resource type="PackedScene" uid="uid://bb6hj6l23043x" path="res://assets/models/human.blend" id="1_eg4yq"] [ext_resource type="Script" uid="uid://bpxggc8nr6tf6" path="res://scenes/player.gd" id="1_muv8p"] -[ext_resource type="PackedScene" uid="uid://c5of6aaxop1hl" path="res://scenes/block.tscn" id="2_tc7dm"] +[ext_resource type="PackedScene" uid="uid://bcwmsmb3jum7j" path="res://scenes/block.tscn" id="2_tc7dm"] [ext_resource type="Script" uid="uid://b7fopt7sx74g8" path="res://scenes/Levels/menu.gd" id="3_tc7dm"] +[ext_resource type="PackedScene" path="res://scenes/Characters/repo_bot.tscn" id="4_repo"] [sub_resource type="PhysicsMaterial" id="PhysicsMaterial_2q6dc"] bounce = 0.5 @@ -27,6 +28,9 @@ size = Vector3(1080, 2, 1080) [node name="human" parent="." instance=ExtResource("1_eg4yq")] +[node name="RepoBot" parent="." instance=ExtResource("4_repo")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.9426608, 0, -4.4451966) + [node name="Thing" type="RigidBody3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -3.7986288) physics_material_override = SubResource("PhysicsMaterial_2q6dc") @@ -52,6 +56,7 @@ shape = SubResource("SphereShape3D_mx8sn") [node name="Camera3D" type="Camera3D" parent="Player"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.31670225, 0) +current = true [node name="SpotLight3D" type="SpotLight3D" parent="Player"] diff --git a/game/scenes/player.gd b/game/scenes/player.gd index 54e72c4..90f7398 100644 --- a/game/scenes/player.gd +++ b/game/scenes/player.gd @@ -17,6 +17,14 @@ var rotation_x := 0.0 var rotation_y := 0.0 var cameraMoveMode := false var current_number_of_jumps := 0 +var _pending_mouse_delta := Vector2.ZERO +var _last_move_forward := Vector3(0, 0, 1) +var _last_move_right := Vector3(1, 0, 0) +var _camera_offset_local := Vector3.ZERO +var _camera_yaw := 0.0 +var _camera_pitch := 0.0 + +@export var camera_follow_speed := 10.0 var jump_sound = preload("res://assets/audio/jump.ogg") var audio_player = AudioStreamPlayer.new() @@ -33,8 +41,32 @@ func _ready() -> void: add_child(audio_player) audio_player.stream = jump_sound audio_player.volume_db = -20 + if cam: + _camera_offset_local = cam.transform.origin + _camera_pitch = cam.rotation.x + _camera_yaw = global_transform.basis.get_euler().y + cam.set_as_top_level(true) + cam.global_position = global_position + (Basis(Vector3.UP, _camera_yaw) * _camera_offset_local) + cam.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0) + var move_basis := cam.global_transform.basis if cam else global_transform.basis + var forward := move_basis.z + var right := move_basis.x + forward.y = 0.0 + right.y = 0.0 + if forward.length() > 0.0001: + _last_move_forward = forward.normalized() + if right.length() > 0.0001: + _last_move_right = right.normalized() func _integrate_forces(state): + if cameraMoveMode and _pending_mouse_delta != Vector2.ZERO: + rotation_x -= _pending_mouse_delta.y * mouse_sensitivity + rotation_y -= _pending_mouse_delta.x * mouse_sensitivity + rotation_x = clamp(rotation_x, deg_to_rad(-90), deg_to_rad(90)) # Prevent flipping + _camera_pitch = rotation_x + rotation.y = rotation_y + _pending_mouse_delta = Vector2.ZERO + # Input as 2D vector var input2v := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") @@ -44,13 +76,19 @@ func _integrate_forces(state): if cam: forward = cam.global_transform.basis.z right = cam.global_transform.basis.x - # project onto ground plane so looking up/down doesn't tilt movement + # Project onto ground plane so looking up/down doesn't kill movement. forward.y = 0.0 right.y = 0.0 - if forward.length() > 0.0001: + if forward.length() > 0.0001: forward = forward.normalized() - if right.length() > 0.0001: + _last_move_forward = forward + else: + forward = _last_move_forward + if right.length() > 0.0001: right = right.normalized() + _last_move_right = right + else: + right = _last_move_right var dir := (right * input2v.x + forward * input2v.y).normalized() var target_v := dir * MOVE_SPEED @@ -72,6 +110,14 @@ func _integrate_forces(state): linear_velocity.y = JUMP_SPEED audio_player.play() + if cam: + var target_yaw := global_transform.basis.get_euler().y + _camera_yaw = lerp_angle(_camera_yaw, target_yaw, camera_follow_speed * state.step) + var target_basis := Basis(Vector3.UP, _camera_yaw) + var target_pos := global_position + (target_basis * _camera_offset_local) + cam.global_position = cam.global_position.lerp(target_pos, camera_follow_speed * state.step) + cam.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0) + func _input(event): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_MIDDLE: @@ -83,12 +129,7 @@ func _input(event): Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) if event is InputEventMouseMotion and cameraMoveMode: - rotation_x -= event.relative.y * mouse_sensitivity - rotation_y -= event.relative.x * mouse_sensitivity - rotation_x = clamp(rotation_x, deg_to_rad(-90), deg_to_rad(90)) # Prevent flipping - - $Camera3D.rotation.x = rotation_x - rotation.y = rotation_y + _pending_mouse_delta += event.relative if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: