Compare commits

...
10 Commits
15 changed files with 904 additions and 86 deletions
+109
View File
@@ -0,0 +1,109 @@
@tool
extends DirectionalLight3D
class_name DayNightController
signal time_changed(current_hours: float)
@export var grid_manager_path: NodePath
@export_range(1.0, 48.0, 0.5) var day_length_hours: float = 12.0
@export_range(1.0, 600.0, 1.0) var real_seconds_per_game_hour: float = 60.0
@export_range(0.0, 48.0, 0.25) var start_time_of_day_hours: float = 6.0
@export var orbit_axis: Vector3 = Vector3(1.0, 0.0, -1.0)
@export_range(0.0, 90.0, 0.5) var inclination_degrees: float = 35.0
var _grid_manager: Node3D = null
var _time_of_day_hours: float = 0.0
var _initial_environment_settings: Dictionary = {}
var _sunlight_multiplier: float = 1.0
func _ready() -> void:
set_process(true)
_resolve_references()
_time_of_day_hours = clamp(start_time_of_day_hours, 0.0, day_length_hours)
_apply_sun_lighting()
_emit_time_changed()
_capture_initial_environment()
_update_environment()
func _process(delta: float) -> void:
if Engine.is_editor_hint():
return
if day_length_hours <= 0.0 or real_seconds_per_game_hour <= 0.0:
return
_time_of_day_hours = fposmod(_time_of_day_hours + delta / real_seconds_per_game_hour, day_length_hours)
_apply_sun_lighting()
_emit_time_changed()
_update_environment()
func _resolve_references() -> void:
if grid_manager_path != NodePath():
_grid_manager = get_node_or_null(grid_manager_path)
else:
_grid_manager = get_parent() as Node3D
func _apply_sun_lighting() -> void:
if day_length_hours <= 0.0:
return
var cycle: float = 0.0
if day_length_hours > 0.0:
cycle = fposmod(_time_of_day_hours / day_length_hours, 1.0)
var sun_angle: float = cycle * TAU
var axis: Vector3 = orbit_axis.normalized()
if axis.length() == 0.0:
axis = Vector3.RIGHT
var direction: Vector3 = Vector3.UP.rotated(axis, sun_angle)
direction = direction.rotated(Vector3.UP, deg_to_rad(inclination_degrees)).normalized()
var target: Vector3 = _grid_manager.global_transform.origin if _grid_manager else global_transform.origin
var sun_distance: float = 60.0
var sun_position: Vector3 = target - direction * sun_distance
var up_vector: Vector3 = Vector3.UP
if direction.is_equal_approx(up_vector) or direction.is_equal_approx(-up_vector):
up_vector = Vector3.FORWARD
look_at_from_position(sun_position, target, up_vector)
var daylight_phase: float = sin((sun_angle - TAU * 0.25))
var daylight: float = clamp(daylight_phase * 0.5 + 0.5, 0.0, 1.0)
var dawn_color: Color = Color(0.95, 0.58, 0.32)
var night_color: Color = Color(0.12, 0.18, 0.35)
var day_color: Color = Color(1.0, 0.96, 0.88)
var warm_mix: Color = dawn_color.lerp(day_color, clamp((daylight - 0.25) / 0.5, 0.0, 1.0))
light_color = night_color.lerp(warm_mix, daylight)
light_energy = lerp(0.05, 1.25, daylight)
shadow_enabled = daylight > 0.1
_sunlight_multiplier = daylight
func get_time_of_day_hours() -> float:
return _time_of_day_hours
func get_day_length_hours() -> float:
return day_length_hours
func _emit_time_changed() -> void:
emit_signal("time_changed", _time_of_day_hours)
func _capture_initial_environment() -> void:
var world: World3D = get_world_3d()
if not world:
return
var env: Environment = world.environment
if env:
_initial_environment_settings = {
"ambient_light_color": env.ambient_light_color,
"ambient_light_energy": env.ambient_light_energy,
"background_color": env.background_color
}
func _update_environment() -> void:
var world: World3D = get_world_3d()
if not world:
return
var env: Environment = world.environment
if not env:
return
var daylight: float = clamp(_sunlight_multiplier, 0.0, 1.0)
var ambient_day: Color = Color(0.6, 0.7, 0.85)
var ambient_night: Color = Color(0.02, 0.05, 0.1)
var background_day: Color = Color(0.5, 0.75, 1.0)
var background_night: Color = Color(0.01, 0.02, 0.08)
env.ambient_light_color = ambient_night.lerp(ambient_day, daylight)
env.ambient_light_energy = lerp(0.15, 1.5, daylight)
env.background_color = background_night.lerp(background_day, daylight)
+1
View File
@@ -0,0 +1 @@
uid://oy8cl8dscrh2
+117
View File
@@ -0,0 +1,117 @@
@tool
extends Camera3D
class_name GridCameraController
@export var grid_manager_path: NodePath
@export var ground_mesh_path: NodePath
@export var ground_collision_path: NodePath
@export_range(0.1, 10.0, 0.1) var zoom_min: float = 0.5
@export_range(0.1, 10.0, 0.1) var zoom_max: float = 3.0
@export_range(0.01, 1.0, 0.01) var zoom_step: float = 0.1
var _grid_manager: GridPlaneManager = null
var _ground_mesh: MeshInstance3D = null
var _ground_collision: CollisionShape3D = null
var _zoom_factor: float = 1.0
func _ready() -> void:
_resolve_references()
if _grid_manager:
_grid_manager.grid_layout_changed.connect(_on_grid_layout_changed)
_update_camera_view()
_update_ground_assets()
func _on_grid_layout_changed(_cells: Dictionary) -> void:
_update_camera_view()
_update_ground_assets()
func _unhandled_input(event: InputEvent) -> void:
if Engine.is_editor_hint():
return
if event is InputEventMouseButton and event.pressed:
var button_event := event as InputEventMouseButton
if button_event.button_index == MOUSE_BUTTON_WHEEL_UP:
_adjust_zoom(-zoom_step)
elif button_event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_adjust_zoom(zoom_step)
func _adjust_zoom(delta: float) -> void:
var new_zoom: float = clamp(_zoom_factor + delta, zoom_min, zoom_max)
if is_equal_approx(new_zoom, _zoom_factor):
return
_zoom_factor = new_zoom
_update_camera_view()
_update_ground_assets()
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 ground_mesh_path != NodePath():
_ground_mesh = get_node_or_null(ground_mesh_path) as MeshInstance3D
if ground_collision_path != NodePath():
_ground_collision = get_node_or_null(ground_collision_path) as CollisionShape3D
func _update_camera_view() -> void:
if not is_inside_tree():
return
var grid_size: Vector2 = Vector2(20, 20)
var target: Vector3 = global_transform.origin
if _grid_manager:
grid_size = _grid_manager.get_grid_size()
target = _grid_manager.global_transform.origin
var extent: float = max(grid_size.x, grid_size.y)
var half_extent: float = max(extent * 0.5, 1.0)
var distance: float = max(half_extent * 1.1, 6.0) * _zoom_factor
var height: float = max(half_extent * 0.8, 6.0) * _zoom_factor
var offset: Vector3 = Vector3(distance, height, distance)
var desired_position: Vector3 = target + offset
look_at_from_position(desired_position, target, Vector3.UP)
func _update_ground_assets() -> void:
if not is_inside_tree():
return
var required_size: Vector2 = _compute_required_ground_size()
if _ground_mesh and _ground_mesh.mesh is PlaneMesh:
var plane_mesh: PlaneMesh = _ground_mesh.mesh as PlaneMesh
plane_mesh.size = required_size
if _ground_collision and _ground_collision.shape is BoxShape3D:
var box: BoxShape3D = _ground_collision.shape as BoxShape3D
var thickness: float = _grid_manager.get_ground_thickness() if _grid_manager else 0.4
box.size = Vector3(required_size.x, thickness, required_size.y)
func _compute_required_ground_size() -> Vector2:
var grid_size: Vector2 = Vector2(20, 20)
var padding: float = 20.0
if _grid_manager:
grid_size = _grid_manager.get_grid_size()
padding = _grid_manager.get_ground_padding()
var base_half: Vector2 = Vector2(max(grid_size.x * 0.5, 1.0), max(grid_size.y * 0.5, 1.0))
var half_extents: Vector2 = base_half
var viewport: Viewport = get_viewport()
if viewport:
var view_size: Vector2 = viewport.get_visible_rect().size
var corners: Array = [
Vector2.ZERO,
Vector2(view_size.x, 0),
Vector2(0, view_size.y),
view_size
]
var plane_normal: Vector3 = (_grid_manager.global_transform.basis.y if _grid_manager else global_transform.basis.y).normalized()
var plane_point: Vector3 = (_grid_manager.global_transform.origin if _grid_manager else global_transform.origin)
for corner in corners:
var origin: Vector3 = project_ray_origin(corner)
var direction: Vector3 = project_ray_normal(corner)
var denominator: float = plane_normal.dot(direction)
if abs(denominator) < 0.0001:
continue
var t: float = plane_normal.dot(plane_point - origin) / denominator
if t <= 0.0:
continue
var world_point: Vector3 = origin + direction * t
var local_point: Vector3 = _grid_manager.to_local(world_point) if _grid_manager else to_local(world_point)
half_extents.x = max(half_extents.x, abs(local_point.x))
half_extents.y = max(half_extents.y, abs(local_point.z))
var padding_vec: Vector2 = Vector2.ONE * padding
return (half_extents + padding_vec) * 2.0
@@ -0,0 +1 @@
uid://5p4mcxw7wann
+358
View File
@@ -0,0 +1,358 @@
@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)
@@ -0,0 +1 @@
uid://6asq70b60kif
+80 -9
View File
@@ -1,15 +1,22 @@
[gd_scene load_steps=9 format=4 uid="uid://qd6xvgwrd00g"] [gd_scene load_steps=18 format=4 uid="uid://qd6xvgwrd00g"]
[ext_resource type="Script" uid="uid://3goclr4srt7k" path="res://scenes/Grid/grid_plane_manager.gd" id="1_72cxc"] [ext_resource type="Script" uid="uid://3goclr4srt7k" path="res://scenes/Grid/grid_plane_manager.gd" id="1_72cxc"]
[ext_resource type="Script" uid="uid://5p4mcxw7wann" path="res://scenes/Grid/grid_camera_controller.gd" id="2_o2iv9"]
[ext_resource type="Script" uid="uid://ehef5rrel4d6" path="res://scenes/Grid/grid_highlight_controller.gd" id="3_n2b7o"]
[ext_resource type="Script" uid="uid://oy8cl8dscrh2" path="res://scenes/Grid/day_night_controller.gd" id="4_u8bvi"]
[ext_resource type="Script" uid="uid://6asq70b60kif" path="res://scenes/Grid/grid_character_controller.gd" id="5_n4d0h"]
[ext_resource type="Script" uid="uid://24uoe7hanwp2" path="res://scenes/Grid/time_display.gd" id="6_8n3rl"]
[ext_resource type="PackedScene" uid="uid://r703rwsvxsx2" path="res://scenes/Grid/grid_home_marker.tscn" id="7_1vmu7"]
[ext_resource type="Script" uid="uid://25dqopfaaam0" path="res://scenes/Grid/water_display.gd" id="8_8k3xv"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"]
albedo_color = Color(0.211765, 0.364706, 0.211765, 1) albedo_color = Color(0.211765, 0.364706, 0.211765, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_ground"] [sub_resource type="PlaneMesh" id="PlaneMesh_ground"]
size = Vector2(10, 10) size = Vector2(166.33133, 166.33133)
[sub_resource type="BoxShape3D" id="BoxShape3D_ground"] [sub_resource type="BoxShape3D" id="BoxShape3D_ground"]
size = Vector3(10, 0.4, 10) size = Vector3(166.33133, 0.4, 166.33133)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_grid"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_grid"]
no_depth_test = true no_depth_test = true
@@ -17,7 +24,7 @@ shading_mode = 0
vertex_color_use_as_albedo = true vertex_color_use_as_albedo = true
albedo_color = Color(0.811765, 0.917647, 1, 0.74902) albedo_color = Color(0.811765, 0.917647, 1, 0.74902)
[sub_resource type="ArrayMesh" id="ArrayMesh_q3k88"] [sub_resource type="ArrayMesh" id="ArrayMesh_1wejn"]
_surfaces = [{ _surfaces = [{
"aabb": AABB(-5, 0.02, -5, 10, 1e-05, 10), "aabb": AABB(-5, 0.02, -5, 10, 1e-05, 10),
"attribute_data": PackedByteArray("M7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6U="), "attribute_data": PackedByteArray("M7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6U="),
@@ -34,15 +41,22 @@ no_depth_test = true
shading_mode = 0 shading_mode = 0
albedo_color = Color(0.976471, 0.843137, 0.333333, 0.6) albedo_color = Color(0.976471, 0.843137, 0.333333, 0.6)
[sub_resource type="PlaneMesh" id="PlaneMesh_8dkrf"] [sub_resource type="PlaneMesh" id="PlaneMesh_2l7eq"]
size = Vector2(1, 1) size = Vector2(1, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_character"]
albedo_color = Color(0.941176, 0.403922, 0.262745, 1)
[sub_resource type="SphereMesh" id="SphereMesh_character"]
radius = 0.35
[node name="GridField" type="Node3D"] [node name="GridField" type="Node3D"]
script = ExtResource("1_72cxc") script = ExtResource("1_72cxc")
columns = 10 columns = 10
rows = 10 rows = 10
cell_size = 1.0 cell_size = 1.0
ground_thickness = 0.4 ground_thickness = 0.4
ground_padding = 20.0
show_grid_lines = true show_grid_lines = true
grid_line_color = Color(0.2, 0.7, 1, 0.65) grid_line_color = Color(0.2, 0.7, 1, 0.65)
@@ -62,21 +76,78 @@ shape = SubResource("BoxShape3D_ground")
unique_name_in_owner = true unique_name_in_owner = true
material_override = SubResource("StandardMaterial3D_grid") material_override = SubResource("StandardMaterial3D_grid")
cast_shadow = 0 cast_shadow = 0
mesh = SubResource("ArrayMesh_q3k88") mesh = SubResource("ArrayMesh_1wejn")
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 5, 0) unique_name_in_owner = true
transform = Transform3D(1.0000002, 1.306986e-15, 5.713018e-23, 0, -4.3711392e-08, 1.0000001, 1.3069862e-15, -1.0000001, -4.3711392e-08, 5.165678e-06, 60, 9.1084837e-07)
light_color = Color(1, 0.96, 0.88, 1)
light_energy = 1.25
shadow_enabled = true shadow_enabled = true
script = ExtResource("4_u8bvi")
grid_manager_path = NodePath("..")
day_length_hours = 24.0
real_seconds_per_game_hour = 5.0
[node name="Camera3D" type="Camera3D" parent="."] [node name="Camera3D" type="Camera3D" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
transform = Transform3D(0.70710677, -0.40824828, 0.57735026, 0, 0.8164966, 0.57735026, -0.70710677, -0.40824828, 0.57735026, 6, 6, 6) transform = Transform3D(0.7071067, -0.40824828, 0.5773502, 0, 0.8164966, 0.5773502, -0.7071067, -0.40824828, 0.5773502, 6, 6, 6)
current = true current = true
fov = 60.0 fov = 60.0
script = ExtResource("2_o2iv9")
grid_manager_path = NodePath("..")
ground_mesh_path = NodePath("../GroundMesh")
ground_collision_path = NodePath("../GroundBody/GroundCollision")
[node name="HoverHighlight" type="MeshInstance3D" parent="."] [node name="HoverHighlight" type="MeshInstance3D" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
material_override = SubResource("StandardMaterial3D_highlight") material_override = SubResource("StandardMaterial3D_highlight")
cast_shadow = 0 cast_shadow = 0
mesh = SubResource("PlaneMesh_8dkrf") mesh = SubResource("PlaneMesh_2l7eq")
script = ExtResource("3_n2b7o")
grid_manager_path = NodePath("..")
camera_path = NodePath("../Camera3D")
[node name="GridCharacter" type="Node3D" parent="."]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.5, 0.3, -4.5)
script = ExtResource("5_n4d0h")
grid_manager_path = NodePath("..")
camera_path = NodePath("../Camera3D")
move_speed = 5.0
home_marker_scene = ExtResource("7_1vmu7")
day_night_controller_path = NodePath("../DirectionalLight3D")
[node name="Mesh" type="MeshInstance3D" parent="GridCharacter"]
material_override = SubResource("StandardMaterial3D_character")
mesh = SubResource("SphereMesh_character")
[node name="HUD" type="CanvasLayer" parent="."]
[node name="TimeLabel" type="Label" parent="HUD"]
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -220.0
offset_top = 20.0
offset_right = -20.0
offset_bottom = 60.0
grow_horizontal = 0
theme_override_font_sizes/font_size = 28
text = "06:00"
horizontal_alignment = 2
vertical_alignment = 1
script = ExtResource("6_8n3rl")
day_night_controller_path = NodePath("../../DirectionalLight3D")
[node name="WaterLabel" type="Label" parent="HUD"]
offset_left = 20.0
offset_top = 20.0
offset_right = 420.0
offset_bottom = 60.0
grow_horizontal = 0
theme_override_font_sizes/font_size = 22
text = "Water: 0 ml | Bladder: 0 ml"
script = ExtResource("8_8k3xv")
character_path = NodePath("../../GridCharacter")
+80
View File
@@ -0,0 +1,80 @@
@tool
extends MeshInstance3D
class_name GridHighlightController
@export var grid_manager_path: NodePath
@export var camera_path: NodePath
var _grid_manager: GridPlaneManager = null
var _camera: Camera3D = null
var _hovered_cell: Vector2i = Vector2i(-1, -1)
func _ready() -> void:
set_process(true)
_resolve_references()
_refresh_mesh()
if _grid_manager:
_grid_manager.grid_layout_changed.connect(_on_grid_layout_changed)
func _on_grid_layout_changed(_cells: Dictionary) -> void:
_refresh_mesh()
_hovered_cell = Vector2i(-1, -1)
visible = false
func _process(_delta: float) -> void:
if Engine.is_editor_hint():
return
if not _grid_manager or not _camera:
return
var viewport: Viewport = get_viewport()
if viewport == null:
return
var mouse_position: Vector2 = viewport.get_mouse_position()
_update_hovered_cell(mouse_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
func _refresh_mesh() -> void:
if not _grid_manager:
return
mesh = _grid_manager.make_highlight_mesh()
visible = false
func _update_hovered_cell(mouse_position: Vector2) -> void:
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:
_set_hover_visible(false)
return
var t: float = plane_normal.dot(plane_point - ray_origin) / denominator
if t <= 0.0:
_set_hover_visible(false)
return
var world_point: Vector3 = ray_origin + ray_direction * t
var cell_index: Vector2i = _grid_manager.world_to_cell(world_point, false)
if cell_index.x < 0 or cell_index.y < 0 or not _grid_manager.has_cell(cell_index):
_set_hover_visible(false)
return
if cell_index == _hovered_cell:
return
_hovered_cell = cell_index
var local_center: Vector3 = _grid_manager.cell_to_world(cell_index)
var target_position: Vector3 = _grid_manager.to_global(local_center)
global_position = target_position + plane_normal * 0.02
_set_hover_visible(true)
func _set_hover_visible(state: bool) -> void:
if not state:
visible = false
_hovered_cell = Vector2i(-1, -1)
else:
visible = true
@@ -0,0 +1 @@
uid://ehef5rrel4d6
+15
View File
@@ -0,0 +1,15 @@
[gd_scene load_steps=4 format=4 uid="uid://r703rwsvxsx2"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_home"]
albedo_color = Color(0.196078, 0.643137, 0.909804, 1)
metallic = 0.0
roughness = 1.0
[sub_resource type="CylinderMesh" id="CylinderMesh_home"]
radius = 0.45
height = 0.05
[node name="GridHome" type="MeshInstance3D"]
mesh = SubResource("CylinderMesh_home")
material_override = SubResource("StandardMaterial3D_home")
cast_shadow = 0
+38 -77
View File
@@ -60,6 +60,17 @@ var _ground_thickness: float = 0.4
get: get:
return _ground_thickness return _ground_thickness
var _ground_padding: float = 20.0
@export_range(0.0, 200.0, 1.0) var ground_padding: float:
set(value):
value = max(value, 0.0)
if is_equal_approx(value, _ground_padding):
return
_ground_padding = value
emit_signal("grid_layout_changed", _cells)
get:
return _ground_padding
var _show_grid_lines: bool = true var _show_grid_lines: bool = true
@export var show_grid_lines: bool: @export var show_grid_lines: bool:
set(value): set(value):
@@ -84,28 +95,31 @@ var _grid_line_color: Color = Color(0.2, 0.7, 1.0, 0.65)
const _LINE_HEIGHT_OFFSET := 0.02 const _LINE_HEIGHT_OFFSET := 0.02
const _SQRT3 := 1.7320508075688772 const _SQRT3 := 1.7320508075688772
@onready var _ground_mesh: MeshInstance3D = %GroundMesh
@onready var _ground_collision: CollisionShape3D = %GroundCollision
@onready var _grid_lines: MeshInstance3D = %GridLines @onready var _grid_lines: MeshInstance3D = %GridLines
@onready var _camera: Camera3D = %Camera3D
@onready var _hover_highlight: MeshInstance3D = %HoverHighlight
var _grid_size: Vector2 = Vector2.ZERO var _grid_size: Vector2 = Vector2.ZERO
var _cells: Dictionary = {} var _cells: Dictionary = {}
var _queued_rebuild: bool = false var _queued_rebuild: bool = false
var _hovered_cell: Vector2i = Vector2i(-1, -1)
var _hex_center_offset_local: Vector3 = Vector3.ZERO var _hex_center_offset_local: Vector3 = Vector3.ZERO
func _ready() -> void: func _ready() -> void:
set_process(true)
_queue_rebuild() _queue_rebuild()
func get_cells() -> Dictionary: func get_cells() -> Dictionary:
return _cells.duplicate() return _cells.duplicate()
func get_grid_size() -> Vector2:
return _grid_size
func get_grid_type() -> GridType:
return _grid_type
func get_cell_center(index: Vector2i) -> Vector3: func get_cell_center(index: Vector2i) -> Vector3:
return _cells.get(index, Vector3.ZERO) return _cells.get(index, Vector3.ZERO)
func has_cell(index: Vector2i) -> bool:
return _cells.has(index)
func cell_to_world(index: Vector2i) -> Vector3: func cell_to_world(index: Vector2i) -> Vector3:
return get_cell_center(index) return get_cell_center(index)
@@ -119,6 +133,22 @@ func for_each_cell(callback: Callable) -> void:
for index in _cells.keys(): for index in _cells.keys():
callback.call(index, _cells[index]) callback.call(index, _cells[index])
func get_cell_size_value() -> float:
return _cell_size
func get_ground_thickness() -> float:
return _ground_thickness
func get_ground_padding() -> float:
return _ground_padding
func make_highlight_mesh() -> Mesh:
if _grid_type == GridType.HEX:
return _build_hex_highlight_mesh()
var plane := PlaneMesh.new()
plane.size = Vector2(_cell_size, _cell_size)
return plane
func _queue_rebuild() -> void: func _queue_rebuild() -> void:
if _queued_rebuild: if _queued_rebuild:
return return
@@ -137,20 +167,9 @@ func _rebuild_grid() -> void:
_generate_hex_grid() _generate_hex_grid()
_: _:
_generate_square_grid() _generate_square_grid()
_update_ground_assets()
_update_grid_lines() _update_grid_lines()
_update_highlight_mesh()
_update_camera_view()
emit_signal("grid_layout_changed", _cells) emit_signal("grid_layout_changed", _cells)
func _update_ground_assets() -> void:
if _ground_mesh and _ground_mesh.mesh is PlaneMesh:
var plane_mesh := _ground_mesh.mesh as PlaneMesh
plane_mesh.size = _grid_size
if _ground_collision and _ground_collision.shape is BoxShape3D:
var box := _ground_collision.shape as BoxShape3D
box.size = Vector3(_grid_size.x, _ground_thickness, _grid_size.y)
func _generate_square_grid() -> void: func _generate_square_grid() -> void:
_hex_center_offset_local = Vector3.ZERO _hex_center_offset_local = Vector3.ZERO
_cells.clear() _cells.clear()
@@ -267,14 +286,8 @@ func _compute_hex_corners(center: Vector3, radius: float) -> Array[Vector3]:
return corners return corners
func _update_highlight_mesh() -> void: func _update_highlight_mesh() -> void:
if not _hover_highlight: # Deprecated internally; kept for compatibility.
return pass
if _grid_type == GridType.HEX:
_hover_highlight.mesh = _build_hex_highlight_mesh()
else:
var plane := PlaneMesh.new()
plane.size = Vector2(_cell_size, _cell_size)
_hover_highlight.mesh = plane
func _build_hex_highlight_mesh() -> ArrayMesh: func _build_hex_highlight_mesh() -> ArrayMesh:
var st := SurfaceTool.new() var st := SurfaceTool.new()
@@ -340,55 +353,3 @@ func _cube_round(cube: Vector3) -> Vector3:
else: else:
rz = -rx - ry rz = -rx - ry
return Vector3(rx, ry, rz) return Vector3(rx, ry, rz)
func _update_camera_view() -> void:
if not _camera:
return
var extent: float = max(_grid_size.x, _grid_size.y)
var half_extent: float = extent * 0.5
var distance: float = max(half_extent * 1.1, 6.0)
var height: float = max(half_extent * 0.8, 6.0)
var offset: Vector3 = Vector3(distance, height, distance)
var target: Vector3 = global_transform.origin
var offset_global: Vector3 = global_transform.basis * offset
var desired_position: Vector3 = target + offset_global
_camera.look_at_from_position(desired_position, target, Vector3.UP)
func _process(_delta: float) -> void:
if Engine.is_editor_hint():
return
var mouse_position: Vector2 = get_viewport().get_mouse_position()
_update_hovered_cell(mouse_position)
func _update_hovered_cell(mouse_position: Vector2) -> void:
if not _camera or not _hover_highlight:
return
var ray_origin: Vector3 = _camera.project_ray_origin(mouse_position)
var ray_direction: Vector3 = _camera.project_ray_normal(mouse_position)
var plane_normal: Vector3 = global_transform.basis.y.normalized()
var plane_point: Vector3 = global_transform.origin
var denominator: float = plane_normal.dot(ray_direction)
if abs(denominator) < 0.0001:
_clear_hover()
return
var t: float = plane_normal.dot(plane_point - ray_origin) / denominator
if t <= 0.0:
_clear_hover()
return
var world_point: Vector3 = ray_origin + ray_direction * t
var cell_index: Vector2i = world_to_cell(world_point, false)
if cell_index.x < 0 or cell_index.y < 0 or not _cells.has(cell_index):
_clear_hover()
return
if cell_index == _hovered_cell:
return
_hovered_cell = cell_index
var local_center: Vector3 = cell_to_world(cell_index)
var target_position: Vector3 = to_global(local_center)
_hover_highlight.global_position = target_position + plane_normal * 0.02
_hover_highlight.visible = true
func _clear_hover() -> void:
if _hover_highlight:
_hover_highlight.visible = false
_hovered_cell = Vector2i(-1, -1)
+56
View File
@@ -0,0 +1,56 @@
@tool
extends Label
class_name TimeDisplay
@export var day_night_controller_path: NodePath
@export var display_enabled: bool = true
var _day_night_controller: DayNightController = null
func _ready() -> void:
set_process(true)
_resolve_references()
_connect_controller()
_update_visibility()
_update_text()
func _process(_delta: float) -> void:
if Engine.is_editor_hint():
_update_text()
func set_display_enabled(value: bool) -> void:
display_enabled = value
_update_visibility()
_update_text()
func _resolve_references() -> void:
if day_night_controller_path != NodePath():
_day_night_controller = get_node_or_null(day_night_controller_path) as DayNightController
else:
_day_night_controller = get_parent() as DayNightController
func _connect_controller() -> void:
if _day_night_controller and not _day_night_controller.is_connected("time_changed", Callable(self, "_on_time_changed")):
_day_night_controller.time_changed.connect(_on_time_changed)
func _on_time_changed(_hours: float) -> void:
_update_text()
func _update_visibility() -> void:
visible = display_enabled and _day_night_controller != null
func _update_text() -> void:
if not display_enabled or not _day_night_controller:
text = ""
return
var hours: float = _day_night_controller.get_time_of_day_hours()
var total_hours: float = max(_day_night_controller.get_day_length_hours(), 0.001)
var clamped_hours: float = clamp(hours, 0.0, total_hours)
var whole_hours: int = int(clamped_hours)
var minutes: int = int(round((clamped_hours - float(whole_hours)) * 60.0))
if minutes == 60:
whole_hours += 1
minutes = 0
if whole_hours >= int(total_hours):
whole_hours = 0
text = "%02d:%02d" % [whole_hours, minutes]
+1
View File
@@ -0,0 +1 @@
uid://24uoe7g9nwp2
+45
View File
@@ -0,0 +1,45 @@
@tool
extends Label
class_name WaterDisplay
@export var character_path: NodePath
@export var show_bladder: bool = true
@export var show_food: bool = true
@export var show_water: bool = true
var _character: GridCharacterController = null
func _ready() -> void:
set_process(true)
_resolve_character()
if _character:
_character.vitals_changed.connect(_on_vitals_changed)
_on_vitals_changed(_character.get_water_level(), _character.get_bladder_level(), _character.is_water_low(), _character.is_bladder_full(), _character.get_food_level(), _character.is_food_low())
func _process(_delta: float) -> void:
if Engine.is_editor_hint():
_update_text(0.0, 0.0, false, false, 0.0, false)
func _resolve_character() -> void:
if character_path != NodePath():
_character = get_node_or_null(character_path) as GridCharacterController
else:
_character = get_parent() as GridCharacterController
func _on_vitals_changed(water_level: float, bladder_level: float, needs_water: bool, needs_bathroom: bool, food_level: float, needs_food: bool) -> void:
_update_text(water_level, bladder_level, needs_water, needs_bathroom, food_level, needs_food)
func _update_text(water_level: float, bladder_level: float, needs_water: bool, needs_bathroom: bool, food_level: float, needs_food: bool) -> void:
var parts: Array[String] = []
if show_water:
parts.append("Water: %4d ml%s" % [int(water_level), " !" if needs_water else ""])
if show_bladder:
parts.append("Bladder: %4d ml%s" % [int(bladder_level), " !" if needs_bathroom else ""])
if show_food:
parts.append("Food: %4d kcal%s" % [int(food_level), " !" if needs_food else ""])
if show_food and parts.size() >= 3:
var first_line := " | ".join(parts.slice(0, 2))
var second_line := " | ".join(parts.slice(2, parts.size()))
text = first_line + "\n" + second_line
else:
text = " | ".join(parts)
+1
View File
@@ -0,0 +1 @@
uid://25dqope89amz