extends Node3D @export var spread_radius := 2.0 @export var spread_interval_min := 5.0 @export var spread_interval_max := 15.0 @export var max_mushrooms_in_radius := 3 @export var check_radius := 1.5 @export var max_total_mushrooms := 50 @export var interact_radius := 2.0 @export var look_at_threshold := 0.95 static var total_mushrooms := 0 @onready var spread_timer: Timer = $SpreadTimer @onready var ground_ray: RayCast3D = $GroundRay @onready var cap: MeshInstance3D = $Cap @onready var stem: MeshInstance3D = $Stem var _player_in_range := false var _is_highlighted := false var highlight_mat: StandardMaterial3D var _audio_player: AudioStreamPlayer3D func _ready() -> void: total_mushrooms += 1 _setup_timer() # Randomize initial scale for variety scale = Vector3.ONE * randf_range(0.8, 1.2) rotation.y = randf_range(0, TAU) # Setup interaction area var area = Area3D.new() add_child(area) var shape = CollisionShape3D.new() var sphere = SphereShape3D.new() sphere.radius = interact_radius shape.shape = sphere area.add_child(shape) area.body_entered.connect(_on_body_entered) area.body_exited.connect(_on_body_exited) highlight_mat = StandardMaterial3D.new() highlight_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA highlight_mat.albedo_color = Color(1.0, 1.0, 1.0, 0.3) highlight_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED _audio_player = AudioStreamPlayer3D.new() _audio_player.stream = preload("res://assets/audio/pop.ogg") add_child(_audio_player) func _process(_delta: float) -> void: if not _player_in_range: _set_highlight(false) return var cam = get_viewport().get_camera_3d() if not cam: _set_highlight(false) return var dir_to_mushroom = (global_position - cam.global_position).normalized() var forward = -cam.global_transform.basis.z var my_dot = forward.dot(dir_to_mushroom) if my_dot > look_at_threshold: # Check if we are the most directly looked-at mushroom in range var is_best = true for mushroom in get_tree().get_nodes_in_group("mushrooms"): if mushroom == self or not mushroom.get("_player_in_range"): continue var m_dir = (mushroom.global_position - cam.global_position).normalized() var m_dot = forward.dot(m_dir) if m_dot > my_dot: is_best = false break elif m_dot == my_dot and mushroom.get_instance_id() > get_instance_id(): # Tie breaker is_best = false break _set_highlight(is_best) else: _set_highlight(false) func _set_highlight(value: bool) -> void: if _is_highlighted == value: return _is_highlighted = value if value: if cap: cap.material_overlay = highlight_mat if stem: stem.material_overlay = highlight_mat else: if cap: cap.material_overlay = null if stem: stem.material_overlay = null func _on_body_entered(body: Node3D) -> void: if body.is_in_group("player"): _player_in_range = true func _on_body_exited(body: Node3D) -> void: if body.is_in_group("player"): _player_in_range = false _set_highlight(false) func _unhandled_input(event: InputEvent) -> void: if _is_highlighted and event.is_action_pressed("interact"): get_viewport().set_input_as_handled() _collect_mushroom() func _collect_mushroom() -> void: # Disable interaction and visuals _is_highlighted = false _set_highlight(false) _player_in_range = false if cap: cap.visible = false if stem: stem.visible = false # Stop spreading logic spread_timer.stop() # Disable processing so it can't be interacted with again set_process_unhandled_input(false) set_process(false) # Play sound _audio_player.play() # Wait for sound to finish, then free await _audio_player.finished queue_free() func _setup_timer() -> void: spread_timer.wait_time = randf_range(spread_interval_min, spread_interval_max) spread_timer.start() func _on_spread_timer_timeout() -> void: if total_mushrooms >= max_total_mushrooms: return if _count_nearby_mushrooms() >= max_mushrooms_in_radius: # Too crowded, try again later _setup_timer() return _attempt_spread() _setup_timer() func _attempt_spread() -> void: var random_angle := randf_range(0, TAU) var random_dist := randf_range(spread_radius * 0.5, spread_radius) var offset := Vector3(cos(random_angle) * random_dist, 5.0, sin(random_angle) * random_dist) ground_ray.position = offset ground_ray.force_raycast_update() if ground_ray.is_colliding(): var collision_point := ground_ray.get_collision_point() var collision_normal := ground_ray.get_collision_normal() # Only spread on relatively flat surfaces if collision_normal.dot(Vector3.UP) > 0.7: _spawn_mushroom(collision_point) func _spawn_mushroom(pos: Vector3) -> void: var mushroom_scene = load(scene_file_path) var new_mushroom = mushroom_scene.instantiate() get_parent().add_child(new_mushroom) new_mushroom.global_position = pos # Visual feedback/animation new_mushroom.scale = Vector3.ZERO var tween = create_tween() tween.tween_property(new_mushroom, "scale", Vector3.ONE * randf_range(0.8, 1.2), 1.0).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) func _count_nearby_mushrooms() -> int: var count := 0 for mushroom in get_tree().get_nodes_in_group("mushrooms"): if mushroom == self: continue if global_position.distance_to(mushroom.global_position) < check_radius: count += 1 return count func _exit_tree() -> void: total_mushrooms -= 1