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.
141 lines
4 KiB
GDScript
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)
|