174 lines
5.5 KiB
GDScript
174 lines
5.5 KiB
GDScript
class_name Dartboard
|
|
extends StaticBody3D
|
|
|
|
## The board. 21 material slots: sector_1 .. sector_20, plus "bulls".
|
|
##
|
|
## Owns the face -> surface -> slot mapping, which is what makes hit detection
|
|
## survive the board moving, rotating, or having its sectors remapped. Ask the
|
|
## geometry which sector was hit; don't calculate it from an angle.
|
|
|
|
## Every slot on the board, in the order Blender exported them (order doesn't
|
|
## matter — we index by name).
|
|
const BULL_SLOT := &"bulls"
|
|
|
|
@onready var mesh_instance: MeshInstance3D = $Model/dartboard
|
|
|
|
## StringName -> surface index
|
|
var _slot_to_surface: Dictionary = {}
|
|
## global face index -> StringName
|
|
var _face_to_slot: Dictionary = {}
|
|
|
|
var _outer_radius: float = 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
_index_slots()
|
|
_index_faces()
|
|
_cache_radius()
|
|
|
|
|
|
# --- Indexing -----------------------------------------------------------
|
|
|
|
func _index_slots() -> void:
|
|
var mesh := mesh_instance.mesh
|
|
if mesh == null:
|
|
push_error("Dartboard has no mesh.")
|
|
return
|
|
|
|
for i in mesh.get_surface_count():
|
|
var mat := mesh.surface_get_material(i)
|
|
if mat == null:
|
|
push_warning("Surface %d has no material — can't be skinned or hit." % i)
|
|
continue
|
|
|
|
var slot := StringName(mat.resource_name)
|
|
if slot == &"":
|
|
push_warning("Surface %d's material has no name." % i)
|
|
continue
|
|
|
|
_slot_to_surface[slot] = i
|
|
|
|
print("Dartboard: indexed %d slots." % _slot_to_surface.size())
|
|
if _slot_to_surface.size() != 21:
|
|
push_warning("Expected 21 slots (20 sectors + bulls), got %d." % _slot_to_surface.size())
|
|
|
|
|
|
## Build a global-face-index -> slot map.
|
|
##
|
|
## The physics engine reports face_index as an index into the WHOLE trimesh,
|
|
## but the mesh stores faces per-surface. So we walk the surfaces in order,
|
|
## accumulating a running face count, and record which range belongs to which.
|
|
##
|
|
## This assumes the ConcavePolygonShape3D was built from the mesh with surfaces
|
|
## in the same order — which is what create_trimesh_shape() does.
|
|
func _index_faces() -> void:
|
|
var mesh := mesh_instance.mesh
|
|
if mesh == null:
|
|
return
|
|
|
|
var running := 0
|
|
for i in mesh.get_surface_count():
|
|
var arrays := mesh.surface_get_arrays(i)
|
|
if arrays.is_empty():
|
|
continue
|
|
|
|
var indices: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
|
|
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
|
|
# Triangle count: from the index buffer if present, else from vertices.
|
|
var tri_count := (indices.size() / 3) if indices.size() > 0 else (verts.size() / 3)
|
|
|
|
var mat := mesh.surface_get_material(i)
|
|
var slot := StringName(mat.resource_name) if mat else &""
|
|
|
|
for f in tri_count:
|
|
_face_to_slot[running + f] = slot
|
|
|
|
running += tri_count
|
|
|
|
print("Dartboard: mapped %d faces." % _face_to_slot.size())
|
|
|
|
|
|
func _cache_radius() -> void:
|
|
var aabb := mesh_instance.get_aabb()
|
|
# The disc's two large axes. The third is the board's thickness.
|
|
var sizes := [aabb.size.x, aabb.size.y, aabb.size.z]
|
|
sizes.sort()
|
|
_outer_radius = sizes[2] * 0.5 # largest dimension = diameter
|
|
|
|
|
|
# --- Public -------------------------------------------------------------
|
|
|
|
## Which slot does this trimesh face belong to?
|
|
func slot_for_face(face_index: int) -> StringName:
|
|
return _face_to_slot.get(face_index, &"")
|
|
|
|
|
|
## Paint a texture onto one slot. Used for both skins and modifier overlays.
|
|
func paint_slot(slot: StringName, texture: Texture2D) -> void:
|
|
if not _slot_to_surface.has(slot):
|
|
push_warning("No slot named '%s'." % slot)
|
|
return
|
|
|
|
var surface: int = _slot_to_surface[slot]
|
|
var mat := mesh_instance.get_active_material(surface)
|
|
if mat == null:
|
|
return
|
|
|
|
# Duplicate, or we mutate the shared material from the .glb — which leaks
|
|
# to every instance AND can get written back to disk in the editor.
|
|
var unique := mat.duplicate() as StandardMaterial3D
|
|
unique.albedo_texture = texture
|
|
unique.albedo_color = Color.WHITE # don't tint the texture
|
|
mesh_instance.set_surface_override_material(surface, unique)
|
|
|
|
|
|
## Reset a slot to its imported material.
|
|
func clear_slot(slot: StringName) -> void:
|
|
if not _slot_to_surface.has(slot):
|
|
return
|
|
mesh_instance.set_surface_override_material(_slot_to_surface[slot], null)
|
|
|
|
|
|
func get_slots() -> Array[StringName]:
|
|
var out: Array[StringName] = []
|
|
out.assign(_slot_to_surface.keys())
|
|
return out
|
|
|
|
|
|
## Board radius in local units.
|
|
func outer_radius() -> float:
|
|
return _outer_radius
|
|
|
|
|
|
## Project a LOCAL point onto the board's face plane, returning 2D coords
|
|
## where +Y is "up" on the board (toward the 20).
|
|
##
|
|
## Which two axes those are depends on how the .glb imported. glTF is +Y up,
|
|
## so a board modeled facing +Z in Blender usually lands facing +Z here, with
|
|
## the disc in the X/Y plane. If sector detection is 90 degrees off, this is
|
|
## the function to fix.
|
|
func planar_coords(local_point: Vector3) -> Vector2:
|
|
var aabb := mesh_instance.get_aabb()
|
|
var center := aabb.position + aabb.size * 0.5
|
|
|
|
# Identify the thin axis — that's the board's normal.
|
|
var s := aabb.size
|
|
if s.z <= s.x and s.z <= s.y:
|
|
return Vector2(local_point.x - center.x, local_point.y - center.y)
|
|
elif s.y <= s.x and s.y <= s.z:
|
|
return Vector2(local_point.x - center.x, local_point.z - center.z)
|
|
else:
|
|
return Vector2(local_point.z - center.z, local_point.y - center.y)
|
|
|
|
|
|
## Debug: what did Blender actually export?
|
|
func print_slots() -> void:
|
|
var mesh := mesh_instance.mesh
|
|
print("=== Dartboard slots ===")
|
|
for i in mesh.get_surface_count():
|
|
var mat := mesh.surface_get_material(i)
|
|
print(" surface %2d: '%s'" % [i, mat.resource_name if mat else "(none)"])
|
|
print(" radius: %.4f aabb: %s" % [_outer_radius, mesh_instance.get_aabb().size])
|
|
print("=======================")
|