Files
DartGame/game/core/ui/drop_catcher.gd
T

151 lines
4.3 KiB
GDScript

extends Control
## Screen position -> board slot. That's the whole job.
##
## Scoring lives in Phase 2's ScoringEngine. This resolves WHERE, not WHAT IT'S
## WORTH. Keeping them separate means the modifier UI can ship before the
## scoring pipeline exists.
##
## HOW IT WORKS
## ------------
## The raycast returns a `face_index` — which triangle of the trimesh was hit.
## Dartboard maps that face to a material slot name. The material name IS the
## sector.
##
## No trigonometry. No calibration constants. Survives the board being rotated,
## tilted, moved, or having its sectors remapped — because we're reading the
## geometry, not calculating from an assumed layout.
##
## This is only possible because the board has a ConcavePolygonShape3D (trimesh)
## collider. A box or cylinder gives you no face_index.
signal slot_hovered(slot: StringName)
signal slot_dropped(slot: StringName, modifier: Modifier)
const RAY_LENGTH := 1000.0
@export var camera: Camera3D
@export var dartboard: Dartboard
@export_group("Debug")
## Print every hover. Noisy but useful when calibrating.
@export var log_hovers := false
## slot -> Modifier
var applied: Dictionary = {}
var _last_hovered: StringName = &""
func _ready() -> void:
# The tray's pieces need to receive mouse events. This node is just a
# service — it must never eat them.
mouse_filter = Control.MOUSE_FILTER_IGNORE
if camera == null:
push_error("DropCatcher: `camera` export is not set.")
if dartboard == null:
push_error("DropCatcher: `dartboard` export is not set.")
# --- Public -------------------------------------------------------------
## Which slot is under this screen position? &"" if the board isn't there.
func slot_at_screen_pos(screen_pos: Vector2) -> StringName:
if camera == null or dartboard == null:
return &""
var hit := _raycast(screen_pos)
if hit.is_empty():
return &""
# Only the board counts. The SpiderBody (layer 4) is a bounce surface, not
# a drop target.
if hit.get("collider") != dartboard:
return &""
var face: int = hit.get("face_index", -1)
if face < 0:
push_warning("Raycast returned no face_index. Is the collider a trimesh?")
return &""
var slot := dartboard.slot_for_face(face)
if slot == &"":
push_warning("Face %d didn't map to any slot." % face)
return slot
## Called every frame by a piece being dragged, so it can highlight.
func hover(screen_pos: Vector2) -> StringName:
var slot := slot_at_screen_pos(screen_pos)
if slot != _last_hovered:
_last_hovered = slot
slot_hovered.emit(slot)
if log_hovers and slot != &"":
print("hover: %s" % slot)
return slot
## Drop a modifier at a screen position. True if it landed on a valid target.
func apply_modifier_at_screen_pos(screen_pos: Vector2, modifier: Modifier) -> bool:
if modifier == null:
push_warning("DropCatcher: null modifier.")
return false
var slot := slot_at_screen_pos(screen_pos)
if slot == &"":
return false # missed the board
if not _is_valid_target(slot):
return false # hit the board, but not somewhere a modifier can go
dartboard.paint_slot(slot, modifier.texture)
applied[slot] = modifier
slot_dropped.emit(slot, modifier)
print("applied '%s' -> %s" % [modifier.id, slot])
return true
## Which slots can hold a modifier?
##
## Sectors: yes. Bulls: yes (it's a slot in its own right — the bull modifier
## affects inner and outer together). Flat tire: no — it's the outer band, not
## a scoring region.
func _is_valid_target(slot: StringName) -> bool:
return dartboard.is_sector(slot) or slot == Dartboard.BULL_SLOT
func modifier_at(slot: StringName) -> Modifier:
return applied.get(slot)
func clear(slot: StringName) -> void:
applied.erase(slot)
dartboard.clear_slot(slot)
func clear_all() -> void:
applied.clear()
dartboard.clear_all_slots()
# --- Internals ----------------------------------------------------------
func _raycast(screen_pos: Vector2) -> Dictionary:
var origin := camera.project_ray_origin(screen_pos)
var params := PhysicsRayQueryParameters3D.create(
origin,
origin + camera.project_ray_normal(screen_pos) * RAY_LENGTH
)
params.collide_with_areas = false
params.collide_with_bodies = true
# Don't let the dart block the drop. It's on layer 2; the board is layer 1.
params.collision_mask = 1
return get_viewport().world_3d.direct_space_state.intersect_ray(params)