Files
godot-playground/scenes/Grid/grid_character_controller.gd
T
2025-11-09 20:19:20 -06:00

359 lines
12 KiB
GDScript

@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)