360 lines
10 KiB
GDScript
360 lines
10 KiB
GDScript
class_name AnimalStateRecord
|
|
extends RefCounted
|
|
|
|
signal changed(state: AnimalStateRecord)
|
|
signal fed(state: AnimalStateRecord, actor_id: int)
|
|
|
|
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
|
|
const NEVER_FED_TICK := -1
|
|
|
|
var data: Dictionary
|
|
|
|
|
|
func _init(record_data: Dictionary = {}) -> void:
|
|
data = record_data.duplicate(true)
|
|
|
|
|
|
static func create_from_node(node: AnimalNode) -> AnimalStateRecord:
|
|
return (
|
|
AnimalStateRecord
|
|
. new(
|
|
{
|
|
"schema_version": SCHEMA_VERSION,
|
|
"animal_id": String(node.animal_id),
|
|
"display_name": node.display_name,
|
|
"species_id": String(node.species_id),
|
|
"position":
|
|
[node.global_position.x, node.global_position.y, node.global_position.z],
|
|
"hunger": node.initial_hunger,
|
|
"last_fed_tick": NEVER_FED_TICK,
|
|
"reserved_by": -1,
|
|
"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:
|
|
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
|
|
. has_all(
|
|
[
|
|
"animal_id",
|
|
"display_name",
|
|
"species_id",
|
|
"position",
|
|
"hunger",
|
|
"last_fed_tick",
|
|
"reserved_by",
|
|
"enabled",
|
|
"can_npcs_feed",
|
|
"can_player_feed",
|
|
"routine_site_id",
|
|
"travel_target_site_id",
|
|
"travel_target_position",
|
|
"has_travel_target",
|
|
"next_routine_tick",
|
|
]
|
|
)
|
|
):
|
|
return null
|
|
var animal_id := String(record_data["animal_id"])
|
|
var display_name := String(record_data["display_name"])
|
|
var species_id := String(record_data["species_id"])
|
|
var saved_position = record_data["position"]
|
|
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:
|
|
return null
|
|
if not saved_position is Array or saved_position.size() != 3:
|
|
return null
|
|
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 or next_routine_tick < 0:
|
|
return null
|
|
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)
|
|
normalized["schema_version"] = SCHEMA_VERSION
|
|
normalized["animal_id"] = animal_id
|
|
normalized["display_name"] = display_name
|
|
normalized["species_id"] = species_id
|
|
normalized["position"] = [
|
|
float(saved_position[0]), float(saved_position[1]), float(saved_position[2])
|
|
]
|
|
normalized["hunger"] = hunger
|
|
normalized["last_fed_tick"] = last_fed_tick
|
|
normalized["reserved_by"] = reserved_by
|
|
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)
|
|
|
|
|
|
func apply_definition(node: AnimalNode) -> bool:
|
|
if node == null or node.animal_id != get_animal_id():
|
|
return false
|
|
return (
|
|
node.display_name == get_display_name()
|
|
and node.species_id == get_species_id()
|
|
and node.can_npcs_feed == can_npc_feed()
|
|
and node.can_player_feed == can_player_feed_animal()
|
|
)
|
|
|
|
|
|
func get_animal_id() -> StringName:
|
|
return StringName(data["animal_id"])
|
|
|
|
|
|
func get_display_name() -> String:
|
|
return String(data["display_name"])
|
|
|
|
|
|
func get_species_id() -> StringName:
|
|
return StringName(data["species_id"])
|
|
|
|
|
|
func get_position() -> Vector3:
|
|
var saved_position: Array = data["position"]
|
|
return Vector3(float(saved_position[0]), float(saved_position[1]), float(saved_position[2]))
|
|
|
|
|
|
func get_hunger() -> float:
|
|
return float(data["hunger"])
|
|
|
|
|
|
func get_last_fed_tick() -> int:
|
|
return int(data["last_fed_tick"])
|
|
|
|
|
|
func get_reserved_by() -> int:
|
|
return int(data["reserved_by"])
|
|
|
|
|
|
func is_enabled() -> bool:
|
|
return bool(data["enabled"])
|
|
|
|
|
|
func can_npc_feed() -> bool:
|
|
return bool(data["can_npcs_feed"])
|
|
|
|
|
|
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
|
|
|
|
|
|
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
|
|
data["reserved_by"] = agent_id
|
|
changed.emit(self)
|
|
return true
|
|
|
|
|
|
func release(agent_id: int) -> void:
|
|
if get_reserved_by() != agent_id:
|
|
return
|
|
data["reserved_by"] = -1
|
|
changed.emit(self)
|
|
|
|
|
|
func advance_tick() -> void:
|
|
if not is_enabled():
|
|
return
|
|
var next_hunger := minf(get_hunger() + HUNGER_PER_TICK, 100.0)
|
|
if is_equal_approx(next_hunger, get_hunger()):
|
|
return
|
|
data["hunger"] = next_hunger
|
|
changed.emit(self)
|
|
|
|
|
|
func feed(actor_id: int, current_tick: int) -> bool:
|
|
if not needs_feed() or current_tick < 0:
|
|
return false
|
|
data["hunger"] = maxf(get_hunger() - HUNGER_RELIEF, 0.0)
|
|
data["last_fed_tick"] = current_tick
|
|
data["reserved_by"] = -1
|
|
changed.emit(self)
|
|
fed.emit(self, actor_id)
|
|
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
|
|
data["position"] = [value.x, value.y, value.z]
|
|
changed.emit(self)
|
|
|
|
|
|
func set_hunger(value: float) -> void:
|
|
if not is_finite(value):
|
|
return
|
|
data["hunger"] = clampf(value, 0.0, 100.0)
|
|
if not needs_feed():
|
|
data["reserved_by"] = -1
|
|
changed.emit(self)
|
|
|
|
|
|
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
|