grunk/src/player/player.gd

282 lines
6.4 KiB
GDScript

class_name Player extends CharacterBody3D
#region Exported Properties
@export_category("HAX!!")
@export var godmode := false
@export_category("Status")
@export var dead := false
@export var movement_enabled := true
@export var activity_enabled := true
@export var look_enabled := true
@export var camera_rumble: float:
set(value):
if Game.settings.enable_screen_shake:
cam_rumbler.intensity = value
get:
return cam_rumbler.intensity
@export_category("Movement")
@export_group("Speed")
@export var run_speed := 80.0
@export var sprint_speed := 160.0
@export var sneak_speed := 40.0
@export var focus_speed := 25.0
@export var air_speed_factor := 0.1
@export_group("Jump")
@export var jump_force := 4.0
@export_group("Friction")
@export var ground_friction := 0.3
@export var air_friction := 0.03
@export_category("Inventory")
@export var inventory: Dictionary[Item, int] = {}
#endregion
#region Member Variables
var gravity: Vector3 = (
ProjectSettings.get_setting("physics/3d/default_gravity")
* ProjectSettings.get_setting("physics/3d/default_gravity_vector")
)
var selected_interactive: Interactive
var firing := false
var sneaking := false
var _was_on_floor := false
@onready var hud: PlayerHUD = %PlayerHUD
@onready var camera_pivot: CameraController = %CameraPivot
@onready var cam_rumbler: Rumbler3D = %CamRumbler
@onready var interact_ray: RayCast3D = %InteractRay
@onready var tool_mount: ToolMount = %ToolMount
@onready var point_spray: PointSpray = %PointSpray
@onready var wide_spray: WideSpray = %WideSpray
@onready var toothbrush: Tool = %Toothbrush
@onready var mp3_player: Tool = %MP3Player
@onready var crouch_head_area: Area3D = %CrouchHeadArea
@onready var crouch_animation: AnimationPlayer = %CrouchAnimation
@onready var grab_animation: AnimationPlayer = %GrabAnimation
@onready var jump_game_sound_emitter: GameSoundEmitter = %JumpGameSoundEmitter
#endregion
## Global static access to player singleton
static var instance: Player
#region _ready
func _ready() -> void:
World.instance.manager.milestone_reached.connect(_on_milestone)
instance = self
#endregion
#region Public Methods
func get_speed() -> float:
var speed := run_speed
if Input.is_action_pressed("sprint"):
speed = sprint_speed
if sneaking:
speed = sneak_speed
if firing:
speed = focus_speed
if not is_on_floor():
speed *= air_speed_factor
return speed
func get_friction() -> float:
if is_on_floor():
return ground_friction
return air_friction
func get_tool() -> Tool:
return tool_mount.get_active()
## Add the given item to the player's inventory.
func add_item(item: Item, amount: int = 1) -> void:
inventory[item] = inventory.get(item, 0) + amount
## Remove the given item from the player's inventory.
func remove_item(item: Item, amount: int = 1) -> void:
inventory[item] = inventory.get(item, 0) - amount
if inventory[item] <= 0:
inventory.erase(item)
func crouch() -> void:
if not sneaking and not crouch_animation.is_playing():
crouch_animation.play("crouch")
sneaking = true
func uncrouch() -> void:
if (
sneaking
and not crouch_animation.is_playing()
and not crouch_head_area.has_overlapping_bodies()
):
crouch_animation.play_backwards("crouch")
sneaking = false
func toggle_crouch() -> void:
if sneaking:
uncrouch()
else:
crouch()
## Get fuckign grabbed, idiot!
## Begin grab death sequence animation.
func get_grabbed() -> void:
if dead or godmode: # No double-grabsies
return
movement_enabled = false
activity_enabled = false
look_enabled = false
dead = true
uncrouch()
grab_animation.play("get_grabbed")
camera_pivot.reset_pitch(0.4)
#endregion
#region Event Handlers
func _on_milestone(milestone: Milestone) -> void:
if milestone.mp3_player:
tool_mount.set_active(mp3_player)
if milestone.toothbrush:
tool_mount.set_active(toothbrush)
if milestone.stickers:
pass # TODO equip stickers
func _signal_death() -> void:
# Called from the death animation
World.instance.manager.on_player_death()
#endregion
#region _physics_process
func _physics_process(delta: float) -> void:
# Will be null if no valid interactor is selected.
var interactive: Interactive = interact_ray.get_collider() as Interactive
hud.select_interactive(interactive)
if interactive:
interactive.select()
if activity_enabled:
# World interaction
if interactive and Input.is_action_just_pressed("interact"):
interactive.activate()
# Tool selection
if Input.is_action_just_pressed("select_next_tool"):
tool_mount.set_active_relative(1)
elif Input.is_action_just_pressed("select_prev_tool"):
tool_mount.set_active_relative(-1)
elif Input.is_action_just_pressed("select_point_spray"):
tool_mount.set_active(point_spray)
elif Input.is_action_just_pressed("select_wide_spray"):
tool_mount.set_active(wide_spray)
elif Input.is_action_just_pressed("select_brush"):
tool_mount.set_active(toothbrush)
elif Input.is_action_just_pressed("select_mp3_player"):
tool_mount.set_active(mp3_player)
# Tool use
if Input.is_action_pressed("fire"):
get_tool().fire()
firing = true
else:
get_tool().idle()
firing = false
if Input.is_action_just_pressed("switch_mode"):
get_tool().switch_mode()
# Two sneaking modes -- hold and toggle
if Game.settings.hold_to_sneak:
if Input.is_action_pressed("sneak"):
crouch()
else:
uncrouch()
else:
if Input.is_action_just_pressed("sneak"):
toggle_crouch()
# Jumping
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
if is_on_floor():
if not _was_on_floor and not sneaking:
# just landed
jump_game_sound_emitter.emit_sound_here()
else:
# Gravity
velocity += gravity * delta
# Input movement
if movement_enabled:
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var rel_input := input_dir.rotated(-camera_pivot.global_rotation.y)
var direction := Vector3(rel_input.x, 0, rel_input.y).normalized()
var movement := direction * get_speed() * delta
velocity.x += movement.x
velocity.z += movement.z
# Friction
var friction := get_friction()
var weight := 1 - exp(-friction * 60 * delta)
velocity.x = lerpf(velocity.x, 0, weight)
velocity.z = lerpf(velocity.z, 0, weight)
_was_on_floor = is_on_floor()
move_and_slide()
#endregion
#region Persistence
func serialize() -> Dictionary:
return {
"inventory": inventory,
}
func deserialize(state: Dictionary) -> void:
@warning_ignore("unsafe_cast")
inventory.assign(state["inventory"] as Dictionary)
#endregion