extends CharacterBody3D ## First-person player controller. ## ## Move (WASD), look (mouse), jump (space), sprint (shift). Gravity + ground ## collision via CharacterBody3D. In deep water the player floats with their ## head above the surface and moves at swimming speed. Deliberately minimal: no ## stamina/health yet (M1), no network ownership yet (M2). Press ESC to free the ## mouse, click to recapture. @export var speed: float = 5.0 @export var sprint_speed: float = 8.0 @export var swim_speed: float = 2.6 @export var jump_velocity: float = 5.5 @export var mouse_sensitivity: float = 0.0025 ## Set by world.gd from the terrain. Feet this far below the surface = swimming. @export var water_level: float = -INF const FLOAT_DEPTH := 1.3 @onready var camera: Camera3D = $Camera3D signal selected_changed signal inventory_requested(container: Node) ## the HUD opens the inventory screen const HOTBAR := 8 var inventory := Inventory.new(40) ## slots 0-7 are the hotbar var selected := 0 var ui_open := false ## set by the inventory screen; freezes look/move var hands: Node3D var survival: Node ## health / stamina / hunger / warmth (survival.gd) var worn := "" ## item id of the cloak you're wearing ("" = none) signal worn_changed var _gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity", 9.8) func _ready() -> void: if not get_parent().get("menu_mode"): # title-screen backdrop: leave the cursor alone Input.mouse_mode = Input.MOUSE_MODE_CAPTURED hands = preload("res://scripts/player/hands.gd").new() hands.name = "Hands" hands.player = self camera.add_child(hands) survival = preload("res://scripts/player/survival.gd").new() survival.name = "Survival" survival.player = self survival.world = get_parent() add_child(survival) func held_item_id() -> String: var s = inventory.get_slot(selected) return s.id if s != null else "" func select(i: int) -> void: selected = wrapi(i, 0, HOTBAR) selected_changed.emit() ## Called by workbenches and chests (container = null for plain crafting). func open_inventory(container: Node) -> void: inventory_requested.emit(container) func _unhandled_input(event: InputEvent) -> void: if ui_open: return if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: rotate_y(-event.relative.x * mouse_sensitivity) camera.rotate_x(-event.relative.y * mouse_sensitivity) camera.rotation.x = clamp(camera.rotation.x, -1.4, 1.4) elif event is InputEventMouseButton and event.pressed: # Recapture after the window lost focus (ESC now opens the pause menu). Input.mouse_mode = Input.MOUSE_MODE_CAPTURED func _physics_process(delta: float) -> void: var float_y := water_level - FLOAT_DEPTH var swimming := global_position.y < float_y + 0.05 and not is_on_floor() if global_position.y < float_y: # Buoyancy: ease back up to floating height. velocity.y = move_toward(velocity.y, clampf((float_y - global_position.y) * 4.0, -3.0, 3.0), 20.0 * delta) elif not is_on_floor(): velocity.y -= _gravity * delta if global_position.y < water_level - 0.6: survival.soak() # waist-deep or more: you're wet var jump := Input.is_action_just_pressed("jump") and not ui_open var can_swim_jump := global_position.y > float_y - 0.4 and global_position.y < float_y + 0.3 if jump and (is_on_floor() or can_swim_jump) and survival.use_stamina(8.0): # (A swim-jump kicks you up out of the water onto a bank.) velocity.y = jump_velocity var input_dir := Vector2.ZERO if ui_open else Input.get_vector("move_left", "move_right", "move_forward", "move_back") var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized() var sprinting: bool = Input.is_key_pressed(KEY_SHIFT) and direction.length() > 0.1 and not ui_open \ and survival.use_stamina(14.0 * delta, true) var current_speed := sprint_speed if sprinting else speed if swimming: current_speed = swim_speed if direction: velocity.x = direction.x * current_speed velocity.z = direction.z * current_speed else: velocity.x = move_toward(velocity.x, 0, current_speed) velocity.z = move_toward(velocity.z, 0, current_speed) move_and_slide()