# Hurry Curry! - a game about cooking # Copyright (C) 2025 Hurry Curry! contributors # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, version 3 of the License only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # class_name ControllablePlayer extends Player const PLAYER_SPEED = 55 const PLAYER_FRICTION = 15 const BOOST_FACTOR = 2.5 const BOOST_DURATION = 0.3 const BOOST_RESTORE = 0.5 const MAX_PLAYER_INTERACT_DIST := 1.9 var onscreen_controls = preload("res://player/onscreen_controls/controls.tscn").instantiate() var facing := Vector2(1, 0) var velocity_ := Vector2(0, 0) var direction := Vector2(0, 0) var stamina := 0. var chat_open := false var input_rotation = 0 var vibration_timer := Timer.new() var current_vibration_strength := 0. var current_vibration_change := 0. var target_tile: Vector2i = Vector2i.ZERO var target_visual: Vector3 = Vector3.ZERO var found_interact_target := false var last_interaction = null func _ready(): vibration_timer = Timer.new() vibration_timer.wait_time = 0.1 vibration_timer.timeout.connect(_on_vibration_timeout) add_child(vibration_timer) var timer = Timer.new() timer.one_shot = false timer.wait_time = 1. / 50. add_child(timer) timer.start() timer.connect("timeout", func(): if game.mp != null and game.join_state == Game.JoinState.JOINED: game.mp.send_movement(game.my_player_id, position_, direction, boosting) ) add_child(onscreen_controls) super() const MAX_DT = 1. / 50. func _process(delta): super(delta) marker.position = G.interpolate(marker.position, target_visual, delta * 30.) while delta > 0.001: var dt = min(delta, MAX_DT) _process_movement(dt) delta -= dt update_touch_scrolls() var moving_duration = 0 var fps_rotation_target = 0 func _process_movement(delta): var input = Input.get_vector("left", "right", "forwards", "backwards") if is_input_enabled() else Vector2.ZERO if Settings.read("gameplay.first_person"): fps_rotation_target += input.x * delta * 3. if abs(input.x) > 0.1: input.y -= 0.5 input.x = 0. input.y = min(input.y, 0) input = input.rotated(fps_rotation_target) else: input = input.rotated(input_rotation) var boost = Input.is_action_pressed("boost") or (Settings.read("gameplay.latch_boost") and boosting) if Input.is_action_pressed("interact_left") or Input.is_action_just_released("interact_left") or Input.is_action_pressed("interact_right") or Input.is_action_just_released("interact_right"): input *= 0 else: update_interact_target() if Settings.read("gameplay.accessible_movement"): if input.length() < 0.5: moving_duration -= delta * 2 else: moving_duration += delta moving_duration = clamp(moving_duration, 0, 1) input *= min(1, moving_duration) interact() var was_boosting = boosting direction = input update(delta, boost) if boosting and not was_boosting and Settings.read("gameplay.vibration"): Input.start_joy_vibration(0, 0, input.length(), 0.15) Input.vibrate_handheld(75, input.length() * 0.1) walking = input.length_squared() > 0.1 position_anim = position_ rotation_anim = rotation_ func update(dt: float, boost: bool): direction = direction.limit_length(1.) if direction.length() > 0.05: facing = direction + (facing - direction) * exp(-dt * 10.) if direction.length() < 0.5: direction *= 0 rotation_ = atan2(facing.x, facing.y); boost = boost and direction.length() > 0.1 boosting = boost and (boosting or stamina >= 1.0) and stamina > 0 if boosting: stamina -= dt / BOOST_DURATION else: stamina += dt / BOOST_RESTORE stamina = max(min(stamina, 1.0), 0.0) var speed = PLAYER_SPEED * (BOOST_FACTOR if boosting else 1.) velocity_ += direction * dt * speed position_ += velocity_ * dt velocity_ = velocity_ * exp(-dt * PLAYER_FRICTION) collide(dt) func collide(dt: float): for xo in range(-1, 2): for yo in range(-1, 2): var tile = Vector2i(xo, yo) + Vector2i(position_) if !game.get_tile_collision(tile): continue tile = Vector2(tile) var d = aabb_point_distance(tile, tile + Vector2.ONE, position_) if d > PLAYER_SIZE: continue var h = 0.01; var d_sample_x = aabb_point_distance(tile, tile + Vector2.ONE, position_ + Vector2(h, 0)) var d_sample_y = aabb_point_distance(tile, tile + Vector2.ONE, position_ + Vector2(0, h)) var grad = (Vector2(d_sample_x - d, d_sample_y - d)) / h position_ += (PLAYER_SIZE - d) * grad; velocity_ -= grad * grad.dot(velocity_) for player: Player in game.players.values(): var diff = position_ - player.position_ var d = diff.length() if d < 0.01: continue if d >= PLAYER_SIZE * 2: continue var norm = diff.normalized(); var f = 100 / (1 + d) velocity_.x += norm.x * f * dt velocity_.y += norm.y * f * dt func is_input_enabled() -> bool: return not game.menu.covered and not Global.game_paused func update_touch_scrolls(): # TODO: Don't call this function every frame, but only when input menu # covered value is updated if onscreen_controls.touch_enabled: onscreen_controls.visible = is_input_enabled() func aabb_point_distance(mi: Vector2, ma: Vector2, p: Vector2) -> float: return (p - p.clamp(mi, ma)).length() func update_position(_new_position: Vector2, _new_rotation: float, _new_boosting: bool): pass func progress(position__: float, speed: float, warn: bool, h): super(position__, speed, warn, h) if warn: current_vibration_strength = position__ current_vibration_change = speed var vibration_strength := pow(current_vibration_strength, 3) if Settings.read("gameplay.vibration"): # todo maybe include the lines above too Input.start_joy_vibration(0, vibration_strength, 0, 0.1) Input.vibrate_handheld(100, vibration_strength) vibration_timer.start() if speed == 0: current_vibration_strength = 0. vibration_timer.stop() func _on_vibration_timeout(): current_vibration_strength = clampf(current_vibration_strength + current_vibration_change * 0.1, 0, 1) if current_vibration_strength == 0.: return var vibration_strength := pow(current_vibration_strength, 3) Input.start_joy_vibration(0, vibration_strength, 0, 0.1) Input.vibrate_handheld(100, vibration_strength) vibration_timer.start() func put_item(tile: Tile, h: int): super(tile, h) if Settings.read("gameplay.vibration"): Input.start_joy_vibration(0, 0.1, 0.0, 0.075) Input.vibrate_handheld(75, 0.1) func take_item(tile: Tile, h: int): super(tile, h) if Settings.read("gameplay.vibration"): Input.start_joy_vibration(0, 0.1, 0.0, 0.075) Input.vibrate_handheld(75, 0.1) func interact(): if not is_input_enabled(): return var tile = game.map.get_tile_instance(target_tile) if tile != null: if not marker.visible: marker.visible = true marker.position = target_visual # clear last interaction if target_tile has moved since if last_interaction != null and not last_interaction == target_tile: game.mp.send_tile_interact(game.my_player_id, last_interaction, false, 0) marker.set_interacting(false) last_interaction = null marker.set_interactive(found_interact_target) for h in [0, 1]: if Input.is_action_just_pressed("interact_"+G.index_to_hand(h)) and last_interaction == null: last_interaction = target_tile game.mp.send_tile_interact(game.my_player_id, target_tile, true, h) tile.interact() marker.set_interacting(true) if Input.is_action_just_released("interact_"+G.index_to_hand(h)): last_interaction = null game.mp.send_tile_interact(game.my_player_id, target_tile, false, h) marker.set_interacting(false) else: marker.visible = false func update_interact_target(): match Settings.read("gameplay.interact_target"): "dirsnap": return update_interact_target_dirsnap() "dir": return update_interact_target_dir() _: return update_interact_target_dir() func update_interact_target_dir(): target_tile = Vector2i( int(floor(movement_base.position.x + sin(movement_base.rotation.y))), int(floor(movement_base.position.z + cos(movement_base.rotation.y))) ) var tile = game.map.get_tile_instance(target_tile) if tile != null: found_interact_target = game.get_tile_interactive(target_tile) target_visual = tile.item_base.global_position else: found_interact_target = false target_visual = Vector3(float(target_tile.x), 1., float(target_tile.y)) func update_interact_target_dirsnap(): var interact_target := Vector2( movement_base.position.x + sin(movement_base.rotation.y) * 0.7, movement_base.position.z + cos(movement_base.rotation.y) * 0.7 ) var interact_target_i := interact_target.floor() target_visual = Vector3(interact_target_i.x, 0, interact_target_i.y) target_tile = interact_target_i var best_distance := 100. found_interact_target = false # Calculate player positions. Tiles with players on them are valid interact targets. var player_positions: Dictionary = {} # Dictionary[id: int, pos: Vector3] for p: Player in game.players.values(): if not p.is_chef: continue player_positions[p.id] = Vector3(p.position_anim.x, 0, p.position_anim.y) # Test all tiles in a 3x3 square around the player for interactible tiles. # Return the one which is closest to interact_target. for offset_x in range(-1, 2): for offset_y in range (-1, 2): var offset_cursor := interact_target_i + Vector2(offset_x, offset_y) var tile_center := Vector2(offset_cursor) + Vector2(0.5, 0.5) if game.get_tile_interactive(offset_cursor): var cursor_tile_distance := interact_target.distance_to(tile_center) var player_tile_distance := Vector2( movement_base.position.x - tile_center.x, movement_base.position.z - tile_center.y ).length() if player_tile_distance < MAX_PLAYER_INTERACT_DIST && cursor_tile_distance < best_distance: found_interact_target = true best_distance = cursor_tile_distance target_visual = game.map.get_tile_instance(offset_cursor).item_base.global_position target_tile = offset_cursor continue # Check if there are any players on this tile. # If there are multilpe, remember the player closest to the center of the tile # (They will be the interact target) var best_cursor_tile_distance := 100. var best_player_tile_distance := 100. var best_player_pos: Vector3 for p_id: int in player_positions.keys(): if p_id == game.my_player_id: continue # I can't interact with myself var p_pos: Vector3 = player_positions[p_id] var p_pos_2d := Vector2(p_pos.x, p_pos.z) var tile_center_distance := p_pos_2d.distance_to(tile_center) var cursor_tile_distance := interact_target.distance_to(tile_center) var player_tile_distance := Vector2(movement_base.position.x, movement_base.position.z).distance_to(tile_center) if tile_center_distance < 0.7: if cursor_tile_distance < best_cursor_tile_distance: best_cursor_tile_distance = cursor_tile_distance best_player_tile_distance = player_tile_distance best_player_pos = p_pos if best_player_tile_distance < MAX_PLAYER_INTERACT_DIST && best_cursor_tile_distance < best_distance: found_interact_target = true best_distance = best_cursor_tile_distance target_visual = best_player_pos target_tile = offset_cursor