Files
pirate-dice/scripts/die.gd
2026-09-07 04:29:23 +02:00

215 lines
6.5 KiB
GDScript

extends RigidBody3D
class_name Die
## Ein physikalischer Würfel mit abgerundeten Kanten.
## Der Wert wird aus der Ruhelage abgelesen.
const SIZE := 0.15
const HALF := SIZE * 0.5
const CORNER := SIZE * 0.12 ## Rundungsradius der Kanten und Ecken
const SEGMENTS := 6 ## Unterteilung je Würfelseite
const PIP_R := SIZE * 0.11 ## Radius eines Auges
const PIP_SEGS := 8
const COL_BODY := Color(0.94, 0.92, 0.86)
const COL_PIP := Color(0.03, 0.03, 0.04)
## Lokale Achse -> aufgedruckter Wert (gegenüberliegende Seiten ergeben 7)
const FACE_AXES: Array[Vector3] = [
Vector3.RIGHT, Vector3.LEFT, Vector3.UP, Vector3.DOWN, Vector3.BACK, Vector3.FORWARD
]
const FACE_VALUES: Array[int] = [1, 6, 2, 5, 3, 4]
const PIPS := {
1: [Vector2(0, 0)],
2: [Vector2(-1, -1), Vector2(1, 1)],
3: [Vector2(-1, -1), Vector2(0, 0), Vector2(1, 1)],
4: [Vector2(-1, -1), Vector2(-1, 1), Vector2(1, -1), Vector2(1, 1)],
5: [Vector2(-1, -1), Vector2(-1, 1), Vector2(0, 0), Vector2(1, -1), Vector2(1, 1)],
6: [Vector2(-1, -1), Vector2(-1, 0), Vector2(-1, 1),
Vector2(1, -1), Vector2(1, 0), Vector2(1, 1)],
}
# Körper und Augen stecken in einem gemeinsamen Mesh mit Vertexfarben,
# damit ein Würfel nur ein einziges Renderobjekt ist.
static var _mesh: ArrayMesh
static var _mat: StandardMaterial3D
func _ready() -> void:
mass = 0.03
contact_monitor = false
can_sleep = true
linear_damp = 0.25
angular_damp = 0.35
var pm := PhysicsMaterial.new()
pm.friction = 0.6
pm.bounce = 0.25
physics_material_override = pm
var shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3.ONE * SIZE
shape.shape = box
add_child(shape)
_build_visuals()
# ------------------------------------------------------------ Aufbau
static func _face_tangents(n: Vector3) -> Array:
var u := Vector3.UP if absf(n.y) < 0.9 else Vector3.RIGHT
u = (u - n * u.dot(n)).normalized()
return [u, n.cross(u)]
## Schiebt einen Punkt der Würfeloberfläche auf die abgerundete Form
## (Minkowski-Summe aus kleinerem Würfel und Kugel).
static func _round_point(p: Vector3) -> Vector3:
var inner := HALF - CORNER
var c := Vector3(clampf(p.x, -inner, inner), clampf(p.y, -inner, inner),
clampf(p.z, -inner, inner))
var d := p - c
if d.length_squared() < 1e-9:
return p
return c + d.normalized() * CORNER
static func _build_mesh() -> ArrayMesh:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
for f in FACE_AXES.size():
var n: Vector3 = FACE_AXES[f]
var t := _face_tangents(n)
st.set_color(COL_BODY)
for i in SEGMENTS:
for j in SEGMENTS:
var quad: Array[Vector3] = []
for c: Vector2 in [Vector2(i, j), Vector2(i + 1, j),
Vector2(i + 1, j + 1), Vector2(i, j + 1)]:
var u := (c.x / SEGMENTS) * 2.0 - 1.0
var v := (c.y / SEGMENTS) * 2.0 - 1.0
quad.append(_round_point(n * HALF + t[0] * u * HALF + t[1] * v * HALF))
for k in [0, 2, 1, 0, 3, 2]:
st.add_vertex(quad[k])
_add_pips(st, n, t, FACE_VALUES[f])
st.index()
st.generate_normals()
return st.commit()
## Augen als flache Scheiben knapp über der Seitenfläche, gleiche Surface.
static func _add_pips(st: SurfaceTool, n: Vector3, t: Array, value: int) -> void:
st.set_color(COL_PIP)
var spread := HALF * 0.50
var lift: Vector3 = n * (HALF + 0.0012)
for p: Vector2 in PIPS[value]:
var mid: Vector3 = lift + t[0] * p.x * spread + t[1] * p.y * spread
var ring: Array[Vector3] = []
for k in PIP_SEGS:
var a := TAU * k / PIP_SEGS
ring.append(mid + (t[0] * cos(a) + t[1] * sin(a)) * PIP_R)
for k in PIP_SEGS:
st.add_vertex(mid)
st.add_vertex(ring[(k + 1) % PIP_SEGS])
st.add_vertex(ring[k])
func _build_visuals() -> void:
if _mesh == null:
_mesh = _build_mesh()
_mat = StandardMaterial3D.new()
_mat.vertex_color_use_as_albedo = true
# Kamera und Deckenlicht stehen beide senkrecht: bei glatter Oberfläche
# liegt der Glanzpunkt genau auf der Fläche und hellt die Augen auf.
_mat.roughness = 0.9
_mat.metallic_specular = 0.0
var body := MeshInstance3D.new()
body.mesh = _mesh
body.material_override = _mat
add_child(body)
# ------------------------------------------------------------ Zustand
## Wert der aktuell nach oben zeigenden Seite.
func top_value() -> int:
var best := -2.0
var val := 1
for i in FACE_AXES.size():
var d: float = (global_transform.basis * FACE_AXES[i]).y
if d > best:
best = d
val = FACE_VALUES[i]
return val
## Wie sauber der Würfel liegt (1.0 = perfekt flach).
func flatness() -> float:
var best := -2.0
for a in FACE_AXES:
best = maxf(best, (global_transform.basis * a).y)
return best
func is_settled() -> bool:
return sleeping or (linear_velocity.length() < 0.06 and angular_velocity.length() < 0.15)
func throw_from(origin: Vector3, rng: RandomNumberGenerator) -> void:
freeze = false
sleeping = false
var t := Transform3D()
t.basis = Basis(Vector3(rng.randf(), rng.randf(), rng.randf()).normalized(), rng.randf() * TAU)
t.origin = origin
global_transform = t
linear_velocity = Vector3(rng.randf_range(-1.5, 1.5), rng.randf_range(-0.3, 0.0),
rng.randf_range(-1.5, 1.5))
angular_velocity = Vector3(rng.randf_range(-24, 24), rng.randf_range(-24, 24),
rng.randf_range(-24, 24))
func nudge(rng: RandomNumberGenerator) -> void:
sleeping = false
apply_central_impulse(Vector3(rng.randf_range(-0.04, 0.04), 0.06, rng.randf_range(-0.04, 0.04)))
apply_torque_impulse(Vector3(rng.randf_range(-0.008, 0.008), rng.randf_range(-0.008, 0.008),
rng.randf_range(-0.008, 0.008)))
## Dreht den Würfel so, dass `value` oben liegt (für Booster-Effekte).
func snap_to_value(value: int, rng: RandomNumberGenerator) -> void:
var idx := FACE_VALUES.find(value)
if idx < 0:
return
var a: Vector3 = FACE_AXES[idx]
var b := Vector3.RIGHT if absf(a.x) < 0.9 else Vector3.UP
b = (b - a * b.dot(a)).normalized()
var c := a.cross(b)
var yaw := rng.randf() * TAU
var wb := Vector3(cos(yaw), 0.0, sin(yaw))
var wa := Vector3.UP
var wc := wa.cross(wb)
var t := global_transform
t.basis = Basis(wb, wa, wc) * Basis(b, a, c).transposed()
place(t)
if not freeze:
sleeping = false
linear_velocity = Vector3.ZERO
angular_velocity = Vector3.ZERO
## Setzt die Lage und schiebt sie sofort in den Physikserver nach. Ohne das
## schreibt der Server im nächsten Schritt noch einmal die alte Lage zurück -
## der Würfel zeigt dann einen Frame lang die falsche Augenzahl.
func place(t: Transform3D) -> void:
global_transform = t
PhysicsServer3D.body_set_state(get_rid(), PhysicsServer3D.BODY_STATE_TRANSFORM, t)
func set_kinematic(on: bool) -> void:
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
freeze = on