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