227 lines
7.0 KiB
GDScript
227 lines
7.0 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.
|
|
const Stoffe = preload("res://scripts/stoffe.gd")
|
|
static var _mesh: ArrayMesh
|
|
static var _mat: StandardMaterial3D
|
|
static var _gold: StandardMaterial3D ## fürs Zählen beim Aufdecken
|
|
var _body: MeshInstance3D
|
|
|
|
|
|
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()
|
|
# Knochen statt Plastik: Vertexfarben bleiben (Körper hell, Augen
|
|
# dunkel), der Shader legt Maserung und einen warmen Kantenschein darüber.
|
|
_mat = Stoffe.knochen()
|
|
if _gold == null:
|
|
# Fürs Zählen: dieselbe Fläche, gold getönt - die Augen bleiben dunkel.
|
|
_gold = _mat.duplicate()
|
|
_gold.albedo_color = Color(1.0, 0.82, 0.35)
|
|
_gold.emission_enabled = true
|
|
_gold.emission = Color(0.9, 0.65, 0.2)
|
|
_gold.emission_energy_multiplier = 0.5
|
|
_body = MeshInstance3D.new()
|
|
_body.mesh = _mesh
|
|
_body.material_override = _mat
|
|
add_child(_body)
|
|
|
|
|
|
## Beim Zählen leuchtet jeder passende Würfel kurz auf.
|
|
func hervorheben(on: bool) -> void:
|
|
_body.material_override = _gold if on else _mat
|
|
|
|
|
|
# ------------------------------------------------------------ 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
|