extends Control # DropCatcher.gd (manual-drag version) # No longer uses Godot's built-in _drop_data. Instead it exposes # paint_at_screen_pos(), which the trapezoid calls on release. # This Control no longer needs to catch mouse events at all, so it can be # tiny/invisible — but keep it in the tree so it can reach the camera & board. @export var camera: Camera3D @export var main: Node # the ROOT node (DartboardProto) const RAY_LENGTH := 1000.0 var dartboard: MeshInstance3D # Standard dartboard number sequence, clockwise starting from the TOP (12 o'clock). const DART_NUMBERS := [20, 1, 18, 4, 13, 6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5] # Calibration: rotate which sector counts as "top". If the wrong number gets # hit, bump this by 1 until it lines up. Range 0-19. @export var sector_offset := 0 # If your board's numbers go counter-clockwise in 3D, flip this. @export var reverse_direction := false # --- Radial band boundaries (fractions of outer radius, 0=center 1=rim) --- # Tune these in the Inspector by reading the norm= prints for each ring. @export var bull_inner_max := 0.04 # <= this = inner bull (50) @export var bull_outer_max := 0.09 # <= this = outer bull (25) @export var inner_max := 0.45 # <= this = inner single @export var triple_max := 0.55 # <= this = triple ring @export var outer_max := 0.80 # <= this = outer single; above = double # Stores which modifier is applied to each surface index: { surface_index: Modifier } var applied_modifiers: Dictionary = {} func _ready() -> void: # Don't block mouse input; the trapezoids handle their own dragging. mouse_filter = Control.MOUSE_FILTER_IGNORE print("DropCatcher ready. camera=", camera, " main=", main) func _get_board() -> MeshInstance3D: if dartboard == null and main != null and "dartboard" in main: dartboard = main.dartboard return dartboard # Public: try to apply a modifier (paint its texture + store its rule) at a # screen position. Returns true if it hit the board, false if it missed. func apply_modifier_at_screen_pos(screen_pos: Vector2, modifier: Resource) -> bool: var board := _get_board() if board == null: push_error("DropCatcher: no dartboard mesh (is 'main' set?).") return false if camera == null: push_error("DropCatcher: camera export not set.") return false if modifier == null: return false var ray_origin := camera.project_ray_origin(screen_pos) var ray_dir := camera.project_ray_normal(screen_pos) var ray_end := ray_origin + ray_dir * RAY_LENGTH var space_state := get_viewport().world_3d.direct_space_state var query := PhysicsRayQueryParameters3D.create(ray_origin, ray_end) query.collide_with_areas = false query.collide_with_bodies = true var result := space_state.intersect_ray(query) if result.is_empty(): print(" drop missed the board") return false var surface_index := _surface_from_hit(result, board) if surface_index < 0: return false _paint_surface(board, surface_index, modifier.texture) applied_modifiers[surface_index] = modifier # remember the scoring rule print(" applied '", modifier.id, "' to surface ", surface_index) return true # Call this when a dart lands on a surface to get the modified score. func score_for_surface(surface_index: int, base_value: int) -> int: if applied_modifiers.has(surface_index): return applied_modifiers[surface_index].apply_score(base_value) return base_value func _surface_from_hit(result: Dictionary, board: MeshInstance3D) -> int: var hit_point: Vector3 = result.get("position", Vector3.ZERO) var local := board.to_local(hit_point) var aabb := board.get_aabb() var center := aabb.position + aabb.size * 0.5 # Board disc lies in the X/Y plane (thin on Z). Measure radius/angle in X/Y. var dx := local.x - center.x var dy := local.y - center.y var radius := Vector2(dx, dy).length() var outer_radius : float = max(aabb.size.x, aabb.size.y) * 0.5 var norm := radius / outer_radius if outer_radius > 0.0 else 0.0 print(" local=", local, " dx=", "%.3f" % dx, " dy=", "%.3f" % dy, " norm=", "%.3f" % norm) # --- RADIAL BAND --- # These thresholds are fractions of the outer radius. TUNE to match your art: # the real proportions of a dartboard, from center outward. var seg_name := "" if norm <= bull_inner_max: seg_name = "bull_inner" # inner bull (50) elif norm <= bull_outer_max: seg_name = "bull_outer" # outer bull (25) elif norm > 1.0: print(" outside board") return -1 else: # --- ANGULAR SECTOR --- # atan2 gives angle of the hit point. We map it to one of 20 sectors, # then index into the dartboard number sequence. var angle := atan2(dx, dy) # -PI..PI, 0 = straight up (+y = top) # Convert to 0..1 going clockwise from top. Adjust the rotation so 0 = top. var t := angle / (2.0 * PI) t = fposmod(t, 1.0) if reverse_direction: t = 1.0 - t var sector_index := int(floor(t * 20.0 + 0.5)) % 20 sector_index = (sector_index + sector_offset) % 20 var number : int = DART_NUMBERS[sector_index] # Which ring band within the sector. var band := "" if norm <= inner_max: band = "inner" # inner single elif norm <= triple_max: band = "triple" # triple ring elif norm <= outer_max: band = "outer" # outer single else: band = "double" # double ring seg_name = "seg_%d_%s" % [number, band] print(" norm=", "%.3f" % norm, " -> ", seg_name) return _index_of_surface_named(board, seg_name) func _index_of_surface_named(board: MeshInstance3D, target_name: String) -> int: var mesh := board.mesh for i in mesh.get_surface_count(): var mat := mesh.surface_get_material(i) if mat and mat.resource_name == target_name: return i return -1 func _paint_surface(board: MeshInstance3D, surface_index: int, tex: Texture2D) -> void: # Always build a fresh StandardMaterial3D so the texture shows as an IMAGE, # not tinted by the old base color. If the segment looks like a flat color # instead of the pattern, the mesh is missing proper UVs on that face. var new_mat := StandardMaterial3D.new() new_mat.albedo_color = Color.WHITE # white so texture isn't tinted new_mat.albedo_texture = tex new_mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR new_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED board.set_surface_override_material(surface_index, new_mat)