feat: add deterministic goat routine

This commit is contained in:
Rijad Zuzo
2026-07-26 23:52:56 +02:00
parent f79c2bb630
commit e3aa3440a2
61 changed files with 1393 additions and 1057 deletions
+13 -3
View File
@@ -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