facets = []
clear_children()
add_base_sphere()
target_color = possible_targets.pick_random()
var rng = RandomNumberGenerator.new()
red_index = rng.randi_range(0, num_facets - 1)
alternate_colors.clear()
if level >= 2: # For level 2+.
alternate_colors.append_array([Color(0.0, 0.5, 1.0), Color(0.0, 1.0, 0.5)]) # Add colors.
if level >= 3: # For level 3+.
alternate_colors.append(Color(1.0, 0.5, 0.0)) # Add color.
if level >= 4: # For level 4+.
alternate_colors.append(Color(0.5, 0.0, 1.0)) # Add color.
if level >= 5: # For level 5+.
alternate_colors.append(Color(1.0, 1.0, 0.0)) # Add color.
generate_facets()
if status_label: # If status label.
status_label.text = "" # Clear text.
status_label.modulate = Color.WHITE # Set color to white.
if win_image: # Hide win image
win_image.visible = false
if countdown_image: # Hide countdown image
countdown_image.visible = false
if next_button: # If next button.
next_button.visible = false # Hide.
if play_again_button: # If play again button.
play_again_button.visible = false # Hide.
func clear_children():
var children = get_children() # Get all children.
for child in children: # Loop through children.
if child is MeshInstance3D or child is Area3D: # If mesh or area.
remove_child(child) # Remove from parent.
child.free() # Free the node.
func add_base_sphere():
var base_mesh = MeshInstance3D.new() # Create new mesh instance.
var sphere_mesh = SphereMesh.new() # Create sphere mesh.
sphere_mesh.radius = sphere_radius # Set radius.
sphere_mesh.height = sphere_radius * 2 # Set height.
sphere_mesh.radial_segments = 128 # Set radial segments.
sphere_mesh.rings = 64 # Set rings.
base_mesh.mesh = sphere_mesh # Assign mesh.
add_child(base_mesh) # Add to scene.
var material = StandardMaterial3D.new() # Create material.
material.albedo_color = base_color # Set albedo color.
material.emission_enabled = true # Enable emission.
material.emission = base_color # Set emission color to base.
material.emission_energy_multiplier = 0.2 * (sphere_radius / 4.0) # Scale emission with radius to prevent washout.
base_mesh.material_override = material # Override material.
func color_distance(c1: Color, c2: Color) -> float:
return pow(c1.r - c2.r, 2) + pow(c1.g - c2.g, 2) + pow(c1.b - c2.b, 2) # Calculate squared color distance.
func generate_facets():
var golden_ratio = (1 + sqrt(5)) / 2 # Golden ratio for distribution.
var rng = RandomNumberGenerator.new() # New RNG.
for i in range(num_facets): # Loop for each facet.
var y = 1 - (i / float(num_facets - 1)) * 2 # Calculate y position.
var radius_at_y = sqrt(1 - y * y) # Radius at y.
var theta = 2 * PI * i / golden_ratio # Theta for spiral.
var x = cos(theta) * radius_at_y # X position.
var z = sin(theta) * radius_at_y # Z position.
var facet_position = Vector3(x, y, z).normalized() * sphere_radius # Normalized position.
var normal = facet_position.normalized() # Normal vector.
var facet = Area3D.new() # New area node for facet.
facet.translate(facet_position) # Translate to position.
add_child(facet) # Add to scene.
facet.set_meta("original_pos", facet_position) # Set meta for original position.
facet.set_meta("original_normal", normal) # Set meta for normal.
facets.append(facet) # Add to facets array.
var up = normal # Up vector as normal.
var arbitrary = Vector3.UP if abs(up.dot(Vector3.UP)) < 0.99 else Vector3.FORWARD # Arbitrary vector for cross.
var right = up.cross(arbitrary).normalized() # Right vector.
var forward = right.cross(up).normalized() # Forward vector.
facet.transform.basis = Basis(right, up, forward) # Set basis for orientation.
facet.collision_layer = 1 # Set collision layer.
facet.collision_mask = 1 # Set collision mask.
var mesh_instance = MeshInstance3D.new() # New mesh instance.
var cylinder_mesh = create_beveled_cylinder_mesh(facet_height, facet_radius, radial_segments, bevel_height, bevel_inset) # Create beveled mesh.
mesh_instance.mesh = cylinder_mesh # Assign mesh.
mesh_instance.translate(Vector3(0, facet_height / 2, 0)) # Translate outward.
facet.add_child(mesh_instance) # Add mesh to facet.
var collision_shape = CollisionShape3D.new() # New collision shape.
var cylinder_shape = CylinderShape3D.new() # New cylinder shape.
cylinder_shape.radius = facet_radius # Set radius.
cylinder_shape.height = facet_height # Set height.
collision_shape.shape = cylinder_shape # Assign shape.
collision_shape.translate(Vector3(0, facet_height / 2, 0)) # Translate to match mesh.
collision_shape.disabled = false # Enable shape.
facet.add_child(collision_shape) # Add to facet.
var material = StandardMaterial3D.new() # New material.
var facet_color: Color # Facet color variable.
var emission_mult: float # Emission multiplier.
if i == red_index: # If target facet.
facet_color = target_color # Set to target color.
emission_mult = 0.5 * (sphere_radius / 4.0) # Scale emission with radius.
elif alternate_colors.size() > 0: # If distractors available.
var distractor_color = alternate_colors[rng.randi() % alternate_colors.size()] # Pick distractor.
if color_distance(distractor_color, target_color) < COLOR_SIMILARITY_THRESHOLD: # If too similar.
facet_color = base_color # Use base.
emission_mult = 0.2 * (sphere_radius / 4.0) # Scale emission.
else: # Not similar.
facet_color = distractor_color # Use distractor.
emission_mult = 0.2 * (sphere_radius / 4.0) # Scale emission.
else: # No distractors.
facet_color = base_color # Use base.
emission_mult = 0.2 * (sphere_radius / 4.0) # Scale emission.
material.albedo_color = facet_color # Set albedo.
material.emission_enabled = true # Enable emission.
material.emission = facet_color # Set emission to color.
material.emission_energy_multiplier = emission_mult # Set scaled intensity.
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA # Enable alpha transparency for fade.
mesh_instance.material_override = material # Override material.
facet.set_meta("facet_index", i) # Set facet index meta.
var audio_player = AudioStreamPlayer3D.new() # New 3D audio player for positional sound.
audio_player.stream = pop_sound # Set the preloaded pop sound.
audio_player.volume_db = 0.0 # Set volume (adjust as needed, e.g., -10 for quieter).
audio_player.pitch_scale = 1.0 + randf_range(-0.2, 0.2) # Slight pitch variation for natural pops.
facet.add_child(audio_player) # Add audio to facet.
func create_beveled_cylinder_mesh(height: float, radius: float, segments: int, bh: float, bi: float) -> Mesh:
if bh > height * 0.4: # Cap bevel height.
bh = height * 0.4 # Adjust if too large.
if bi > radius * 0.5: # Cap bevel inset.
bi = radius * 0.5 # Adjust if too large.
var ring_y: Array[float] = [-height / 2, -height / 2 + bh, height / 2 - bh, height / 2] # Ring y positions.
var ring_rad: Array[float] = [radius - bi, radius, radius, radius - bi] # Ring radii.
var num_rings: int = 4 # Number of rings.
var st = SurfaceTool.new() # New surface tool.
st.begin(Mesh.PRIMITIVE_TRIANGLES) # Start triangle mesh.
for ring in num_rings: # Loop rings.
for s in segments: # Loop segments.
var theta = 2 * PI * s / segments # Calculate theta.
var x = cos(theta) * ring_rad[ring] # X coord.
var z = sin(theta) * ring_rad[ring] # Z coord.
st.add_vertex(Vector3(x, ring_y[ring], z)) # Add vertex.
for ring in range(num_rings - 1): # Loop for sides.
for s in segments: # Loop segments.
var s1 = (s + 1) % segments # Next segment.
var base = ring * segments # Base index.
var base_next = (ring + 1) * segments # Next base.
st.add_index(base + s) # Add index 1.
st.add_index(base_next + s) # Add index 2.
st.add_index(base + s1) # Add index 3.
st.add_index(base + s1) # Add index 4.
st.add_index(base_next + s) # Add index 5.
st.add_index(base_next + s1) # Add index 6.
var vertex_count = num_rings * segments # Total vertices.
var bottom_center = vertex_count # Bottom center index.
st.add_vertex(Vector3(0, ring_y[0], 0)) # Add bottom center.
var bottom_ring_base = 0 # Bottom ring base.
for s in segments: # Loop for bottom cap.
var s1 = (s + 1) % segments # Next.
st.add_index(bottom_center) # Center.
st.add_index(bottom_ring_base + s) # Current.
st.add_index(bottom_ring_base + s1) # Next.
var top_center = vertex_count + 1 # Top center index.
st.add_vertex(Vector3(0, ring_y[3], 0)) # Add top center.
var top_ring_base = (num_rings - 1) * segments # Top ring base.
for s in segments: # Loop for top cap.
var s1 = (s + 1) % segments # Next.
st.add_index(top_center) # Center.
st.add_index(top_ring_base + s1) # Next (reversed for winding).
st.add_index(top_ring_base + s) # Current.
st.generate_normals() # Generate normals.
var mesh = ArrayMesh.new() # New array mesh.
st.commit(mesh) # Commit to mesh.
return mesh # Return the mesh.
func handle_click(click_pos: Vector2):
if not camera or not is_level_active: # Ignore clicks during countdown
return
var from = camera.project_ray_origin(click_pos) # Ray origin from camera.
var to = from + camera.project_ray_normal(click_pos) * 1000 # Ray end.
var space_state = get_world_3d().direct_space_state # Get space state.
var query = PhysicsRayQueryParameters3D.create(from, to) # Create query.
query.collide_with_areas = true # Collide with areas.
query.collision_mask = 1 # Collision mask.
var result = space_state.intersect_ray(query) # Intersect ray.
if result: # If hit.
if result.collider and result.collider.has_meta("facet_index"): # If has facet index.
var clicked_index = result.collider.get_meta("facet_index") # Get index.
var clicked_mesh = result.collider.get_child(0) # Get mesh child.
var clicked_color = clicked_mesh.material_override.albedo_color # Get color.
var clicked_material = clicked_mesh.material_override # Get material for fade.
var audio_player = result.collider.get_child(2) # Get audio player (now index 2 after mesh and collision).
var pop_tween = get_tree().create_tween().set_parallel(true) # Create parallel tween for pop.
pop_tween.tween_property(clicked_mesh, "scale", Vector3(1.2, 1.5, 1.2), 0.1) # Inflate with more y for rounder top in 0.1s.
pop_tween.chain().tween_property(clicked_mesh, "scale", Vector3.ZERO, 0.15) # Then burst to zero in 0.15s for faster pop.
pop_tween.tween_property(clicked_material, "albedo_color:a", 0.0, 0.25) # Fade alpha to 0 over 0.25s.
pop_tween.tween_callback(audio_player.play).set_delay(0.1) # Play sound at pop moment (after inflate).
pop_tween.tween_callback(result.collider.queue_free).set_delay(0.25) # Free facet after animation.
# Fire laser beam on every click
if cannon:
create_laser_beam(cannon.global_transform * barrel_tip_offset, result.position) # Use barrel tip
if clicked_color.is_equal_approx(target_color): # If matches target.
if game_timer: # If timer.
leftover_time = game_timer.time_left # Save leftover time.
game_timer.stop() # Stop timer.
if timer_label: # If label.
timer_label.text = "Time: " + str(snapped(leftover_time, 0.1)) # Update label.
if status_label: # If status.
status_label.visible = false # Hide status label to replace with image.
if win_image: # Show win image instead of text.
win_image.visible = true
if next_button: # If next button.
next_button.visible = true # Show next.
else: # Wrong click.
pop_tween.tween_property(clicked_material, "albedo_color", Color(0.5, 0.5, 0.5), 0.25) # Gray out for wrong pop (fizzle effect).
else: # No hit.
pass # Do nothing.
func create_laser_beam(start: Vector3, end: Vector3):
var direction = (end - start).normalized() # Direction vector
var distance = start.distance_to(end) # Full distance
var beam_start = start # Start beam from barrel tip
var adjusted_end = end - direction * 0.1 # Shorten slightly to end on edge (adjust 0.1 as needed)
var adjusted_distance = beam_start.distance_to(adjusted_end) # Adjusted length
var laser = MeshInstance3D.new() # New mesh for laser
var cylinder = CylinderMesh.new() # Thin cylinder for beam
cylinder.top_radius = 0.05 # Thin radius; adjust
cylinder.bottom_radius = 0.05
cylinder.height = adjusted_distance # Adjusted length
laser.mesh = cylinder
add_child(laser) # Add to scene root (global space)
# Position at beam_start and orient
laser.global_position = beam_start
laser.look_at(adjusted_end, Vector3.UP) # Orient towards adjusted end
laser.rotate_object_local(Vector3(1, 0, 0), PI / 2) # Rotate to align cylinder axis with direction
# Material: Glowing red laser
var mat = StandardMaterial3D.new()
mat.albedo_color = Color.RED
mat.emission_enabled = true
mat.emission = Color.RED
mat.emission_energy_multiplier = 2.0
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
laser.material_override = mat
# Play laser sound using persistent audio player
if laser_audio:
laser_audio.global_transform.origin = beam_start # Update position
laser_audio.play()
print("Laser sound triggered") # Debug print
# Fade out and free after 0.5s
var tween = get_tree().create_tween()
tween.tween_property(mat, "albedo_color:a", 0.0, 0.5)
tween.tween_callback(laser.queue_free)
func _on_timer_timeout():
if timer_label: # If timer label.
timer_label.text = "Lose!" # Set lose text.
if status_label: # If status.
status_label.text = "Game Over!" # Set game over.
status_label.modulate = Color.RED # Red color.
status_label.visible = true # Show.
game_over = true # Set game over flag.
if next_button: # If next button.
next_button.visible = false # Hide.
if play_again_button: # If play again.
play_again_button.visible = true # Show.
if game_over and red_index >= 0 and red_index < (get_child_count() - 1): # If valid target.
var target_facet = get_child(red_index + 1) # Get target facet (skip base).
if target_facet is Area3D: # If area.
var target_pos = target_facet.global_transform.origin.normalized() # Normalized position.
var target_rotation = Basis.looking_at(-target_pos, Vector3.UP).get_euler() # Rotation to look at target.
var rotation_tween = get_tree().create_tween() # Create tween for rotation.
if rotation_tween: # If tween.
var rot_prop = rotation_tween.tween_property(self, "rotation", target_rotation, 1.0) # Tween rotation.
rot_prop.set_trans(Tween.TRANS_QUAD) # Quad transition.
rot_prop.set_ease(Tween.EASE_OUT) # Ease out.
var target_mesh = target_facet.get_child(0) # Get mesh.
if target_mesh and target_mesh.material_override: # If material.
var material = target_mesh.material_override.duplicate() # Duplicate material.
material.emission_energy_multiplier = 0.5 # Set initial emission.
target_mesh.material_override = material # Assign.
var pulse_tween = get_tree().create_tween() # Create pulse tween.
if pulse_tween: # If tween.
var emission_up = pulse_tween.tween_property(material, "emission_energy_multiplier", 0.8, 1.0) # Tween emission up.
emission_up.set_trans(Tween.TRANS_SINE) # Sine transition.
emission_up.set_ease(Tween.EASE_IN_OUT) # Ease in out.
var scale_up = pulse_tween.tween_property(target_mesh, "scale", Vector3(1.2, 1.2, 1.2), 1.0) # Scale up.
scale_up.set_trans(Tween.TRANS_SINE) # Sine.
scale_up.set_ease(Tween.EASE_IN_OUT) # Ease.
var emission_down = pulse_tween.tween_property(material, "emission_energy_multiplier", 0.5, 1.0) # Emission down.
emission_down.set_trans(Tween.TRANS_SINE) # Sine.
emission_down.set_ease(Tween.EASE_IN_OUT) # Ease.
var scale_down = pulse_tween.tween_property(target_mesh, "scale", Vector3(1.0, 1.0, 1.0), 1.0) # Scale down.
scale_down.set_trans(Tween.TRANS_SINE) # Sine.
scale_down.set_ease(Tween.EASE_IN_OUT) # Ease.
func _on_next_level_pressed():
if not game_over: # If not over.
set_level(level + 1) # Advance level.
if status_label: # If status.
status_label.text = "" # Clear.
if win_image: # Hide win image
win_image.visible = false
if next_button: # If next.
next_button.visible = false # Hide.
if play_again_button: # If play again.
play_again_button.visible = false # Hide.
func _on_play_again_pressed():
game_over = false # Reset game over.
angular_velocity = Vector3.ZERO # Reset velocity.
if status_label: # If status.
status_label.text = "" # Clear.
status_label.visible = false # Hide.
if win_image: # Hide win image
win_image.visible = false
if play_again_button: # If play again.
play_again_button.visible = false # Hide.
if next_button: # If next.
next_button.visible = false # Hide.
if game_timer: # If timer.
leftover_time = 0.0 # Reset leftover.
game_timer.stop() # Stop timer.
set_level(1) # Reset to level 1.
func set_level(new_level: int):
level = new_level # Set new level.
var base_time = 10.0 # Base time.
var bonus_time = leftover_time # Bonus from leftover.
if level >= 10: # For high levels.
bonus_time += 5.0 # Extra bonus.
var new_wait_time = base_time + bonus_time # Total time.
if game_timer: # If timer.
game_timer.wait_time = new_wait_time # Set time.
sphere_radius = 4.0 + (level - 1) * 0.3 # Update radius with smaller increment.
num_facets = base_num_facets + (level - 1) * 50 # Update facets.
if camera: # If camera.
camera.position = Vector3(0, 0, sphere_radius * 2.0) # Pull camera further back
camera.look_at(Vector3.ZERO) # Look at center.
camera.far = sphere_radius * 3.0 + 100.0 # Dynamically increase far clip to avoid clipping/washout (buffer for safety).
original_camera_pos = camera.position # Store original pos for shake.
start_level() # Start the level.```