Files
gamedev-the-steward/simulation/animals/animal_care_system.gd
T
2026-07-30 23:03:15 +02:00

305 lines
9.4 KiB
GDScript

class_name AnimalCareSystem
extends RefCounted
signal economic_event_requested(
event_type: StringName,
actor_id: int,
source_id: StringName,
destination_id: StringName,
item_id: StringName,
amount: float,
world_position: Vector3
)
signal narrative_event_requested(
event_type: StringName,
actor_id: int,
source_id: StringName,
action_display: String,
action_id: StringName,
item_id: StringName,
required_amount: float
)
signal routine_started(animal_id: StringName, site_id: StringName, target_position: Vector3)
signal routine_completed(animal_id: StringName, site_id: StringName, position: Vector3)
const ROUTINE_STAY_TICKS := 40
const ROUTINE_RETRY_TICKS := 10
var states: Dictionary = {}
var economy: RefCounted
var active_world_adapter: Node
var current_tick := 0
func configure(economy_service: RefCounted, world_adapter: Node) -> void:
economy = economy_service
active_world_adapter = world_adapter
func advance(simulation_tick: int) -> void:
current_tick = simulation_tick
var animal_ids: Array = states.keys()
animal_ids.sort()
for animal_id in animal_ids:
var animal_state := states[animal_id] as AnimalStateRecord
if animal_state == null:
continue
animal_state.advance_tick()
if animal_state.has_active_travel_target() and animal_state.needs_feed():
animal_state.cancel_travel(current_tick + ROUTINE_RETRY_TICKS)
elif animal_state.is_routine_due(current_tick):
_begin_due_routine(animal_state)
func register_loaded_nodes() -> void:
for node in AnimalNode.get_all():
register_node(node)
func register_node(node: AnimalNode) -> bool:
if node == null or node.animal_id.is_empty():
return false
if (
ResourceNode.get_by_id(node.animal_id) != null
or StorageNode.get_by_id(node.animal_id) != null
or ActivitySite.get_by_id(node.animal_id) != null
):
push_error("AnimalCareSystem: Animal ID '%s' collides with a world target" % node.animal_id)
return false
var action_definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
if (
action_definition == null
or action_definition.target_type != SimulationIds.TARGET_ANIMAL
or node.species_id != SimulationIds.SPECIES_GOAT
):
push_error(
(
"AnimalCareSystem: AnimalNode '%s' has an invalid feed action or species '%s'"
% [node.animal_id, node.species_id]
)
)
return false
var animal_state := get_state(node.animal_id)
if animal_state == null:
animal_state = AnimalStateRecord.create_from_node(node)
states[node.animal_id] = animal_state
elif not animal_state.apply_definition(node):
push_error("AnimalCareSystem: AnimalNode definition mismatch for '%s'" % node.animal_id)
return false
if not node.position_changed.is_connected(_on_node_position_changed):
node.position_changed.connect(_on_node_position_changed)
if not node.routine_arrived.is_connected(_on_node_routine_arrived):
node.routine_arrived.connect(_on_node_routine_arrived)
if not node.navigation_failed.is_connected(_on_node_navigation_failed):
node.navigation_failed.connect(_on_node_navigation_failed)
return node.bind_state(animal_state)
func get_state(animal_id: StringName) -> AnimalStateRecord:
return states.get(animal_id) as AnimalStateRecord
func reserve(animal_id: StringName, agent_id: int) -> bool:
var animal_state := get_state(animal_id)
return animal_state != null and animal_state.can_npc_feed() and animal_state.reserve(agent_id)
func release(animal_id: StringName, agent_id: int) -> void:
var animal_state := get_state(animal_id)
if animal_state != null:
animal_state.release(agent_id)
func can_complete_feed(animal_id: StringName, agent_id: int) -> bool:
var animal_state := get_state(animal_id)
return (
animal_state != null
and animal_state.needs_feed()
and animal_state.get_reserved_by() == agent_id
)
func has_available_feed_target(npc_id: int) -> bool:
if active_world_adapter == null or not active_world_adapter.has_method("get_animal_candidates"):
return false
var candidates: Array[Dictionary] = active_world_adapter.get_animal_candidates(
SimulationIds.ACTION_FEED_ANIMAL
)
for candidate in candidates:
var animal_state := get_state(StringName(candidate["target_id"]))
if (
animal_state != null
and animal_state.can_npc_feed()
and animal_state.is_available_for(npc_id)
):
return true
return false
func find_node_for_player(from_position: Vector3, max_distance: float) -> AnimalNode:
if (
active_world_adapter == null
or not active_world_adapter.has_method("get_animal_nodes_in_radius")
):
return null
var candidates: Array[AnimalNode] = active_world_adapter.get_animal_nodes_in_radius(
from_position, max_distance
)
for node in candidates:
var animal_state := get_state(node.animal_id)
if (
animal_state != null
and animal_state.can_player_feed_animal()
and animal_state.needs_feed()
):
return node
return null
func feed(animal_id: StringName, actor_id: int, npcs: Array[SimNPC], current_tick: int) -> bool:
var animal_state := get_state(animal_id)
if animal_state == null or not animal_state.needs_feed():
return false
if actor_id >= 0:
if (
_find_npc(actor_id, npcs) == null
or not animal_state.can_npc_feed()
or animal_state.get_reserved_by() != actor_id
):
return false
elif not animal_state.can_player_feed_animal():
return false
var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
if definition == null or not definition.has_completion_cost() or economy == null:
return false
var resource_id := definition.completion_cost_resource_id
var required_amount := definition.completion_cost_amount
var pantry: StorageStateRecord = economy.get_storage_for_resource(resource_id)
var available := pantry.get_amount(resource_id) if pantry != null else 0.0
if pantry == null or available < required_amount:
if actor_id >= 0:
(
narrative_event_requested
. emit(
SimulationIds.EVENT_TASK_BLOCKED,
actor_id,
pantry.get_storage_id() if pantry != null else &"",
(
"%s: needs %.0f %s (%.1f available)"
% [
definition.display_name,
required_amount,
String(resource_id).capitalize(),
available,
]
),
definition.action_id,
resource_id,
required_amount
)
)
return false
var consumed: float = economy.withdraw_resource(resource_id, required_amount)
if consumed < required_amount:
economy.deposit_resource(resource_id, consumed)
return false
if not animal_state.feed(actor_id, current_tick):
economy.deposit_resource(resource_id, consumed)
return false
if animal_state.has_active_travel_target():
animal_state.cancel_travel(current_tick + ROUTINE_STAY_TICKS)
else:
animal_state.schedule_routine_at(current_tick + ROUTINE_STAY_TICKS)
economic_event_requested.emit(
SimulationIds.EVENT_ANIMAL_FED,
actor_id,
pantry.get_storage_id(),
animal_state.get_animal_id(),
resource_id,
consumed,
animal_state.get_position()
)
return true
func append_state_records(record: SimulationStateRecord) -> void:
var animal_ids: Array = states.keys()
animal_ids.sort()
for animal_id in animal_ids:
var animal_state := states[animal_id] as AnimalStateRecord
record.animals.append(animal_state)
func restore_state_records(records: Array[AnimalStateRecord], restored_tick: int) -> void:
current_tick = restored_tick
states.clear()
for animal_record in records:
states[animal_record.get_animal_id()] = animal_record
register_loaded_nodes()
func _begin_due_routine(animal_state: AnimalStateRecord) -> bool:
if (
active_world_adapter == null
or not active_world_adapter.has_method("get_animal_routine_sites")
):
return false
if AnimalNode.get_by_id(animal_state.get_animal_id()) == null:
return false
var candidates: Array[Dictionary] = active_world_adapter.get_animal_routine_sites(
animal_state.get_species_id(), animal_state.get_animal_id()
)
if candidates.is_empty():
return false
var current_site_id := animal_state.get_routine_site_id()
for candidate in candidates:
var candidate_id := StringName(candidate["site_id"])
if candidate_id == current_site_id:
continue
var target_position: Vector3 = candidate["position"]
if not animal_state.begin_travel(candidate_id, target_position):
return false
routine_started.emit(animal_state.get_animal_id(), candidate_id, target_position)
return true
return false
func _on_node_position_changed(animal_id: StringName, active_position: Vector3) -> void:
var animal_state := get_state(animal_id)
if animal_state != null and animal_state.has_active_travel_target():
animal_state.set_position(active_position)
func _on_node_routine_arrived(
animal_id: StringName, site_id: StringName, arrival_position: Vector3
) -> void:
var animal_state := get_state(animal_id)
if (
animal_state == null
or not animal_state.has_active_travel_target()
or animal_state.get_travel_target_site_id() != site_id
):
return
if animal_state.complete_travel(arrival_position, current_tick + ROUTINE_STAY_TICKS):
routine_completed.emit(animal_id, site_id, arrival_position)
func _on_node_navigation_failed(animal_id: StringName, site_id: StringName) -> void:
var animal_state := get_state(animal_id)
if (
animal_state != null
and animal_state.has_active_travel_target()
and animal_state.get_travel_target_site_id() == site_id
):
animal_state.cancel_travel(current_tick + ROUTINE_RETRY_TICKS)
static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC:
for npc in npcs:
if npc.id == npc_id:
return npc
return null