feat: add deterministic goat routine
This commit is contained in:
@@ -74,6 +74,32 @@ func get_animal_candidates(action_id: StringName) -> Array[Dictionary]:
|
||||
}
|
||||
)
|
||||
)
|
||||
candidates.sort_custom(
|
||||
func(first: Dictionary, second: Dictionary) -> bool:
|
||||
return String(first["target_id"]) < String(second["target_id"])
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
func get_animal_routine_sites(species_id: StringName) -> Array[Dictionary]:
|
||||
var candidates: Array[Dictionary] = []
|
||||
for site in AnimalRoutineSite.get_all():
|
||||
if not site.supports_species(species_id):
|
||||
continue
|
||||
(
|
||||
candidates
|
||||
. append(
|
||||
{
|
||||
"site_id": String(site.site_id),
|
||||
"position": site.get_routine_position(),
|
||||
"display_name": site.display_name,
|
||||
}
|
||||
)
|
||||
)
|
||||
candidates.sort_custom(
|
||||
func(first: Dictionary, second: Dictionary) -> bool:
|
||||
return String(first["site_id"]) < String(second["site_id"])
|
||||
)
|
||||
return candidates
|
||||
|
||||
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
class_name AnimalNode
|
||||
extends Node3D
|
||||
|
||||
signal hunger_changed(animal_id: StringName, hunger: float)
|
||||
signal fed(animal_id: StringName, actor_id: int)
|
||||
signal reservation_changed(animal_id: StringName, agent_id: int)
|
||||
|
||||
static var _all: Array[AnimalNode] = []
|
||||
|
||||
@export var animal_id: StringName
|
||||
@export var display_name := "Animal"
|
||||
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
|
||||
@export_range(0.0, 100.0, 0.5) var initial_hunger := 70.0
|
||||
@export var initial_enabled := true
|
||||
@export var can_npcs_feed := true
|
||||
@export var can_player_feed := true
|
||||
@export var debug_label_enabled := true
|
||||
|
||||
@onready var interaction_point: Marker3D = get_node_or_null("InteractionPoint") as Marker3D
|
||||
|
||||
var state: AnimalStateRecord
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if animal_id.is_empty():
|
||||
push_error("AnimalNode at %s has empty animal_id" % get_path())
|
||||
initial_enabled = false
|
||||
_update_presentation()
|
||||
return
|
||||
var existing := get_by_id(animal_id)
|
||||
if existing != null:
|
||||
push_error(
|
||||
(
|
||||
"Duplicate AnimalNode animal_id '%s' at %s; first registered at %s"
|
||||
% [animal_id, get_path(), existing.get_path()]
|
||||
)
|
||||
)
|
||||
initial_enabled = false
|
||||
_update_presentation()
|
||||
return
|
||||
_all.append(self)
|
||||
add_to_group("animal_nodes")
|
||||
_try_register_with_simulation()
|
||||
_update_presentation()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_all.erase(self)
|
||||
_disconnect_state()
|
||||
state = null
|
||||
|
||||
|
||||
func bind_state(animal_state: AnimalStateRecord) -> bool:
|
||||
if animal_state == null or animal_state.get_animal_id() != animal_id:
|
||||
return false
|
||||
_disconnect_state()
|
||||
state = animal_state
|
||||
if not state.changed.is_connected(_on_state_changed):
|
||||
state.changed.connect(_on_state_changed)
|
||||
if not state.fed.is_connected(_on_state_fed):
|
||||
state.fed.connect(_on_state_fed)
|
||||
global_position = state.get_position()
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null and visual.has_method("reset_transient_feedback"):
|
||||
visual.reset_transient_feedback()
|
||||
_update_presentation()
|
||||
hunger_changed.emit(animal_id, state.get_hunger())
|
||||
return true
|
||||
|
||||
|
||||
func _disconnect_state() -> void:
|
||||
if state == null:
|
||||
return
|
||||
if state.changed.is_connected(_on_state_changed):
|
||||
state.changed.disconnect(_on_state_changed)
|
||||
if state.fed.is_connected(_on_state_fed):
|
||||
state.fed.disconnect(_on_state_fed)
|
||||
|
||||
|
||||
func _try_register_with_simulation() -> void:
|
||||
var managers := get_tree().get_nodes_in_group("simulation_manager")
|
||||
if managers.size() != 1:
|
||||
return
|
||||
var manager := managers[0]
|
||||
if "animal_care" in manager and manager.animal_care != null:
|
||||
manager.animal_care.register_node(self)
|
||||
|
||||
|
||||
func get_interaction_position() -> Vector3:
|
||||
return interaction_point.global_position if interaction_point != null else global_position
|
||||
|
||||
|
||||
func supports_action(action_id: StringName) -> bool:
|
||||
return action_id == SimulationIds.ACTION_FEED_ANIMAL
|
||||
|
||||
|
||||
func get_hunger() -> float:
|
||||
return state.get_hunger() if state != null else initial_hunger
|
||||
|
||||
|
||||
func needs_feed() -> bool:
|
||||
if state != null:
|
||||
return state.needs_feed()
|
||||
return initial_enabled and initial_hunger >= AnimalStateRecord.FEED_THRESHOLD
|
||||
|
||||
|
||||
func is_enabled() -> bool:
|
||||
return state.is_enabled() if state != null else initial_enabled
|
||||
|
||||
|
||||
func _on_state_changed(changed_state: AnimalStateRecord) -> void:
|
||||
if changed_state != state:
|
||||
return
|
||||
if not global_position.is_equal_approx(state.get_position()):
|
||||
global_position = state.get_position()
|
||||
hunger_changed.emit(animal_id, state.get_hunger())
|
||||
reservation_changed.emit(animal_id, state.get_reserved_by())
|
||||
_update_presentation()
|
||||
|
||||
|
||||
func _on_state_fed(fed_state: AnimalStateRecord, actor_id: int) -> void:
|
||||
if fed_state != state:
|
||||
return
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null and visual.has_method("play_fed_response"):
|
||||
visual.play_fed_response()
|
||||
fed.emit(animal_id, actor_id)
|
||||
|
||||
|
||||
func _update_presentation() -> void:
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null:
|
||||
visual.visible = is_enabled()
|
||||
if visual.has_method("set_hungry"):
|
||||
visual.set_hungry(needs_feed())
|
||||
if not has_node("DebugLabel"):
|
||||
return
|
||||
var label := $DebugLabel as Label3D
|
||||
label.visible = debug_label_enabled
|
||||
if not debug_label_enabled:
|
||||
return
|
||||
var text := "%s\n%s · hunger %.0f" % [display_name, animal_id, get_hunger()]
|
||||
if needs_feed():
|
||||
text += "\nHUNGRY"
|
||||
if state != null and state.get_reserved_by() >= 0:
|
||||
text += "\nheld:%d" % state.get_reserved_by()
|
||||
if state == null:
|
||||
text += "\nUNBOUND"
|
||||
label.text = text
|
||||
|
||||
|
||||
func set_debug_label_enabled(is_enabled: bool) -> void:
|
||||
debug_label_enabled = is_enabled
|
||||
_update_presentation()
|
||||
|
||||
|
||||
static func get_by_id(search_id: StringName) -> AnimalNode:
|
||||
for node in _all:
|
||||
if node.animal_id == search_id:
|
||||
return node
|
||||
return null
|
||||
|
||||
|
||||
static func get_all() -> Array[AnimalNode]:
|
||||
return _all.duplicate()
|
||||
@@ -0,0 +1,317 @@
|
||||
class_name AnimalNode
|
||||
extends Node3D
|
||||
|
||||
signal hunger_changed(animal_id: StringName, hunger: float)
|
||||
signal fed(animal_id: StringName, actor_id: int)
|
||||
signal reservation_changed(animal_id: StringName, agent_id: int)
|
||||
signal position_changed(animal_id: StringName, active_position: Vector3)
|
||||
signal routine_arrived(animal_id: StringName, site_id: StringName, arrival_position: Vector3)
|
||||
signal navigation_failed(animal_id: StringName, site_id: StringName)
|
||||
|
||||
static var _all: Array[AnimalNode] = []
|
||||
|
||||
@export var animal_id: StringName
|
||||
@export var display_name := "Animal"
|
||||
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
|
||||
@export_range(0.0, 100.0, 0.5) var initial_hunger := 70.0
|
||||
@export var initial_enabled := true
|
||||
@export var can_npcs_feed := true
|
||||
@export var can_player_feed := true
|
||||
@export var debug_label_enabled := true
|
||||
@export var initial_routine_site_id: StringName
|
||||
@export_range(0, 10000, 1) var initial_next_routine_tick := 12
|
||||
@export_range(0.1, 10.0, 0.1) var move_speed := 1.6
|
||||
@export_range(0.05, 1.0, 0.05) var waypoint_reached_distance := 0.25
|
||||
@export_range(0.05, 1.0, 0.05) var arrival_distance := 0.3
|
||||
@export_range(0.1, 20.0, 0.1) var rotation_speed := 7.0
|
||||
|
||||
@onready var interaction_point: Marker3D = get_node_or_null("InteractionPoint") as Marker3D
|
||||
|
||||
var state: AnimalStateRecord
|
||||
var current_path := PackedVector3Array()
|
||||
var path_index := 0
|
||||
var navigation_site_id: StringName
|
||||
var navigation_target := Vector3.INF
|
||||
var navigation_request_id := 0
|
||||
var path_pending := false
|
||||
var has_reported_navigation_result := true
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if animal_id.is_empty():
|
||||
push_error("AnimalNode at %s has empty animal_id" % get_path())
|
||||
initial_enabled = false
|
||||
_update_presentation()
|
||||
return
|
||||
var existing := get_by_id(animal_id)
|
||||
if existing != null:
|
||||
push_error(
|
||||
(
|
||||
"Duplicate AnimalNode animal_id '%s' at %s; first registered at %s"
|
||||
% [animal_id, get_path(), existing.get_path()]
|
||||
)
|
||||
)
|
||||
initial_enabled = false
|
||||
_update_presentation()
|
||||
return
|
||||
_all.append(self)
|
||||
add_to_group("animal_nodes")
|
||||
add_to_group("grass_interactors")
|
||||
_try_register_with_simulation()
|
||||
_update_presentation()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
navigation_request_id += 1
|
||||
_all.erase(self)
|
||||
_disconnect_state()
|
||||
state = null
|
||||
|
||||
|
||||
func bind_state(animal_state: AnimalStateRecord) -> bool:
|
||||
if animal_state == null or animal_state.get_animal_id() != animal_id:
|
||||
return false
|
||||
_disconnect_state()
|
||||
state = animal_state
|
||||
if not state.changed.is_connected(_on_state_changed):
|
||||
state.changed.connect(_on_state_changed)
|
||||
if not state.fed.is_connected(_on_state_fed):
|
||||
state.fed.connect(_on_state_fed)
|
||||
global_position = state.get_position()
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null and visual.has_method("reset_transient_feedback"):
|
||||
visual.reset_transient_feedback()
|
||||
_stop_navigation()
|
||||
call_deferred("_sync_navigation_from_state")
|
||||
_update_presentation()
|
||||
hunger_changed.emit(animal_id, state.get_hunger())
|
||||
return true
|
||||
|
||||
|
||||
func _disconnect_state() -> void:
|
||||
if state == null:
|
||||
return
|
||||
if state.changed.is_connected(_on_state_changed):
|
||||
state.changed.disconnect(_on_state_changed)
|
||||
if state.fed.is_connected(_on_state_fed):
|
||||
state.fed.disconnect(_on_state_fed)
|
||||
|
||||
|
||||
func _try_register_with_simulation() -> void:
|
||||
var managers := get_tree().get_nodes_in_group("simulation_manager")
|
||||
if managers.size() != 1:
|
||||
return
|
||||
var manager := managers[0]
|
||||
if "animal_care" in manager and manager.animal_care != null:
|
||||
manager.animal_care.register_node(self)
|
||||
|
||||
|
||||
func get_interaction_position() -> Vector3:
|
||||
return interaction_point.global_position if interaction_point != null else global_position
|
||||
|
||||
|
||||
func supports_action(action_id: StringName) -> bool:
|
||||
return action_id == SimulationIds.ACTION_FEED_ANIMAL
|
||||
|
||||
|
||||
func get_hunger() -> float:
|
||||
return state.get_hunger() if state != null else initial_hunger
|
||||
|
||||
|
||||
func needs_feed() -> bool:
|
||||
if state != null:
|
||||
return state.needs_feed()
|
||||
return initial_enabled and initial_hunger >= AnimalStateRecord.FEED_THRESHOLD
|
||||
|
||||
|
||||
func is_enabled() -> bool:
|
||||
return state.is_enabled() if state != null else initial_enabled
|
||||
|
||||
|
||||
func is_navigating() -> bool:
|
||||
return not navigation_site_id.is_empty() and not has_reported_navigation_result
|
||||
|
||||
|
||||
func get_navigation_path() -> PackedVector3Array:
|
||||
return current_path.duplicate()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if (
|
||||
state == null
|
||||
or not state.has_active_travel_target()
|
||||
or navigation_site_id.is_empty()
|
||||
or has_reported_navigation_result
|
||||
):
|
||||
return
|
||||
if path_pending:
|
||||
return
|
||||
if _horizontal_distance_to(navigation_target) <= arrival_distance:
|
||||
_report_arrival_once()
|
||||
return
|
||||
if current_path.is_empty() or path_index >= current_path.size():
|
||||
_report_navigation_failure_once()
|
||||
return
|
||||
|
||||
var next_position := current_path[path_index]
|
||||
var offset := next_position - global_position
|
||||
if offset.length() <= waypoint_reached_distance:
|
||||
path_index += 1
|
||||
return
|
||||
|
||||
var direction := offset.normalized()
|
||||
global_position = global_position.move_toward(next_position, move_speed * delta)
|
||||
position_changed.emit(animal_id, global_position)
|
||||
var target_angle := atan2(direction.x, direction.z)
|
||||
rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta)
|
||||
|
||||
|
||||
func _on_state_changed(changed_state: AnimalStateRecord) -> void:
|
||||
if changed_state != state:
|
||||
return
|
||||
if not global_position.is_equal_approx(state.get_position()):
|
||||
global_position = state.get_position()
|
||||
hunger_changed.emit(animal_id, state.get_hunger())
|
||||
reservation_changed.emit(animal_id, state.get_reserved_by())
|
||||
_sync_navigation_from_state()
|
||||
_update_presentation()
|
||||
|
||||
|
||||
func _on_state_fed(fed_state: AnimalStateRecord, actor_id: int) -> void:
|
||||
if fed_state != state:
|
||||
return
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null and visual.has_method("play_fed_response"):
|
||||
visual.play_fed_response()
|
||||
fed.emit(animal_id, actor_id)
|
||||
|
||||
|
||||
func _update_presentation() -> void:
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null:
|
||||
visual.visible = is_enabled()
|
||||
if visual.has_method("set_hungry"):
|
||||
visual.set_hungry(needs_feed())
|
||||
if visual.has_method("set_moving"):
|
||||
visual.set_moving(is_navigating())
|
||||
if not has_node("DebugLabel"):
|
||||
return
|
||||
var label := $DebugLabel as Label3D
|
||||
label.visible = debug_label_enabled
|
||||
if not debug_label_enabled:
|
||||
return
|
||||
var text := "%s\n%s · hunger %.0f" % [display_name, animal_id, get_hunger()]
|
||||
if needs_feed():
|
||||
text += "\nHUNGRY"
|
||||
if state != null and state.get_reserved_by() >= 0:
|
||||
text += "\nheld:%d" % state.get_reserved_by()
|
||||
if state != null and state.has_active_travel_target():
|
||||
text += "\n→ %s" % state.get_travel_target_site_id()
|
||||
elif state != null and not state.get_routine_site_id().is_empty():
|
||||
text += "\n@ %s" % state.get_routine_site_id()
|
||||
if state == null:
|
||||
text += "\nUNBOUND"
|
||||
label.text = text
|
||||
|
||||
|
||||
func set_debug_label_enabled(is_enabled: bool) -> void:
|
||||
debug_label_enabled = is_enabled
|
||||
_update_presentation()
|
||||
|
||||
|
||||
func _sync_navigation_from_state() -> void:
|
||||
if state == null or not state.has_active_travel_target():
|
||||
if not navigation_site_id.is_empty():
|
||||
_stop_navigation()
|
||||
return
|
||||
var target_site_id := state.get_travel_target_site_id()
|
||||
var target_position := state.get_travel_target_position()
|
||||
if (
|
||||
navigation_site_id == target_site_id
|
||||
and navigation_target.is_equal_approx(target_position)
|
||||
and not has_reported_navigation_result
|
||||
):
|
||||
return
|
||||
_request_navigation(target_site_id, target_position)
|
||||
|
||||
|
||||
func _request_navigation(site_id: StringName, target_position: Vector3) -> void:
|
||||
navigation_request_id += 1
|
||||
var request_id := navigation_request_id
|
||||
navigation_site_id = site_id
|
||||
navigation_target = target_position
|
||||
current_path = PackedVector3Array()
|
||||
path_index = 0
|
||||
path_pending = true
|
||||
has_reported_navigation_result = false
|
||||
_update_presentation()
|
||||
|
||||
await get_tree().physics_frame
|
||||
if request_id != navigation_request_id or state == null:
|
||||
return
|
||||
current_path = NavigationServer3D.map_get_path(
|
||||
get_world_3d().navigation_map, global_position, target_position, true
|
||||
)
|
||||
path_pending = false
|
||||
if current_path.is_empty():
|
||||
_report_navigation_failure_once()
|
||||
|
||||
|
||||
func _stop_navigation() -> void:
|
||||
navigation_request_id += 1
|
||||
current_path = PackedVector3Array()
|
||||
path_index = 0
|
||||
navigation_site_id = &""
|
||||
navigation_target = Vector3.INF
|
||||
path_pending = false
|
||||
has_reported_navigation_result = true
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null and visual.has_method("set_moving"):
|
||||
visual.set_moving(false)
|
||||
|
||||
|
||||
func _report_arrival_once() -> void:
|
||||
if has_reported_navigation_result:
|
||||
return
|
||||
has_reported_navigation_result = true
|
||||
global_position = navigation_target
|
||||
position_changed.emit(animal_id, global_position)
|
||||
var arrived_site_id := navigation_site_id
|
||||
current_path = PackedVector3Array()
|
||||
path_index = 0
|
||||
path_pending = false
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null and visual.has_method("set_moving"):
|
||||
visual.set_moving(false)
|
||||
routine_arrived.emit(animal_id, arrived_site_id, global_position)
|
||||
|
||||
|
||||
func _report_navigation_failure_once() -> void:
|
||||
if has_reported_navigation_result:
|
||||
return
|
||||
has_reported_navigation_result = true
|
||||
var failed_site_id := navigation_site_id
|
||||
current_path = PackedVector3Array()
|
||||
path_index = 0
|
||||
path_pending = false
|
||||
var visual := get_node_or_null("Visual")
|
||||
if visual != null and visual.has_method("set_moving"):
|
||||
visual.set_moving(false)
|
||||
navigation_failed.emit(animal_id, failed_site_id)
|
||||
|
||||
|
||||
func _horizontal_distance_to(target_position: Vector3) -> float:
|
||||
return Vector2(global_position.x, global_position.z).distance_to(
|
||||
Vector2(target_position.x, target_position.z)
|
||||
)
|
||||
|
||||
|
||||
static func get_by_id(search_id: StringName) -> AnimalNode:
|
||||
for node in _all:
|
||||
if node.animal_id == search_id:
|
||||
return node
|
||||
return null
|
||||
|
||||
|
||||
static func get_all() -> Array[AnimalNode]:
|
||||
return _all.duplicate()
|
||||
@@ -0,0 +1,76 @@
|
||||
@tool
|
||||
class_name AnimalRoutineSite
|
||||
extends Marker3D
|
||||
|
||||
static var _all: Array[AnimalRoutineSite] = []
|
||||
|
||||
@export var site_id: StringName
|
||||
@export var display_name := "Animal site"
|
||||
@export var species_id: StringName = SimulationIds.SPECIES_GOAT
|
||||
@export var debug_label_enabled := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_update_debug_label()
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
if site_id.is_empty():
|
||||
push_error("AnimalRoutineSite at %s has an empty site_id" % get_path())
|
||||
return
|
||||
var existing := get_by_id(site_id)
|
||||
if existing != null:
|
||||
push_error(
|
||||
(
|
||||
"Duplicate AnimalRoutineSite site_id '%s' at %s; first registered at %s"
|
||||
% [site_id, get_path(), existing.get_path()]
|
||||
)
|
||||
)
|
||||
return
|
||||
_all.append(self)
|
||||
add_to_group("animal_routine_sites")
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
_all.erase(self)
|
||||
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
if site_id.is_empty():
|
||||
warnings.append("Assign a stable site_id before using this routine site.")
|
||||
if species_id.is_empty():
|
||||
warnings.append("Assign the species that can use this routine site.")
|
||||
return warnings
|
||||
|
||||
|
||||
func supports_species(candidate_species_id: StringName) -> bool:
|
||||
return candidate_species_id == species_id
|
||||
|
||||
|
||||
func get_routine_position() -> Vector3:
|
||||
return global_position
|
||||
|
||||
|
||||
func set_debug_label_enabled(is_enabled: bool) -> void:
|
||||
debug_label_enabled = is_enabled
|
||||
_update_debug_label()
|
||||
|
||||
|
||||
func _update_debug_label() -> void:
|
||||
var label := get_node_or_null("DebugLabel") as Label3D
|
||||
if label == null:
|
||||
return
|
||||
label.visible = debug_label_enabled
|
||||
label.text = "%s\n%s" % [display_name, site_id]
|
||||
|
||||
|
||||
static func get_by_id(search_id: StringName) -> AnimalRoutineSite:
|
||||
for site in _all:
|
||||
if site.site_id == search_id:
|
||||
return site
|
||||
return null
|
||||
|
||||
|
||||
static func get_all() -> Array[AnimalRoutineSite]:
|
||||
return _all.duplicate()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpx5saq28qf4o
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_scene load_steps=26 format=3]
|
||||
[gd_scene load_steps=24 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/animals/AnimalNode.gd" id="1_animal"]
|
||||
[ext_resource type="Script" path="res://world/jajce/CozyGoat.gd" id="2_visual"]
|
||||
[ext_resource type="Script" path="res://world/animals/animal_node.gd" id="1_animal"]
|
||||
[ext_resource type="Script" path="res://world/animals/goat/cozy_goat_visual.gd" id="2_visual"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_wool"]
|
||||
albedo_color = Color(0.88, 0.82, 0.67, 1)
|
||||
@@ -23,10 +23,6 @@ roughness = 0.86
|
||||
albedo_color = Color(0.12, 0.5, 0.5, 1)
|
||||
roughness = 0.72
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_wood"]
|
||||
albedo_color = Color(0.35, 0.19, 0.09, 1)
|
||||
roughness = 0.96
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_hunger"]
|
||||
transparency = 1
|
||||
shading_mode = 0
|
||||
@@ -124,13 +120,6 @@ outer_radius = 0.32
|
||||
rings = 14
|
||||
ring_segments = 8
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_bowl"]
|
||||
material = SubResource("Material_wood")
|
||||
top_radius = 0.36
|
||||
bottom_radius = 0.28
|
||||
height = 0.13
|
||||
radial_segments = 12
|
||||
|
||||
[sub_resource type="TorusMesh" id="Mesh_hunger_bowl"]
|
||||
material = SubResource("Material_hunger")
|
||||
inner_radius = 0.18
|
||||
@@ -164,7 +153,9 @@ script = ExtResource("1_animal")
|
||||
animal_id = &"goat_dunja"
|
||||
display_name = "Dunja"
|
||||
species_id = &"goat"
|
||||
initial_hunger = 72.0
|
||||
initial_hunger = 40.0
|
||||
initial_routine_site_id = &"dunja_shelter"
|
||||
initial_next_routine_tick = 8
|
||||
|
||||
[node name="Visual" type="Node3D" parent="."]
|
||||
script = ExtResource("2_visual")
|
||||
@@ -276,10 +267,6 @@ rotation_degrees = Vector3(60, 0, 0)
|
||||
position = Vector3(0, 0.18, 0)
|
||||
mesh = SubResource("Mesh_tail")
|
||||
|
||||
[node name="FeedBowl" type="MeshInstance3D" parent="Visual"]
|
||||
position = Vector3(0.72, 0.08, 1.43)
|
||||
mesh = SubResource("Mesh_bowl")
|
||||
|
||||
[node name="HungerCueRoot" type="Node3D" parent="Visual"]
|
||||
position = Vector3(0, 2.05, 0.18)
|
||||
|
||||
@@ -7,15 +7,22 @@ const FED_RESPONSE_BASE_Y := 1.8
|
||||
@onready var tail_root: Node3D = $TailRoot
|
||||
@onready var hunger_cue_root: Node3D = $HungerCueRoot
|
||||
@onready var fed_response_root: Node3D = $FedResponseRoot
|
||||
@onready var leg_front_left: Node3D = $LegFrontLeft
|
||||
@onready var leg_front_right: Node3D = $LegFrontRight
|
||||
@onready var leg_back_left: Node3D = $LegBackLeft
|
||||
@onready var leg_back_right: Node3D = $LegBackRight
|
||||
|
||||
var idle_phase := 0.0
|
||||
var is_hungry := false
|
||||
var is_moving := false
|
||||
var fed_tween: Tween
|
||||
var head_base_position: Vector3
|
||||
var visual_base_position: Vector3
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
head_base_position = head_root.position
|
||||
visual_base_position = position
|
||||
reset_transient_feedback()
|
||||
|
||||
|
||||
@@ -23,11 +30,18 @@ func _process(delta: float) -> void:
|
||||
idle_phase = fmod(idle_phase + delta, TAU)
|
||||
var breath := sin(idle_phase * 1.7) * 0.025
|
||||
var hungry_drop := -0.12 if is_hungry else 0.0
|
||||
var walking_bob := absf(sin(idle_phase * 5.4)) * 0.045 if is_moving else 0.0
|
||||
position.y = visual_base_position.y + walking_bob
|
||||
head_root.position.y = head_base_position.y + breath + hungry_drop
|
||||
head_root.rotation_degrees.x = (
|
||||
8.0 + sin(idle_phase * 1.2) * 2.0 if is_hungry else -3.0 + sin(idle_phase * 1.4) * 2.5
|
||||
)
|
||||
tail_root.rotation_degrees.z = sin(idle_phase * 3.1) * (4.0 if is_hungry else 13.0)
|
||||
var stride := sin(idle_phase * 5.4) * 14.0 if is_moving else 0.0
|
||||
leg_front_left.rotation_degrees.x = stride
|
||||
leg_back_right.rotation_degrees.x = stride
|
||||
leg_front_right.rotation_degrees.x = -stride
|
||||
leg_back_left.rotation_degrees.x = -stride
|
||||
if hunger_cue_root.visible:
|
||||
hunger_cue_root.position.y = HUNGER_CUE_BASE_Y + sin(idle_phase * 2.2) * 0.07
|
||||
|
||||
@@ -37,6 +51,16 @@ func set_hungry(value: bool) -> void:
|
||||
hunger_cue_root.visible = value
|
||||
|
||||
|
||||
func set_moving(value: bool) -> void:
|
||||
is_moving = value
|
||||
if not value:
|
||||
position = visual_base_position
|
||||
leg_front_left.rotation_degrees.x = 0.0
|
||||
leg_front_right.rotation_degrees.x = 0.0
|
||||
leg_back_left.rotation_degrees.x = 0.0
|
||||
leg_back_right.rotation_degrees.x = 0.0
|
||||
|
||||
|
||||
func reset_transient_feedback() -> void:
|
||||
if fed_tween != null:
|
||||
fed_tween.kill()
|
||||
@@ -0,0 +1,203 @@
|
||||
[gd_scene load_steps=19 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/animals/animal_routine_site.gd" id="1_site"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_timber"]
|
||||
albedo_color = Color(0.43, 0.26, 0.12, 1)
|
||||
roughness = 0.96
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_roof"]
|
||||
albedo_color = Color(0.4, 0.38, 0.18, 1)
|
||||
roughness = 0.98
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_fence"]
|
||||
albedo_color = Color(0.55, 0.38, 0.19, 1)
|
||||
roughness = 0.95
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_clover"]
|
||||
albedo_color = Color(0.29, 0.55, 0.24, 1)
|
||||
roughness = 0.92
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_bowl"]
|
||||
albedo_color = Color(0.24, 0.48, 0.48, 1)
|
||||
roughness = 0.82
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_straw"]
|
||||
albedo_color = Color(0.7, 0.53, 0.25, 1)
|
||||
roughness = 0.98
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="Material_glow"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(1, 0.72, 0.3, 0.9)
|
||||
emission_enabled = true
|
||||
emission = Color(1, 0.46, 0.12, 1)
|
||||
emission_energy_multiplier = 0.42
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_post"]
|
||||
material = SubResource("Material_timber")
|
||||
size = Vector3(0.18, 2.05, 0.18)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_beam"]
|
||||
material = SubResource("Material_timber")
|
||||
size = Vector3(2.75, 0.16, 0.18)
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_roof"]
|
||||
material = SubResource("Material_roof")
|
||||
size = Vector3(1.65, 0.13, 2.55)
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_fence_post"]
|
||||
material = SubResource("Material_fence")
|
||||
top_radius = 0.08
|
||||
bottom_radius = 0.11
|
||||
height = 0.92
|
||||
radial_segments = 8
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_fence_rail"]
|
||||
material = SubResource("Material_fence")
|
||||
size = Vector3(2.55, 0.1, 0.1)
|
||||
|
||||
[sub_resource type="SphereMesh" id="Mesh_clover"]
|
||||
material = SubResource("Material_clover")
|
||||
radius = 0.24
|
||||
height = 0.16
|
||||
radial_segments = 8
|
||||
rings = 4
|
||||
|
||||
[sub_resource type="CylinderMesh" id="Mesh_bowl"]
|
||||
material = SubResource("Material_bowl")
|
||||
top_radius = 0.34
|
||||
bottom_radius = 0.27
|
||||
height = 0.13
|
||||
radial_segments = 12
|
||||
|
||||
[sub_resource type="BoxMesh" id="Mesh_straw"]
|
||||
material = SubResource("Material_straw")
|
||||
size = Vector3(1.65, 0.09, 1.15)
|
||||
|
||||
[sub_resource type="SphereMesh" id="Mesh_glow"]
|
||||
material = SubResource("Material_glow")
|
||||
radius = 0.08
|
||||
height = 0.13
|
||||
radial_segments = 8
|
||||
rings = 4
|
||||
|
||||
[node name="DunjaHabitat" type="Node3D"]
|
||||
|
||||
[node name="Shelter" type="Node3D" parent="."]
|
||||
|
||||
[node name="PostFrontLeft" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(-1.18, 1.02, 2.2)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostFrontRight" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(1.18, 1.02, 2.2)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostBackLeft" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(-1.18, 1.02, 0.25)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="PostBackRight" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(1.18, 1.02, 0.25)
|
||||
mesh = SubResource("Mesh_post")
|
||||
|
||||
[node name="FrontBeam" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(0, 2.02, 2.2)
|
||||
mesh = SubResource("Mesh_beam")
|
||||
|
||||
[node name="BackBeam" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(0, 2.02, 0.25)
|
||||
mesh = SubResource("Mesh_beam")
|
||||
|
||||
[node name="RoofLeft" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(-0.72, 2.31, 1.22)
|
||||
rotation_degrees = Vector3(0, 0, -19)
|
||||
mesh = SubResource("Mesh_roof")
|
||||
|
||||
[node name="RoofRight" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(0.72, 2.31, 1.22)
|
||||
rotation_degrees = Vector3(0, 0, 19)
|
||||
mesh = SubResource("Mesh_roof")
|
||||
|
||||
[node name="StrawBed" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(0, 0.05, 1.15)
|
||||
mesh = SubResource("Mesh_straw")
|
||||
|
||||
[node name="FeedBowl" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(0.72, 0.08, 2.52)
|
||||
mesh = SubResource("Mesh_bowl")
|
||||
|
||||
[node name="Lantern" type="MeshInstance3D" parent="Shelter"]
|
||||
position = Vector3(0, 1.87, 2.1)
|
||||
mesh = SubResource("Mesh_glow")
|
||||
|
||||
[node name="ShelterSite" type="Marker3D" parent="."]
|
||||
position = Vector3(0, 0, 1.5)
|
||||
script = ExtResource("1_site")
|
||||
site_id = &"dunja_shelter"
|
||||
display_name = "Warm shelter"
|
||||
species_id = &"goat"
|
||||
|
||||
[node name="DebugLabel" type="Label3D" parent="ShelterSite"]
|
||||
position = Vector3(0, 2.45, 0)
|
||||
billboard = 1
|
||||
no_depth_test = true
|
||||
font_size = 20
|
||||
outline_size = 5
|
||||
modulate = Color(1, 0.78, 0.45, 1)
|
||||
pixel_size = 0.006
|
||||
|
||||
[node name="Pasture" type="Node3D" parent="."]
|
||||
position = Vector3(-4, 0, -2.5)
|
||||
|
||||
[node name="PostNorthLeft" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(-1.3, 0.46, -1)
|
||||
mesh = SubResource("Mesh_fence_post")
|
||||
|
||||
[node name="PostNorthRight" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(1.3, 0.46, -1)
|
||||
mesh = SubResource("Mesh_fence_post")
|
||||
|
||||
[node name="NorthRail" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(0, 0.58, -1)
|
||||
mesh = SubResource("Mesh_fence_rail")
|
||||
|
||||
[node name="PostWestSouth" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(-1.3, 0.46, 1)
|
||||
mesh = SubResource("Mesh_fence_post")
|
||||
|
||||
[node name="WestRail" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(-1.3, 0.58, 0)
|
||||
rotation_degrees = Vector3(0, 90, 0)
|
||||
mesh = SubResource("Mesh_fence_rail")
|
||||
|
||||
[node name="CloverA" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(-0.45, 0.08, -0.3)
|
||||
scale = Vector3(1.1, 1, 0.8)
|
||||
mesh = SubResource("Mesh_clover")
|
||||
|
||||
[node name="CloverB" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(0.35, 0.08, 0.2)
|
||||
scale = Vector3(0.85, 1, 1.15)
|
||||
mesh = SubResource("Mesh_clover")
|
||||
|
||||
[node name="CloverC" type="MeshInstance3D" parent="Pasture"]
|
||||
position = Vector3(0.78, 0.07, -0.48)
|
||||
scale = Vector3(0.72, 0.9, 0.72)
|
||||
mesh = SubResource("Mesh_clover")
|
||||
|
||||
[node name="PastureSite" type="Marker3D" parent="."]
|
||||
position = Vector3(-4, 0, -2.5)
|
||||
script = ExtResource("1_site")
|
||||
site_id = &"dunja_pasture"
|
||||
display_name = "Clover pasture"
|
||||
species_id = &"goat"
|
||||
|
||||
[node name="DebugLabel" type="Label3D" parent="PastureSite"]
|
||||
position = Vector3(0, 2.15, 0)
|
||||
billboard = 1
|
||||
no_depth_test = true
|
||||
font_size = 20
|
||||
outline_size = 5
|
||||
modulate = Color(0.72, 0.95, 0.54, 1)
|
||||
pixel_size = 0.006
|
||||
@@ -3,14 +3,14 @@
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="1_world"]
|
||||
[ext_resource type="Script" path="res://world/jajce/beauty_camera.gd" id="2_camera"]
|
||||
|
||||
[node name="JajceGoatLookdev" type="Node3D"]
|
||||
[node name="DunjaLookdev" type="Node3D"]
|
||||
|
||||
[node name="JajceWorld" parent="." instance=ExtResource("1_world")]
|
||||
|
||||
[node name="GoatCamera" type="Camera3D" parent="."]
|
||||
position = Vector3(0.2, 2.9, -7)
|
||||
position = Vector3(5, 4.5, -25)
|
||||
current = true
|
||||
fov = 32.0
|
||||
fov = 38.0
|
||||
script = ExtResource("2_camera")
|
||||
focus_point = Vector3(-4, 1.15, -11.5)
|
||||
focus_point = Vector3(-2, 1.05, -17.5)
|
||||
animate_orbit = false
|
||||
@@ -363,6 +363,10 @@ func _apply_debug_overlay_visibility() -> void:
|
||||
if node.has_method("set_debug_label_enabled"):
|
||||
node.set_debug_label_enabled(debug_overlay_visible)
|
||||
|
||||
for node in AnimalRoutineSite.get_all():
|
||||
if node.has_method("set_debug_label_enabled"):
|
||||
node.set_debug_label_enabled(debug_overlay_visible)
|
||||
|
||||
for node in StorageNode.get_all():
|
||||
if node.has_method("set_debug_label_enabled"):
|
||||
node.set_debug_label_enabled(debug_overlay_visible)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[gd_scene load_steps=60 format=3]
|
||||
[gd_scene load_steps=61 format=3]
|
||||
|
||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||
@@ -27,7 +27,8 @@
|
||||
[ext_resource type="Material" path="res://world/jajce/materials/cozy_grass_process_material.tres" id="25_grass_process"]
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/RiverbankResourceCluster.tscn" id="26_resource_cluster"]
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/ForestEdgeResourceCluster.tscn" id="27_forest_cluster"]
|
||||
[ext_resource type="PackedScene" path="res://world/jajce/CozyGoat.tscn" id="28_goat"]
|
||||
[ext_resource type="PackedScene" path="res://world/animals/goat/cozy_goat.tscn" id="28_goat"]
|
||||
[ext_resource type="PackedScene" path="res://world/animals/goat/dunja_habitat.tscn" id="29_habitat"]
|
||||
|
||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||
_shader_parameters = {
|
||||
@@ -462,8 +463,13 @@ phase = 4.4
|
||||
[node name="Animals" type="Node3D" parent="WorldObjects"]
|
||||
|
||||
[node name="Dunja" parent="WorldObjects/Animals" instance=ExtResource("28_goat")]
|
||||
position = Vector3(-4, 0, -11.5)
|
||||
rotation_degrees = Vector3(0, -18, 0)
|
||||
position = Vector3(0, 0, -15.5)
|
||||
rotation_degrees = Vector3(0, 150, 0)
|
||||
|
||||
[node name="AnimalHabitats" type="Node3D" parent="WorldObjects"]
|
||||
|
||||
[node name="DunjaHabitat" parent="WorldObjects/AnimalHabitats" instance=ExtResource("29_habitat")]
|
||||
position = Vector3(0, 0, -17)
|
||||
|
||||
[node name="ResourceNodes" type="Node3D" parent="WorldObjects"]
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ func _initialize_world_presentation() -> void:
|
||||
groups_to_snap.append(child)
|
||||
if has_node("WorldObjects"):
|
||||
var wo := $WorldObjects
|
||||
for group_name in ["Animals", "ResourceNodes", "StorageSites", "ActivitySites"]:
|
||||
for group_name in [
|
||||
"Animals", "AnimalHabitats", "ResourceNodes", "StorageSites", "ActivitySites"
|
||||
]:
|
||||
if wo.has_node(group_name):
|
||||
groups_to_snap.append_array(wo.get_node(group_name).get_children())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user