class_name PantryStockVisual extends Node3D enum StockLevel { EMPTY, LOW, STOCKED } const LOW_STOCK_THRESHOLD := 4.0 const REFILL_RESPONSE_START := Vector3(0.0, 0.08, -0.5) const REFILL_RESPONSE_END := Vector3(0.0, 0.36, -0.5) @onready var empty_root: Node3D = $EmptyStock @onready var low_stock_root: Node3D = $LowStock @onready var full_stock_root: Node3D = $FullStock @onready var refill_response_root: Node3D = $RefillResponse @onready var refill_response_visuals: Array[MeshInstance3D] = [ $RefillResponse/Ring, $RefillResponse/MoteLeft, $RefillResponse/MoteCenter, $RefillResponse/MoteRight, ] var displayed_amount := -1.0 var stock_level := StockLevel.EMPTY var transition_tween: Tween var refill_tween: Tween func _ready() -> void: refill_response_root.visible = false func set_food_amount(amount: float, animate: bool = true) -> void: var normalized_amount := maxf(amount, 0.0) var next_level := _get_stock_level(normalized_amount) if next_level == StockLevel.EMPTY: _stop_refill_response() var was_initialized := displayed_amount >= 0.0 var was_empty := was_initialized and displayed_amount <= 0.001 if was_initialized and next_level == stock_level: displayed_amount = normalized_amount return var should_animate := animate and was_initialized displayed_amount = normalized_amount stock_level = next_level if should_animate: _transition_to_level(next_level) else: _apply_level_immediately(next_level) if was_empty and normalized_amount > 0.001 and animate: _play_refill_response() func get_stock_level() -> StockLevel: return stock_level func _get_stock_level(amount: float) -> StockLevel: if amount <= 0.001: return StockLevel.EMPTY if amount < LOW_STOCK_THRESHOLD: return StockLevel.LOW return StockLevel.STOCKED func _apply_level_immediately(level: StockLevel) -> void: _stop_transition() _set_root_visible(empty_root, level == StockLevel.EMPTY) _set_root_visible(low_stock_root, level in [StockLevel.LOW, StockLevel.STOCKED]) _set_root_visible(full_stock_root, level == StockLevel.STOCKED) func _transition_to_level(level: StockLevel) -> void: _stop_transition() transition_tween = create_tween().set_parallel(true) _transition_root(empty_root, level == StockLevel.EMPTY) _transition_root(low_stock_root, level in [StockLevel.LOW, StockLevel.STOCKED]) _transition_root(full_stock_root, level == StockLevel.STOCKED) transition_tween.chain().tween_callback(_finish_transition.bind(level)) func _transition_root(root: Node3D, should_show: bool) -> void: if should_show: if not root.visible: root.visible = true root.scale = Vector3.ONE * 0.62 ( transition_tween . tween_property(root, "scale", Vector3.ONE, 0.28) . set_trans(Tween.TRANS_BACK) . set_ease(Tween.EASE_OUT) ) return if not root.visible: return ( transition_tween . tween_property(root, "scale", Vector3.ONE * 0.18, 0.16) . set_trans(Tween.TRANS_SINE) . set_ease(Tween.EASE_IN) ) func _finish_transition(level: StockLevel) -> void: transition_tween = null _set_root_visible(empty_root, level == StockLevel.EMPTY) _set_root_visible(low_stock_root, level in [StockLevel.LOW, StockLevel.STOCKED]) _set_root_visible(full_stock_root, level == StockLevel.STOCKED) func _set_root_visible(root: Node3D, is_visible: bool) -> void: root.visible = is_visible root.scale = Vector3.ONE func _stop_transition() -> void: if transition_tween != null: transition_tween.kill() transition_tween = null func _play_refill_response() -> void: _stop_refill_response() refill_response_root.visible = true refill_response_root.position = REFILL_RESPONSE_START refill_response_root.scale = Vector3.ONE * 0.45 _set_refill_transparency(0.0) refill_tween = create_tween() refill_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT) refill_tween.tween_property(refill_response_root, "scale", Vector3.ONE, 0.24) refill_tween.parallel().tween_method(_set_refill_transparency, 0.0, 1.0, 0.18) refill_tween.tween_property(refill_response_root, "position", REFILL_RESPONSE_END, 0.5) refill_tween.parallel().tween_method(_set_refill_transparency, 1.0, 0.0, 0.5) refill_tween.tween_callback(_stop_refill_response) func _set_refill_transparency(value: float) -> void: for visual in refill_response_visuals: visual.transparency = clampf(1.0 - value, 0.0, 1.0) func _stop_refill_response() -> void: if refill_tween != null: refill_tween.kill() refill_tween = null if refill_response_root == null: return refill_response_root.visible = false refill_response_root.position = REFILL_RESPONSE_START refill_response_root.scale = Vector3.ONE * 0.45 _set_refill_transparency(0.0)