feat: introduce identity-backed goat
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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 @@
|
||||
uid://6s3isttop6a6
|
||||
Reference in New Issue
Block a user