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