feat: add deterministic goat routine
This commit is contained in:
@@ -2,7 +2,7 @@ extends Node
|
||||
|
||||
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
||||
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
||||
const AnimalCareSystemScript := preload("res://simulation/animals/AnimalCareSystem.gd")
|
||||
const AnimalCareSystemScript := preload("res://simulation/animals/animal_care_system.gd")
|
||||
const RelationshipSystemScript := preload("res://simulation/relationships/RelationshipSystem.gd")
|
||||
const EventKnowledgeSystemScript := preload("res://simulation/knowledge/EventKnowledgeSystem.gd")
|
||||
|
||||
@@ -162,7 +162,7 @@ func simulate_tick() -> void:
|
||||
tick_count += 1
|
||||
if debug_logs:
|
||||
print("--- Tick ", tick_count, " ---")
|
||||
animal_care.advance()
|
||||
animal_care.advance(tick_count)
|
||||
var village_was_changed := false
|
||||
_population_view.rebuild(npcs)
|
||||
for npc in npcs:
|
||||
@@ -1192,7 +1192,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
for resource_record in record.resources:
|
||||
resource_states[resource_record.get_node_id()] = resource_record
|
||||
register_loaded_resource_nodes()
|
||||
animal_care.restore_state_records(record.animals)
|
||||
animal_care.restore_state_records(record.animals, tick_count)
|
||||
village_changed.emit(village)
|
||||
state_restored.emit()
|
||||
return true
|
||||
|
||||
@@ -19,10 +19,16 @@ signal narrative_event_requested(
|
||||
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:
|
||||
@@ -30,13 +36,19 @@ func configure(economy_service: RefCounted, world_adapter: Node) -> void:
|
||||
active_world_adapter = world_adapter
|
||||
|
||||
|
||||
func advance() -> void:
|
||||
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:
|
||||
animal_state.advance_tick()
|
||||
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:
|
||||
@@ -74,6 +86,12 @@ func register_node(node: AnimalNode) -> bool:
|
||||
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)
|
||||
|
||||
|
||||
@@ -190,6 +208,10 @@ func feed(animal_id: StringName, actor_id: int, npcs: Array[SimNPC], current_tic
|
||||
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,
|
||||
@@ -211,13 +233,70 @@ func append_state_records(record: SimulationStateRecord) -> void:
|
||||
record.animals.append(animal_state)
|
||||
|
||||
|
||||
func restore_state_records(records: Array[AnimalStateRecord]) -> void:
|
||||
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()
|
||||
)
|
||||
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:
|
||||
@@ -32,6 +32,8 @@ const RESOURCE_WOOD := &"wood"
|
||||
|
||||
const ANIMAL_DUNJA := &"goat_dunja"
|
||||
const SPECIES_GOAT := &"goat"
|
||||
const ANIMAL_SITE_DUNJA_PASTURE := &"dunja_pasture"
|
||||
const ANIMAL_SITE_DUNJA_SHELTER := &"dunja_shelter"
|
||||
|
||||
const STORAGE_VILLAGE_PANTRY := &"village_pantry"
|
||||
const STORAGE_VILLAGE_WOODPILE := &"village_woodpile"
|
||||
|
||||
@@ -2,7 +2,7 @@ class_name SimulationStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_NAME := "the_steward.simulation"
|
||||
const SCHEMA_VERSION := 10
|
||||
const SCHEMA_VERSION := 11
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||
@@ -11,6 +11,7 @@ const PROVENANCE_LEGACY_SCHEMA_VERSION := 5
|
||||
const RETENTION_LEGACY_SCHEMA_VERSION := 6
|
||||
const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8
|
||||
const ANIMAL_LEGACY_SCHEMA_VERSION := 9
|
||||
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
|
||||
const PREVIOUS_SCHEMA_VERSION := 7
|
||||
|
||||
var simulation: Dictionary
|
||||
@@ -95,6 +96,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
PREVIOUS_SCHEMA_VERSION,
|
||||
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
||||
ANIMAL_LEGACY_SCHEMA_VERSION,
|
||||
ROUTINE_LEGACY_SCHEMA_VERSION,
|
||||
]
|
||||
):
|
||||
record_data = _migrate_legacy(record_data, version)
|
||||
@@ -676,7 +678,8 @@ static func _is_valid_supply_resolution(
|
||||
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
|
||||
var migrated := legacy_data.duplicate(true)
|
||||
migrated["schema_version"] = SCHEMA_VERSION
|
||||
migrated["animals"] = []
|
||||
if version <= ANIMAL_LEGACY_SCHEMA_VERSION:
|
||||
migrated["animals"] = []
|
||||
if version == LEGACY_SCHEMA_VERSION:
|
||||
var village_data: Dictionary = legacy_data.get("village", {})
|
||||
var initial_food := float(village_data.get("food", 0.0))
|
||||
@@ -727,7 +730,14 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
||||
migrated.get("economic_events", []),
|
||||
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
|
||||
)
|
||||
if version not in [OPPORTUNITY_LEGACY_SCHEMA_VERSION, ANIMAL_LEGACY_SCHEMA_VERSION]:
|
||||
if (
|
||||
version
|
||||
not in [
|
||||
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
||||
ANIMAL_LEGACY_SCHEMA_VERSION,
|
||||
ROUTINE_LEGACY_SCHEMA_VERSION,
|
||||
]
|
||||
):
|
||||
migrated["opportunities"] = []
|
||||
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
|
||||
opportunity_simulation_data["next_opportunity_id"] = 0
|
||||
|
||||
@@ -4,7 +4,8 @@ extends RefCounted
|
||||
signal changed(state: AnimalStateRecord)
|
||||
signal fed(state: AnimalStateRecord, actor_id: int)
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const SCHEMA_VERSION := 2
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
const HUNGER_PER_TICK := 0.125
|
||||
const FEED_THRESHOLD := 65.0
|
||||
const HUNGER_RELIEF := 55.0
|
||||
@@ -34,13 +35,21 @@ static func create_from_node(node: AnimalNode) -> AnimalStateRecord:
|
||||
"enabled": node.initial_enabled,
|
||||
"can_npcs_feed": node.can_npcs_feed,
|
||||
"can_player_feed": node.can_player_feed,
|
||||
"routine_site_id": String(node.initial_routine_site_id),
|
||||
"travel_target_site_id": "",
|
||||
"travel_target_position": [0.0, 0.0, 0.0],
|
||||
"has_travel_target": false,
|
||||
"next_routine_tick": node.initial_next_routine_tick,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
var version := int(record_data.get("schema_version", -1))
|
||||
if version == LEGACY_SCHEMA_VERSION:
|
||||
record_data = _migrate_legacy(record_data)
|
||||
elif version != SCHEMA_VERSION:
|
||||
return null
|
||||
if not (
|
||||
record_data
|
||||
@@ -56,6 +65,11 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
||||
"enabled",
|
||||
"can_npcs_feed",
|
||||
"can_player_feed",
|
||||
"routine_site_id",
|
||||
"travel_target_site_id",
|
||||
"travel_target_position",
|
||||
"has_travel_target",
|
||||
"next_routine_tick",
|
||||
]
|
||||
)
|
||||
):
|
||||
@@ -67,6 +81,12 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
||||
var hunger := float(record_data["hunger"])
|
||||
var last_fed_tick := int(record_data["last_fed_tick"])
|
||||
var reserved_by := int(record_data["reserved_by"])
|
||||
var routine_site_id := String(record_data["routine_site_id"])
|
||||
var travel_target_site_id := String(record_data["travel_target_site_id"])
|
||||
var saved_travel_target = record_data["travel_target_position"]
|
||||
var has_travel_target := bool(record_data["has_travel_target"])
|
||||
var next_routine_tick := int(record_data["next_routine_tick"])
|
||||
var enabled := bool(record_data["enabled"])
|
||||
if animal_id.is_empty() or display_name.is_empty() or species_id.is_empty():
|
||||
return null
|
||||
if StringName(species_id) != SimulationIds.SPECIES_GOAT:
|
||||
@@ -76,11 +96,25 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
||||
for component in saved_position:
|
||||
if not is_finite(float(component)):
|
||||
return null
|
||||
if not saved_travel_target is Array or saved_travel_target.size() != 3:
|
||||
return null
|
||||
for component in saved_travel_target:
|
||||
if not is_finite(float(component)):
|
||||
return null
|
||||
if not is_finite(hunger) or hunger < 0.0 or hunger > 100.0:
|
||||
return null
|
||||
if last_fed_tick < NEVER_FED_TICK or reserved_by < -1:
|
||||
if last_fed_tick < NEVER_FED_TICK or reserved_by < -1 or next_routine_tick < 0:
|
||||
return null
|
||||
if reserved_by >= 0 and hunger < FEED_THRESHOLD:
|
||||
if reserved_by >= 0 and (hunger < FEED_THRESHOLD or has_travel_target):
|
||||
return null
|
||||
if has_travel_target != (not travel_target_site_id.is_empty()):
|
||||
return null
|
||||
if has_travel_target and (not enabled or travel_target_site_id == routine_site_id):
|
||||
return null
|
||||
var travel_target_position := Vector3(
|
||||
float(saved_travel_target[0]), float(saved_travel_target[1]), float(saved_travel_target[2])
|
||||
)
|
||||
if not has_travel_target and not travel_target_position.is_zero_approx():
|
||||
return null
|
||||
|
||||
var normalized := record_data.duplicate(true)
|
||||
@@ -94,9 +128,16 @@ static func from_dictionary(record_data: Dictionary) -> AnimalStateRecord:
|
||||
normalized["hunger"] = hunger
|
||||
normalized["last_fed_tick"] = last_fed_tick
|
||||
normalized["reserved_by"] = reserved_by
|
||||
normalized["enabled"] = bool(record_data["enabled"])
|
||||
normalized["enabled"] = enabled
|
||||
normalized["can_npcs_feed"] = bool(record_data["can_npcs_feed"])
|
||||
normalized["can_player_feed"] = bool(record_data["can_player_feed"])
|
||||
normalized["routine_site_id"] = routine_site_id
|
||||
normalized["travel_target_site_id"] = travel_target_site_id
|
||||
normalized["travel_target_position"] = [
|
||||
travel_target_position.x, travel_target_position.y, travel_target_position.z
|
||||
]
|
||||
normalized["has_travel_target"] = has_travel_target
|
||||
normalized["next_routine_tick"] = next_routine_tick
|
||||
return AnimalStateRecord.new(normalized)
|
||||
|
||||
|
||||
@@ -152,6 +193,27 @@ func can_player_feed_animal() -> bool:
|
||||
return bool(data["can_player_feed"])
|
||||
|
||||
|
||||
func get_routine_site_id() -> StringName:
|
||||
return StringName(data["routine_site_id"])
|
||||
|
||||
|
||||
func get_travel_target_site_id() -> StringName:
|
||||
return StringName(data["travel_target_site_id"])
|
||||
|
||||
|
||||
func get_travel_target_position() -> Vector3:
|
||||
var saved_position: Array = data["travel_target_position"]
|
||||
return Vector3(float(saved_position[0]), float(saved_position[1]), float(saved_position[2]))
|
||||
|
||||
|
||||
func has_active_travel_target() -> bool:
|
||||
return bool(data["has_travel_target"])
|
||||
|
||||
|
||||
func get_next_routine_tick() -> int:
|
||||
return int(data["next_routine_tick"])
|
||||
|
||||
|
||||
func needs_feed() -> bool:
|
||||
return is_enabled() and get_hunger() >= FEED_THRESHOLD
|
||||
|
||||
@@ -160,6 +222,16 @@ func is_available_for(agent_id: int) -> bool:
|
||||
return needs_feed() and (get_reserved_by() == -1 or get_reserved_by() == agent_id)
|
||||
|
||||
|
||||
func is_routine_due(current_tick: int) -> bool:
|
||||
return (
|
||||
is_enabled()
|
||||
and not needs_feed()
|
||||
and get_reserved_by() == -1
|
||||
and not has_active_travel_target()
|
||||
and current_tick >= get_next_routine_tick()
|
||||
)
|
||||
|
||||
|
||||
func reserve(agent_id: int) -> bool:
|
||||
if agent_id < 0 or not is_available_for(agent_id):
|
||||
return false
|
||||
@@ -196,6 +268,56 @@ func feed(actor_id: int, current_tick: int) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func begin_travel(site_id: StringName, target_position: Vector3) -> bool:
|
||||
if (
|
||||
site_id.is_empty()
|
||||
or site_id == get_routine_site_id()
|
||||
or not target_position.is_finite()
|
||||
or not is_enabled()
|
||||
or has_active_travel_target()
|
||||
or needs_feed()
|
||||
or get_reserved_by() >= 0
|
||||
):
|
||||
return false
|
||||
data["travel_target_site_id"] = String(site_id)
|
||||
data["travel_target_position"] = [target_position.x, target_position.y, target_position.z]
|
||||
data["has_travel_target"] = true
|
||||
changed.emit(self)
|
||||
return true
|
||||
|
||||
|
||||
func complete_travel(arrival_position: Vector3, next_routine_tick: int) -> bool:
|
||||
if not has_active_travel_target() or not arrival_position.is_finite() or next_routine_tick < 0:
|
||||
return false
|
||||
data["position"] = [arrival_position.x, arrival_position.y, arrival_position.z]
|
||||
data["routine_site_id"] = data["travel_target_site_id"]
|
||||
data["travel_target_site_id"] = ""
|
||||
data["travel_target_position"] = [0.0, 0.0, 0.0]
|
||||
data["has_travel_target"] = false
|
||||
data["next_routine_tick"] = next_routine_tick
|
||||
changed.emit(self)
|
||||
return true
|
||||
|
||||
|
||||
func cancel_travel(next_routine_tick: int) -> bool:
|
||||
if next_routine_tick < 0:
|
||||
return false
|
||||
data["travel_target_site_id"] = ""
|
||||
data["travel_target_position"] = [0.0, 0.0, 0.0]
|
||||
data["has_travel_target"] = false
|
||||
data["next_routine_tick"] = next_routine_tick
|
||||
changed.emit(self)
|
||||
return true
|
||||
|
||||
|
||||
func schedule_routine_at(next_routine_tick: int) -> bool:
|
||||
if next_routine_tick < 0 or has_active_travel_target():
|
||||
return false
|
||||
data["next_routine_tick"] = next_routine_tick
|
||||
changed.emit(self)
|
||||
return true
|
||||
|
||||
|
||||
func set_position(value: Vector3) -> void:
|
||||
if not value.is_finite() or value.is_equal_approx(get_position()):
|
||||
return
|
||||
@@ -216,8 +338,22 @@ func set_enabled(value: bool) -> void:
|
||||
data["enabled"] = value
|
||||
if not value:
|
||||
data["reserved_by"] = -1
|
||||
data["travel_target_site_id"] = ""
|
||||
data["travel_target_position"] = [0.0, 0.0, 0.0]
|
||||
data["has_travel_target"] = false
|
||||
changed.emit(self)
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return data.duplicate(true)
|
||||
|
||||
|
||||
static func _migrate_legacy(record_data: Dictionary) -> Dictionary:
|
||||
var migrated := record_data.duplicate(true)
|
||||
migrated["schema_version"] = SCHEMA_VERSION
|
||||
migrated["routine_site_id"] = ""
|
||||
migrated["travel_target_site_id"] = ""
|
||||
migrated["travel_target_position"] = [0.0, 0.0, 0.0]
|
||||
migrated["has_travel_target"] = false
|
||||
migrated["next_routine_tick"] = 0
|
||||
return migrated
|
||||
Reference in New Issue
Block a user