extends StaticBody3D @export var chop_work_steps: float = 5.0 @export var chop_min_speed: float = 2.0 @export var audio: AudioStreamPlayer3D @export var chop_audio: AudioStream @export var knife: XRToolsPickable @onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone @export var progress_bar: ProgressBar3D # Chopping state. Follow the hob/sink pattern so the display refreshes when # values change; only the world owner runs the station's process in practice. @export var process_result: String = "": set = _set_result @export var process_result_work: float = 0.0: set = _set_result_work @export var work_progress: float = 0.0: set = _set_work_progress func _ready() -> void: if not audio: push_error("Counter is missing reference to AudioStreamPlayer3D") if not chop_audio: push_error("Counter is missing reference to chop_audio AudioStream") if not progress_bar or progress_bar is not ProgressBar3D: push_error("Counter missing reference to progressbar, or wrong type:", progress_bar) if not knife or knife is not XRToolsPickable: push_error("Counter missing reference to knife, or wrong type:", knife) if not snap_zone or snap_zone is not XRToolsSnapZone: push_error("Counter missing reference to snap_zone, or wrong type:", snap_zone) snap_zone.has_picked_up.connect(_on_object_picked_up) snap_zone.has_dropped.connect(_on_object_dropped) progress_bar.override_fill_color(Color.GREEN) _hide_all_tools() _refresh_progress_bar() func _set_work_progress(value: float) -> void: work_progress = value _refresh_progress_bar() func _set_result(value: String) -> void: process_result = value _refresh_progress_bar() # When a chopping recipe is set, show the knife for the player to use. if not process_result.is_empty(): knife.visible = true knife.enabled = true SweetLogger.debug("chopping recipe set: {0}", [process_result]) else: _hide_all_tools() func _set_result_work(value: float) -> void: process_result_work = value _refresh_progress_bar() func _refresh_progress_bar() -> void: if not progress_bar: return var progress := 0.0 if process_result != "" and process_result_work > 0: progress = clampf(float(work_progress) / process_result_work, 0.0, 1.0) progress_bar.set_progress(progress) progress_bar.set_bar_visible(not process_result.is_empty()) progress_bar.override_fill_color(Color.GREEN) # Add work from gesture area (knife hits). This increments the chopping progress. func add_work(work: float) -> void: SweetLogger.debug("add_work: {0}", [work]) # Only the world owner should drive the authoritative state. Guarding is # handled by higher-level NetworkManager logic elsewhere, mirror hob's # convert_held_to_item which checks ownership before spawning. work_progress += work # Play chopping sound if available if audio and chop_audio: audio.stream = chop_audio audio.play() # If we've reached the required time, convert the held item if process_result != "" and process_result_work > 0 and work_progress >= process_result_work: SweetLogger.debug("chopping complete, converting item to: {0}", [process_result]) work_progress = 0 convert_held_to_item(process_result) func _on_gesture_area_body_entered(body: Node3D) -> void: SweetLogger.debug("gesture area entered: {0}", [body]) if body.is_in_group("chopping_tool"): if process_result == "": SweetLogger.debug("knife entered but nothing to chop") return var speed := 0.0 if body is RigidBody3D: speed = body.linear_velocity.length() elif body is CharacterBody3D: speed = body.velocity.length() if speed < chop_min_speed: SweetLogger.debug("knife too slow for chopping: {0}", [speed]) return add_work(chop_work_steps) func _on_object_picked_up(_item: Variant) -> void: SweetLogger.debug("object picked up: {0}", [_item]) var food_item = Helper.find_food_item(_item) if not food_item: SweetLogger.debug("held object is not a FoodItem") return var result = RecipeManager.get_chopping_result(food_item.id) if not result: SweetLogger.debug("held a FoodItem that is not choppable, id: {0} process_result: {1}", [food_item.id, result]) return process_result = result process_result_work = RecipeManager.get_chopping_work(food_item.id) SweetLogger.debug("set process_result {0} work: {1}", [process_result, process_result_work]) func _on_object_dropped(_item: Variant) -> void: SweetLogger.debug("object drop") work_progress = 0 process_result = "" process_result_work = 0 _hide_all_tools() func convert_held_to_item(_item: String) -> void: if not NetworkManager.owns_world(): return SweetLogger.debug("converting {0}", [_item]) var old_pickable = snap_zone.picked_up_object if not old_pickable: push_warning("Counter finished processing _item, but snap zone is missing its reference") return var original_transform = old_pickable.global_transform var new_scene_instance = NetworkManager.spawn_item(RecipeManager.get_item_scene(_item).resource_path, original_transform) SweetLogger.debug("freeing old_pickable {0}", [old_pickable]) snap_zone.drop_object() NetworkManager.despawn_item(old_pickable) snap_zone.pick_up_object(new_scene_instance) func _hide_all_tools(): knife.visible = false knife.enabled = false