@tool extends Node3D class_name GridCharacterController @export var grid_manager_path: NodePath @export var camera_path: NodePath @export var start_cell: Vector2i = Vector2i.ZERO @export_range(0.1, 20.0, 0.1) var move_speed: float = 4.0 @export_range(0.0, 2.0, 0.01) var height_offset: float = 0.3 @export var home_marker_scene: PackedScene @export_range(1.0, 16.0, 0.5) var sleep_requirement_hours: float = 8.0 @export var day_night_controller_path: NodePath @export_range(500.0, 5000.0, 50.0) var max_water_ml: float = 2000.0 @export_range(0.0, 5.0, 0.1) var passive_water_usage_ml_per_minute: float = 1.0 @export_range(0.0, 200.0, 0.5) var movement_water_usage_ml_per_meter: float = 2.0 @export_range(50.0, 2000.0, 50.0) var low_water_threshold_ml: float = 400.0 @export_range(100.0, 2000.0, 25.0) var bladder_capacity_ml: float = 750.0 @export_range(200.0, 6000.0, 50.0) var max_food_kcal: float = 2000.0 @export_range(0.0, 5.0, 0.1) var passive_food_usage_kcal_per_minute: float = 1.5 @export_range(0.0, 200.0, 0.5) var movement_food_usage_kcal_per_meter: float = 1.0 @export_range(50.0, 1000.0, 25.0) var low_food_threshold_kcal: float = 300.0 @export var vitals_display_path: NodePath var _grid_manager: GridPlaneManager = null var _camera: Camera3D = null var _current_cell: Vector2i = Vector2i(-1, -1) var _target_position: Vector3 var _is_moving: bool = false var _home_instance: Node3D = null var _day_night_controller: DayNightController = null var _last_time_hours: float = 0.0 var _awake_accum_hours: float = 0.0 var _sleep_accum_hours: float = 0.0 var _is_sleeping: bool = false var _sleep_pending_return: bool = false var _awake_duration_hours: float = 0.0 var _water_level_ml: float = 0.0 var _bladder_level_ml: float = 0.0 var _needs_water: bool = false var _needs_bathroom: bool = false var _returning_home_for_needs: bool = false var _food_level_kcal: float = 0.0 var _needs_food: bool = false signal vitals_changed(water_level: float, bladder_level: float, needs_water: bool, needs_bathroom: bool, food_level: float, needs_food: bool) func get_water_level() -> float: return _water_level_ml func get_bladder_level() -> float: return _bladder_level_ml func is_water_low() -> bool: return _needs_water func is_bladder_full() -> bool: return _needs_bathroom func get_food_level() -> float: return _food_level_kcal func is_food_low() -> bool: return _needs_food func _ready() -> void: set_physics_process(true) set_process_unhandled_input(true) _resolve_references() if _grid_manager: _grid_manager.grid_layout_changed.connect(_on_grid_layout_changed) _move_to_cell(start_cell, true) _spawn_home() _water_level_ml = max_water_ml _bladder_level_ml = 0.0 _food_level_kcal = max_food_kcal _update_awake_duration() if _day_night_controller: _last_time_hours = _day_night_controller.get_time_of_day_hours() else: _last_time_hours = 0.0 _is_sleeping = false _sleep_pending_return = false emit_signal("vitals_changed", _water_level_ml, _bladder_level_ml, _needs_water, _needs_bathroom, _food_level_kcal, _needs_food) func _physics_process(delta: float) -> void: if not _is_moving: return var direction: Vector3 = _target_position - global_transform.origin var distance: float = direction.length() if distance <= 0.01: global_position = _target_position _is_moving = false _handle_arrival_at_current_cell() return var step: float = move_speed * delta var move_vector: Vector3 = direction.normalized() * min(step, distance) global_position += move_vector emit_signal("vitals_changed", _water_level_ml, _bladder_level_ml, _needs_water, _needs_bathroom, _food_level_kcal, _needs_food) func _unhandled_input(event: InputEvent) -> void: if Engine.is_editor_hint(): return if _should_ignore_player_input(): return if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: _move_to_mouse_click() func _move_to_mouse_click() -> void: if not _grid_manager or not _camera: return if _should_ignore_player_input(): return var viewport: Viewport = get_viewport() if viewport == null: return var mouse_position: Vector2 = viewport.get_mouse_position() var target_cell: Vector2i = _pick_cell(mouse_position) if target_cell.x < 0 or target_cell.y < 0: return _move_to_cell(target_cell) func _move_to_cell(cell: Vector2i, instant: bool = false) -> void: if not _grid_manager or not _grid_manager.has_cell(cell): return _current_cell = cell var local_center: Vector3 = _grid_manager.cell_to_world(cell) var world_center: Vector3 = _grid_manager.to_global(local_center) _target_position = world_center + Vector3.UP * height_offset var travel_distance := global_transform.origin.distance_to(_target_position) _apply_movement_water_usage(travel_distance) if instant: global_position = _target_position _is_moving = false _handle_arrival_at_current_cell() else: _is_moving = true func _pick_cell(mouse_position: Vector2) -> Vector2i: var ray_origin: Vector3 = _camera.project_ray_origin(mouse_position) var ray_direction: Vector3 = _camera.project_ray_normal(mouse_position) var plane_normal: Vector3 = _grid_manager.global_transform.basis.y.normalized() var plane_point: Vector3 = _grid_manager.global_transform.origin var denominator: float = plane_normal.dot(ray_direction) if abs(denominator) < 0.0001: return Vector2i(-1, -1) var t: float = plane_normal.dot(plane_point - ray_origin) / denominator if t <= 0.0: return Vector2i(-1, -1) var world_point: Vector3 = ray_origin + ray_direction * t return _grid_manager.world_to_cell(world_point, true) func _on_grid_layout_changed(_cells: Dictionary) -> void: if not _grid_manager: return if not _grid_manager.has_cell(_current_cell): _current_cell = Vector2i( clamp(_current_cell.x, 0, _grid_manager.columns - 1), clamp(_current_cell.y, 0, _grid_manager.rows - 1) ) _move_to_cell(_current_cell, true) _update_home_position() func _resolve_references() -> void: if grid_manager_path != NodePath(): _grid_manager = get_node_or_null(grid_manager_path) as GridPlaneManager else: _grid_manager = get_parent() as GridPlaneManager if camera_path != NodePath(): _camera = get_node_or_null(camera_path) as Camera3D if not _home_instance and home_marker_scene: _spawn_home() if day_night_controller_path != NodePath(): _day_night_controller = get_node_or_null(day_night_controller_path) as DayNightController else: var parent := get_parent() if parent: _day_night_controller = parent.get_node_or_null("../DirectionalLight3D") as DayNightController _connect_day_night() func _spawn_home() -> void: if not home_marker_scene or not _grid_manager: return if _home_instance and is_instance_valid(_home_instance): _home_instance.queue_free() var new_home := home_marker_scene.instantiate() if not new_home: return if not (new_home is Node3D): new_home.queue_free() return _home_instance = new_home var parent := get_parent() if parent: call_deferred("_add_home_to_parent", parent) func _add_home_to_parent(parent: Node) -> void: if not parent or not _home_instance: return if _home_instance.get_parent() == parent: _update_home_position() return parent.add_child(_home_instance) _update_home_position() func _update_home_position() -> void: if not _home_instance or not _grid_manager or not _grid_manager.has_cell(start_cell): return var local_center := _grid_manager.cell_to_world(start_cell) var world_center := _grid_manager.to_global(local_center) _home_instance.global_position = world_center + Vector3.UP * 0.01 func _should_ignore_player_input() -> bool: return _is_sleeping or _sleep_pending_return or _needs_water or _needs_bathroom or _needs_food func _handle_arrival_at_current_cell() -> void: if _current_cell == start_cell: _on_arrived_home() func _on_arrived_home() -> void: _is_moving = false if _needs_bathroom: _empty_bladder() _needs_bathroom = false if _needs_water: _fill_water() _needs_water = false if _needs_food: _eat_food() _returning_home_for_needs = false if _sleep_pending_return: _enter_sleep_state() func _connect_day_night() -> void: if not _day_night_controller: return var callback := Callable(self, "_on_time_changed") if not _day_night_controller.time_changed.is_connected(callback): _day_night_controller.time_changed.connect(callback) _update_awake_duration() func _update_awake_duration() -> void: var total_hours := 24.0 if _day_night_controller: total_hours = _day_night_controller.get_day_length_hours() _awake_duration_hours = max(total_hours - sleep_requirement_hours, 0.1) func _on_time_changed(current_hours: float) -> void: if _day_night_controller == null: return var delta := _compute_time_delta(current_hours) _last_time_hours = current_hours _apply_passive_water_usage(delta) if _is_sleeping: _sleep_accum_hours += delta if _sleep_accum_hours >= sleep_requirement_hours: _exit_sleep_state() else: if _sleep_pending_return: return _awake_accum_hours += delta if _awake_accum_hours >= _awake_duration_hours: _begin_sleep_cycle() func _compute_time_delta(current_hours: float) -> float: var day_length: float = _day_night_controller.get_day_length_hours() if _day_night_controller else 24.0 var delta: float = current_hours - _last_time_hours if delta < 0.0: delta += day_length return delta func _begin_sleep_cycle() -> void: if _sleep_pending_return or _is_sleeping: return _sleep_pending_return = true _sleep_accum_hours = 0.0 _move_to_cell(start_cell) func _enter_sleep_state() -> void: _sleep_pending_return = false _is_sleeping = true _sleep_accum_hours = 0.0 _empty_bladder() func _exit_sleep_state() -> void: _is_sleeping = false _sleep_pending_return = false _sleep_accum_hours = 0.0 _awake_accum_hours = 0.0 if _current_cell == start_cell: if _needs_water: _fill_water() _needs_water = false if _needs_bathroom: _empty_bladder() _needs_bathroom = false func _apply_passive_water_usage(delta_hours: float) -> void: if delta_hours <= 0.0: return var minutes := delta_hours * 60.0 _consume_water(minutes * passive_water_usage_ml_per_minute) _consume_food(minutes * passive_food_usage_kcal_per_minute) func _apply_movement_water_usage(distance: float) -> void: if distance <= 0.0: return _consume_water(movement_water_usage_ml_per_meter * distance) _consume_food(movement_food_usage_kcal_per_meter * distance) func _consume_water(amount: float) -> void: if amount <= 0.0: return _water_level_ml = max(_water_level_ml - amount, 0.0) _bladder_level_ml = min(_bladder_level_ml + amount, bladder_capacity_ml * 2.0) if _bladder_level_ml >= bladder_capacity_ml: _needs_bathroom = true if not _returning_home_for_needs and not _sleep_pending_return: _returning_home_for_needs = true _request_home_return() if _water_level_ml <= low_water_threshold_ml: _needs_water = true if not _returning_home_for_needs and not _sleep_pending_return: _returning_home_for_needs = true _request_home_return() emit_signal("vitals_changed", _water_level_ml, _bladder_level_ml, _needs_water, _needs_bathroom, _food_level_kcal, _needs_food) func _consume_food(amount: float) -> void: if amount <= 0.0: return _food_level_kcal = max(_food_level_kcal - amount, 0.0) if _food_level_kcal <= low_food_threshold_kcal: _needs_food = true if not _returning_home_for_needs and not _sleep_pending_return: _returning_home_for_needs = true _request_home_return() emit_signal("vitals_changed", _water_level_ml, _bladder_level_ml, _needs_water, _needs_bathroom, _food_level_kcal, _needs_food) func _fill_water() -> void: _water_level_ml = max_water_ml emit_signal("vitals_changed", _water_level_ml, _bladder_level_ml, _needs_water, _needs_bathroom, _food_level_kcal, _needs_food) func _empty_bladder() -> void: _bladder_level_ml = 0.0 _needs_bathroom = false emit_signal("vitals_changed", _water_level_ml, _bladder_level_ml, _needs_water, _needs_bathroom, _food_level_kcal, _needs_food) func _eat_food() -> void: _food_level_kcal = max_food_kcal _needs_food = false emit_signal("vitals_changed", _water_level_ml, _bladder_level_ml, _needs_water, _needs_bathroom, _food_level_kcal, _needs_food) func _request_home_return() -> void: if _is_sleeping: return if _current_cell == start_cell and not _is_moving: _on_arrived_home() return _move_to_cell(start_cell)