Explosion_der_Spiele/scripts/player/player_controller.gd
copilot 1d9e0a6532 Wire quest flow and combat across all levels.
Connect bear-save, massage, farm paywall, cheese catapults, Dressler hijack, final boss phases, fat-stim UI, and scene fixes so the satire path plays through.
2026-09-14 19:08:00 +02:00

141 lines
4 KiB
GDScript

extends CharacterBody3D
class_name PlayerController
## Third-person controller: camera follows player, movement is camera-relative.
## Billboard sprites face the camera (Mario-tree style).
@export var move_speed: float = 6.0
@export var attack_range: float = 1.8
@export var attack_damage: int = 1
@export var mouse_sensitivity: float = 0.0025
@export var min_pitch_deg: float = -35.0
@export var max_pitch_deg: float = 55.0
@export var spring_length: float = 5.5
@export var camera_height: float = 1.4
signal interacted
signal attacked
@onready var _billboard: BillboardSprite = $BillboardSprite
@onready var _yaw_pivot: Node3D = $CameraYaw
@onready var _spring: SpringArm3D = $CameraYaw/SpringArm3D
@onready var _camera: Camera3D = $CameraYaw/SpringArm3D/Camera3D
var _locked: bool = false
var _pitch: float = deg_to_rad(-18.0)
func _ready() -> void:
add_to_group("player")
if _spring:
_spring.spring_length = spring_length
_spring.collision_mask = 1
_spring.position = Vector3(0.0, camera_height, 0.0)
if _camera:
_camera.current = true
_camera.fov = 70.0
_apply_pitch()
_set_mouse_captured(true)
if _billboard and _billboard.texture == null:
var tex: Texture2D = load("res://assets/sprites/placeholders/player.png")
if tex:
_billboard.set_texture(tex)
func _exit_tree() -> void:
_set_mouse_captured(false)
func _unhandled_input(event: InputEvent) -> void:
if _locked or get_tree().paused:
return
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
var motion := event as InputEventMouseMotion
_yaw_pivot.rotate_y(-motion.relative.x * mouse_sensitivity)
_pitch = clampf(
_pitch - motion.relative.y * mouse_sensitivity,
deg_to_rad(min_pitch_deg),
deg_to_rad(max_pitch_deg)
)
_apply_pitch()
elif event is InputEventMouseButton and event.pressed:
_set_mouse_captured(true)
func _physics_process(_delta: float) -> void:
if _locked or get_tree().paused:
velocity = Vector3.ZERO
return
var input := Vector2(
Input.get_axis("move_left", "move_right"),
Input.get_axis("move_up", "move_down")
)
var basis_yaw := _yaw_pivot.global_transform.basis
var forward := -basis_yaw.z
forward.y = 0.0
forward = forward.normalized()
var right := basis_yaw.x
right.y = 0.0
right = right.normalized()
var dir := (right * input.x) + (forward * -input.y)
if dir.length() > 1.0:
dir = dir.normalized()
var speed := move_speed
if GameState.is_fat():
speed *= 0.15
elif GameState.is_glitched_slow():
speed *= 0.55
if GameState.is_shocked():
velocity = Vector3.ZERO
return
velocity = dir * speed
move_and_slide()
if Input.is_action_just_pressed("interact"):
_try_interact()
if Input.is_action_just_pressed("attack"):
_try_attack()
func lock_input(locked: bool) -> void:
_locked = locked
if locked:
_set_mouse_captured(false)
elif not get_tree().paused:
_set_mouse_captured(true)
func set_gameplay_focused(focused: bool) -> void:
## Called by pause UI: release mouse in menus, recapture in gameplay.
if focused and not _locked:
_set_mouse_captured(true)
else:
_set_mouse_captured(false)
func _apply_pitch() -> void:
if _spring == null:
return
_spring.rotation.x = _pitch
func _set_mouse_captured(captured: bool) -> void:
if captured:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
else:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
func _try_interact() -> void:
interacted.emit()
for body in get_tree().get_nodes_in_group("interactable"):
if body is Node3D and global_position.distance_to(body.global_position) <= 2.2:
if body.has_method("on_interact"):
body.on_interact(self)
return
func _try_attack() -> void:
attacked.emit()
AudioDirector.play_hit()
var dmg := attack_damage
if Inventory.has_item("tooth"):
dmg = max(dmg, 2)
if Inventory.has_item("moon_sword"):
dmg = max(dmg, 99)
if Inventory.has_item("wartchen"):
dmg = max(dmg, 3)
for body in get_tree().get_nodes_in_group("enemy"):
if body is Node3D and global_position.distance_to(body.global_position) <= attack_range:
if body.has_method("take_hit"):
body.take_hit(dmg)