extends Node3D const HUNGER_CUE_BASE_Y := 2.05 const FED_RESPONSE_BASE_Y := 1.8 @onready var head_root: Node3D = $HeadRoot @onready var tail_root: Node3D = $TailRoot @onready var hunger_cue_root: Node3D = $HungerCueRoot @onready var fed_response_root: Node3D = $FedResponseRoot @onready var leg_front_left: Node3D = $LegFrontLeft @onready var leg_front_right: Node3D = $LegFrontRight @onready var leg_back_left: Node3D = $LegBackLeft @onready var leg_back_right: Node3D = $LegBackRight @onready var body: Node3D = $Body @onready var fluff_front: Node3D = $FluffFront @onready var fluff_back: Node3D = $FluffBack @onready var horn_left: Node3D = $HeadRoot/HornLeft @onready var horn_right: Node3D = $HeadRoot/HornRight @onready var beard: Node3D = $HeadRoot/Beard var idle_phase := 0.0 var is_hungry := false var is_moving := false var fed_tween: Tween var head_base_position: Vector3 var visual_base_position: Vector3 var body_base_scale: Vector3 var fluff_front_base_scale: Vector3 var fluff_back_base_scale: Vector3 var _animal_definition: AnimalDefinition var _presentation_parameters: Dictionary = {} func _ready() -> void: head_base_position = head_root.position visual_base_position = position body_base_scale = body.scale fluff_front_base_scale = fluff_front.scale fluff_back_base_scale = fluff_back.scale _apply_definition_presentation() reset_transient_feedback() func apply_animal_definition( definition: AnimalDefinition, presentation_parameters: Dictionary = {} ) -> bool: if definition == null or not definition.validate().is_empty(): return false _animal_definition = definition _presentation_parameters = presentation_parameters.duplicate(true) if is_node_ready(): _apply_definition_presentation() return true func _apply_definition_presentation() -> void: if _animal_definition == null: return scale = _parameter_vector("visual_scale", Vector3.ONE) body.scale = body_base_scale * _parameter_vector("body_scale", Vector3.ONE) fluff_front.scale = fluff_front_base_scale * _parameter_vector("fluff_scale", Vector3.ONE) fluff_back.scale = fluff_back_base_scale * _parameter_vector("fluff_scale", Vector3.ONE) horn_left.visible = bool(_presentation_parameters.get("show_horns", true)) horn_right.visible = bool(_presentation_parameters.get("show_horns", true)) beard.visible = bool(_presentation_parameters.get("show_beard", true)) func _parameter_vector(key: String, fallback: Vector3) -> Vector3: var value: Variant = _presentation_parameters.get(key, fallback) return value as Vector3 if value is Vector3 else fallback func _process(delta: float) -> void: idle_phase = fmod(idle_phase + delta, TAU) var breath := sin(idle_phase * 1.7) * 0.025 var hungry_drop := -0.12 if is_hungry else 0.0 var walking_bob := absf(sin(idle_phase * 5.4)) * 0.045 if is_moving else 0.0 position.y = visual_base_position.y + walking_bob head_root.position.y = head_base_position.y + breath + hungry_drop head_root.rotation_degrees.x = ( 8.0 + sin(idle_phase * 1.2) * 2.0 if is_hungry else -3.0 + sin(idle_phase * 1.4) * 2.5 ) tail_root.rotation_degrees.z = sin(idle_phase * 3.1) * (4.0 if is_hungry else 13.0) var stride := sin(idle_phase * 5.4) * 14.0 if is_moving else 0.0 leg_front_left.rotation_degrees.x = stride leg_back_right.rotation_degrees.x = stride leg_front_right.rotation_degrees.x = -stride leg_back_left.rotation_degrees.x = -stride if hunger_cue_root.visible: hunger_cue_root.position.y = HUNGER_CUE_BASE_Y + sin(idle_phase * 2.2) * 0.07 func set_hungry(value: bool) -> void: is_hungry = value hunger_cue_root.visible = value func set_moving(value: bool) -> void: is_moving = value if not value: position = visual_base_position leg_front_left.rotation_degrees.x = 0.0 leg_front_right.rotation_degrees.x = 0.0 leg_back_left.rotation_degrees.x = 0.0 leg_back_right.rotation_degrees.x = 0.0 func reset_transient_feedback() -> void: if fed_tween != null: fed_tween.kill() fed_tween = null fed_response_root.visible = false fed_response_root.position.y = FED_RESPONSE_BASE_Y fed_response_root.scale = Vector3(0.2, 0.2, 0.2) func play_fed_response() -> void: reset_transient_feedback() fed_response_root.visible = true fed_tween = create_tween() fed_tween.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) fed_tween.tween_property(fed_response_root, "scale", Vector3.ONE, 0.34) fed_tween.parallel().tween_property( fed_response_root, "position:y", FED_RESPONSE_BASE_Y + 0.22, 0.34 ) fed_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN) fed_tween.tween_interval(0.7) fed_tween.tween_property(fed_response_root, "scale", Vector3(0.2, 0.2, 0.2), 0.42) fed_tween.finished.connect(_on_fed_response_finished) func _on_fed_response_finished() -> void: fed_response_root.visible = false fed_tween = null