Files
pirate-dice/scripts/table.gd
2026-09-16 13:21:56 +02:00

638 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node3D
class_name GameTable
## Baut den 3D-Tisch mit Würfelzonen, Bechern, Licht und Kamera.
const TABLE_H := 0.95
const ZONE_RAD := 0.68 ## Radius der runden Würfelzone
const WALL_SEGS := 20 ## Bandensegmente - Godot kennt keinen hohlen Zylinder
const WALL_H := 0.55 ## unsichtbare Begrenzung, damit nichts herausspringt
const SEAT_ARC := ZONE_RAD * 2.0 + 0.85 ## Bogenlänge, die ein Platz am Tischrand braucht
const MIN_ZONE_R := 1.15
const RIM_RUHE := Color(0.22, 0.15, 0.09) ## Zonenrand, wenn der Platz wartet
const RIM_ZUG := Color(0.85, 0.66, 0.22) ## Zonenrand des Platzes, der dran ist
const TILT_DEG := 15.0 ## Neigung der Kamera gegen die Senkrechte
const HUD_TOP := 0.13 ## Bildanteil oben für die Gebotsanzeige
const HUD_BOTTOM := 0.17 ## Bildanteil unten für die Ansage-Leiste
## Tisch und Zonenradius wachsen mit der Spielerzahl.
var table_r := 2.0
var zone_r := 1.2
var seat_count := 0
var local_seat := 0
var seat_dir: Array[Vector3] = []
var zones: Array[Node3D] = []
var cups: Array[Node3D] = []
var labels: Array[Label3D] = []
var cup_tweens: Array = []
var bid_counts: Array[Label3D] = []
var bid_icons: Array[Sprite3D] = []
var _bid_anchor: Array[Vector3] = []
var plates: Array[Sprite3D] = []
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
var topf_muenzen: Dictionary = {} ## Wurfnummer -> Münze in der Tischmitte
static var _muenz_mesh: CylinderMesh
static var _muenz_mat: StandardMaterial3D
var dice: Array = [] ## seat -> Array[Die]
var camera: Camera3D
func build(count: int, local_index: int) -> void:
seat_count = count
local_seat = local_index
zone_r = maxf(MIN_ZONE_R, count * SEAT_ARC / TAU)
table_r = zone_r + ZONE_RAD + 0.45
_build_room()
seat_dir.clear()
for i in count:
var ang := -PI * 0.5 + float(i - local_index) * TAU / float(count)
seat_dir.append(Vector3(cos(ang), 0.0, sin(ang)))
for i in count:
_build_seat(i)
dice.append([])
_build_camera()
func _build_room() -> void:
var env := WorldEnvironment.new()
var e := Environment.new()
e.background_mode = Environment.BG_COLOR
e.background_color = Color(0.04, 0.05, 0.07)
e.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
e.ambient_light_color = Color(0.35, 0.38, 0.45)
e.ambient_light_energy = 1.1
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
e.fog_enabled = true
e.fog_light_color = Color(0.05, 0.06, 0.09)
e.fog_density = 0.02
env.environment = e
add_child(env)
var lamp := SpotLight3D.new()
lamp.position = Vector3(0, TABLE_H + table_r * 1.45, 0)
lamp.rotation_degrees = Vector3(-90, 0, 0)
lamp.light_energy = 2.5
lamp.light_color = Color(1.0, 0.94, 0.82)
lamp.spot_range = table_r * 4.2
lamp.spot_angle = 60.0
lamp.spot_attenuation = 0.6
lamp.shadow_enabled = true
add_child(lamp)
var fill := DirectionalLight3D.new()
fill.rotation_degrees = Vector3(-55, 35, 0)
fill.light_energy = 0.25
add_child(fill)
# Boden
var floor_body := StaticBody3D.new()
var floor_col := CollisionShape3D.new()
var fs := BoxShape3D.new()
fs.size = Vector3(24, 0.4, 24)
floor_col.shape = fs
floor_col.position = Vector3(0, -0.2, 0)
floor_body.add_child(floor_col)
var floor_mi := MeshInstance3D.new()
var fpm := PlaneMesh.new()
fpm.size = Vector2(24, 24)
floor_mi.mesh = fpm
floor_mi.material_override = _mat(Color(0.08, 0.07, 0.09), 0.9)
floor_body.add_child(floor_mi)
add_child(floor_body)
# Tischplatte
var table := StaticBody3D.new()
var tcol := CollisionShape3D.new()
var tcs := CylinderShape3D.new()
tcs.radius = table_r
tcs.height = 0.12
tcol.shape = tcs
tcol.position = Vector3(0, TABLE_H - 0.06, 0)
table.add_child(tcol)
var tmi := MeshInstance3D.new()
var tcm := CylinderMesh.new()
tcm.top_radius = table_r
tcm.bottom_radius = table_r
tcm.height = 0.12
tmi.mesh = tcm
tmi.position = Vector3(0, TABLE_H - 0.06, 0)
tmi.material_override = _mat(Color(0.10, 0.28, 0.17), 0.85)
table.add_child(tmi)
add_child(table)
# Tischbein
var leg := MeshInstance3D.new()
var lcm := CylinderMesh.new()
lcm.top_radius = table_r * 0.09
lcm.bottom_radius = table_r * 0.22
lcm.height = TABLE_H - 0.12
leg.mesh = lcm
leg.position = Vector3(0, (TABLE_H - 0.12) * 0.5, 0)
leg.material_override = _mat(Color(0.16, 0.11, 0.07), 0.7)
add_child(leg)
func _build_seat(i: int) -> void:
var dir: Vector3 = seat_dir[i]
var zone := Node3D.new()
zone.position = dir * zone_r + Vector3(0, TABLE_H, 0)
zone.rotation.y = -atan2(dir.z, dir.x)
add_child(zone)
zones.append(zone)
# Runde Bande aus Segmenten - ein hohler Zylinder ist als Kollisionsform
# nicht vorgesehen. Nur Kollision, kein Mesh: der sichtbare Rand ist ein Torus.
var walls := StaticBody3D.new()
zone.add_child(walls)
var seg_w := TAU * ZONE_RAD / WALL_SEGS * 1.25 # Überlappung, damit keine Lücke bleibt
for k in WALL_SEGS:
var ang := TAU * k / WALL_SEGS
var c := CollisionShape3D.new()
var b := BoxShape3D.new()
b.size = Vector3(0.12, WALL_H, seg_w)
c.shape = b
c.position = Vector3(cos(ang), 0.0, sin(ang)) * (ZONE_RAD + 0.06) \
+ Vector3(0, WALL_H * 0.5, 0)
c.rotation.y = -ang
walls.add_child(c)
# Filzfläche
var felt := MeshInstance3D.new()
var fm := CylinderMesh.new()
fm.top_radius = ZONE_RAD
fm.bottom_radius = ZONE_RAD
fm.height = 0.012
fm.radial_segments = 32
felt.mesh = fm
felt.position = Vector3(0, 0.006, 0)
felt.material_override = _mat(Color(0.07, 0.20, 0.13), 0.95)
zone.add_child(felt)
# Sichtbarer Rand
var rim := MeshInstance3D.new()
var tm := TorusMesh.new()
tm.inner_radius = ZONE_RAD
tm.outer_radius = ZONE_RAD + 0.07
tm.rings = 32
tm.ring_segments = 8
rim.mesh = tm
rim.position = Vector3(0, 0.02, 0)
rim.material_override = _mat(RIM_RUHE, 0.6)
zone.add_child(rim)
rims.append(rim)
# Becher
var cup := Node3D.new()
cup.position = Vector3(0, 0.02, 0)
zone.add_child(cup)
var cup_mi := MeshInstance3D.new()
var cm := CylinderMesh.new()
cm.top_radius = 0.33
cm.bottom_radius = 0.42
cm.height = 0.44
cup_mi.mesh = cm
cup_mi.position = Vector3(0, 0.22, 0)
cup_mi.material_override = _mat(Color(0.35, 0.12, 0.10), 0.5)
cup.add_child(cup_mi)
cups.append(cup)
cup_tweens.append(null)
# Namensschild
var lbl := Label3D.new()
lbl.position = dir * zone_r + Vector3(0, TABLE_H + 0.52, 0) # oben auf dem Becher
lbl.billboard = BaseMaterial3D.BILLBOARD_ENABLED
# Keine feste Bildschirmgrösse: so schrumpft der Name mit dem Becher, wenn
# der Tisch bei mehr Spielern wächst und die Kamera zurückweicht.
lbl.font_size = 64
lbl.pixel_size = 0.0023
lbl.outline_size = 12
lbl.no_depth_test = true
lbl.text = ""
lbl.visible = i != local_seat # den eigenen Namen kennt man
add_child(lbl)
labels.append(lbl)
# Unterlage hinter Name und Ansage
_build_plate_texture()
var pl := Sprite3D.new()
pl.texture = _plate_tex
pl.billboard = BaseMaterial3D.BILLBOARD_ENABLED
pl.no_depth_test = true
pl.pixel_size = 1.0 / 128.0 # Grundgrösse 1 x 1 Welteinheit
pl.render_priority = -1 # immer hinter Schrift und Würfelbild
pl.visible = i != local_seat
add_child(pl)
plates.append(pl)
# Ansagezeile darunter: Anzahl als Text, Augenzahl als Würfelbild
_build_face_textures()
var unten := lbl.position + Vector3(0, -0.001, 0)
var cnt := Label3D.new()
cnt.billboard = BaseMaterial3D.BILLBOARD_ENABLED
cnt.font_size = 64
cnt.pixel_size = 0.0019
cnt.outline_size = 12
cnt.no_depth_test = true
cnt.modulate = Color(1.0, 0.88, 0.52)
cnt.visible = false
add_child(cnt)
bid_counts.append(cnt)
var ic := Sprite3D.new()
ic.billboard = BaseMaterial3D.BILLBOARD_ENABLED
ic.no_depth_test = true
ic.pixel_size = 0.0016
ic.texture = _face_tex[0]
ic.visible = false
add_child(ic)
bid_icons.append(ic)
_bid_anchor.append(unten)
## Fast senkrechte Aufsicht mit leichter Neigung, damit die Würfel noch
## plastisch wirken. Der eigene Platz liegt unten im Bild.
func _build_camera() -> void:
camera = Camera3D.new()
camera.fov = 58.0
camera.current = true
add_child(camera)
_frame_camera()
get_viewport().size_changed.connect(_frame_camera)
func _frame_camera() -> void:
var dir: Vector3 = seat_dir[local_seat]
var fov_v := deg_to_rad(camera.fov)
var tilt := deg_to_rad(TILT_DEG)
var vp := get_viewport().get_visible_rect().size
var aspect := 16.0 / 9.0 if vp.y <= 0.0 else vp.x / vp.y
# Der Tisch füllt das freie Band zwischen den HUD-Feldern aus. Geneigt
# betrachtet wird er zur Ellipse: senkrecht kürzer, waagerecht unverändert.
var band := 1.0 - HUD_TOP - HUD_BOTTOM
var half_v := tan(fov_v * 0.5)
var dist := (table_r * cos(tilt)) / (band * half_v)
# Waagerecht ist normalerweise reichlich Platz, in schmalen Fenstern nicht.
dist = maxf(dist, table_r / (0.98 * tan(fov_v * 0.5) * aspect))
# Das freie Band liegt nicht mittig, der Tisch rückt entsprechend nach oben.
var shift := (HUD_BOTTOM - HUD_TOP) * half_v * dist
var target := dir * shift + Vector3(0, TABLE_H, 0)
var pos := target + (Vector3.UP * cos(tilt) + dir * sin(tilt)) * dist
camera.look_at_from_position(pos, target, Vector3.UP)
_place_bid_lines()
## Setzt Name, Ansage und Unterlage eines Platzes. Alles wird entlang der
## Bildachsen ausgerichtet, sonst stünde bei der Aufsicht eines hinter dem
## anderen statt darunter.
func _layout_seat(i: int) -> void:
if camera == null or i >= plates.size():
return
var rechts: Vector3 = camera.global_transform.basis.x
var hoch: Vector3 = camera.global_transform.basis.y
var p: Vector3 = _bid_anchor[i]
var zeigt_ansage: bool = bid_icons[i].visible
var name_w := _label_width(labels[i])
labels[i].position = p + hoch * (0.10 if zeigt_ansage else 0.0)
var icon_w: float = 96.0 * bid_icons[i].pixel_size
var cnt_w := _label_width(bid_counts[i])
var zeile_w := cnt_w + 0.03 + icon_w
var mitte: Vector3 = p - hoch * 0.10
bid_counts[i].position = mitte - rechts * (zeile_w - cnt_w) * 0.5
bid_icons[i].position = mitte + rechts * (zeile_w - icon_w) * 0.5
var breite := maxf(name_w, zeile_w if zeigt_ansage else 0.0) + 0.14
var hoehe := 0.40 if zeigt_ansage else 0.22
plates[i].scale = Vector3(breite, hoehe, 1.0)
plates[i].position = p
plates[i].visible = labels[i].visible or zeigt_ansage
func _label_width(l: Label3D) -> float:
if l.text == "":
return 0.0
var f := ThemeDB.fallback_font
return f.get_string_size(l.text, HORIZONTAL_ALIGNMENT_LEFT, -1, l.font_size).x * l.pixel_size
func _place_bid_lines() -> void:
for i in plates.size():
_layout_seat(i)
## Würfelbilder für die Ansagen am Tisch. Einmal gezeichnet, von allen genutzt.
static var _face_tex: Array[ImageTexture] = []
static var _plate_tex: ImageTexture
## Fast deckende, abgerundete Unterlage. Halbtransparent ginge sie auf dem
## dunkelroten Becher unter.
static func _build_plate_texture() -> void:
if _plate_tex != null:
return
const S := 128
var img := Image.create(S, S, false, Image.FORMAT_RGBA8)
img.fill(Color(0, 0, 0, 0))
var r := S * 0.22
for y in S:
for x in S:
var dx := maxf(maxf(r - x, x - (S - r)), 0.0)
var dy := maxf(maxf(r - y, y - (S - r)), 0.0)
if Vector2(dx, dy).length() <= r:
img.set_pixel(x, y, Color(0.04, 0.05, 0.07, 0.90))
_plate_tex = ImageTexture.create_from_image(img)
static func _build_face_textures() -> void:
if not _face_tex.is_empty():
return
const S := 192 # doppelt gezeichnet und verkleinert = weiche Kanten
const OUT := 96
for v in range(1, 7):
var img := Image.create(S, S, false, Image.FORMAT_RGBA8)
img.fill(Color(0, 0, 0, 0))
var r := S * 0.18
for y in S:
for x in S:
var dx := maxf(maxf(r - x, x - (S - r)), 0.0)
var dy := maxf(maxf(r - y, y - (S - r)), 0.0)
if Vector2(dx, dy).length() <= r:
img.set_pixel(x, y, Color(0.95, 0.94, 0.90))
var mid := S * 0.5
var spread := S * 0.24
var pr := S * 0.105
for p: Vector2 in Die.PIPS[v]:
var cx := mid + p.x * spread
var cy := mid + p.y * spread
for y in S:
for x in S:
if Vector2(x + 0.5 - cx, y + 0.5 - cy).length() <= pr:
img.set_pixel(x, y, Color(0.09, 0.08, 0.1))
img.resize(OUT, OUT, Image.INTERPOLATE_LANCZOS)
_face_tex.append(ImageTexture.create_from_image(img))
## Zeigt unter dem Namen, was der Platz zuletzt angesagt hat (0 = nichts).
func set_bid(seat: int, count: int, face: int) -> void:
if seat >= bid_counts.size():
return
# Am eigenen Platz steht kein Name, dort läge die Zeile in den Würfeln.
var zeige := seat != local_seat and count > 0 and face >= 1 and face <= 6
bid_counts[seat].visible = zeige
bid_icons[seat].visible = zeige
if zeige:
bid_counts[seat].text = "%d ×" % count
bid_icons[seat].texture = _face_tex[face - 1]
_layout_seat(seat)
func _mat(col: Color, rough: float) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.albedo_color = col
m.roughness = rough
return m
## --- Würfel ---
## Nur die neu dazugekommenen Würfel werden hingelegt. Die schon liegenden
## bleiben, wie sie sind - ein Nachschub mitten in der Runde würde ihnen sonst
## die Drehung nehmen und damit auf dem Tisch die Augenzahl.
func set_dice_count(seat: int, count: int) -> void:
var arr: Array = dice[seat]
while arr.size() > count:
var d = arr.pop_back()
d.queue_free()
# Ein neuer Würfel darf nicht sichtbar sein, wenn die anderen unterm
# Becher liegen - sonst verrät er die Hand des Gegners.
var sichtbar: bool = arr.is_empty() or arr[0].visible
var ab := arr.size()
while arr.size() < count:
var d := Die.new()
add_child(d)
d.global_position = zones[seat].global_position + Vector3(0, 0.4, 0)
d.visible = sichtbar
arr.append(d)
if arr.size() > ab:
_layout_idle(seat, ab)
func dice_of(seat: int) -> Array:
return dice[seat]
func throw_dice(seat: int, rng: RandomNumberGenerator) -> void:
var zone: Node3D = zones[seat]
var arr: Array = dice[seat]
var n := arr.size()
for i in n:
# gleichmässig auf einem Kreis verteilt, damit nichts übereinander fällt
var ang := TAU * i / maxi(n, 1) + rng.randf_range(-0.15, 0.15)
var rad := ZONE_RAD * rng.randf_range(0.30, 0.46)
var off := Vector3(
cos(ang) * rad,
0.48 + rng.randf_range(0.0, 0.10),
sin(ang) * rad
)
arr[i].throw_from(zone.global_transform * off, rng)
## Legt die Würfel ordentlich nebeneinander (für Clients/Reveal). Ab `von`
## bleiben die davorliegenden unangetastet.
func _layout_idle(seat: int, von := 0) -> void:
var zone: Node3D = zones[seat]
var arr: Array = dice[seat]
# Bogen auf einem Kreis um die Zonenmitte - passt zur runden Bande.
# Der Schrittwinkel folgt aus dem gewünschten Abstand: Diagonale eines
# Würfels plus Luft, sonst überlappen die Bilder beim Aufdecken.
const BOGEN_R := 0.48 # etwas weiter aussen, damit das Namensschild frei bleibt
var schritt: float = (Die.SIZE * 1.7) / BOGEN_R
for i in range(von, arr.size()):
var ang := PI + (float(i) - (arr.size() - 1) * 0.5) * schritt
var t := zone.global_transform
t.origin = zone.global_transform * Vector3(
cos(ang) * BOGEN_R, Die.HALF + 0.01, sin(ang) * BOGEN_R)
arr[i].set_kinematic(true)
arr[i].place(t)
func arrange_and_show(seat: int, values: Array, rng: RandomNumberGenerator) -> void:
_layout_idle(seat)
var arr: Array = dice[seat]
for i in mini(arr.size(), values.size()):
arr[i].snap_to_value(values[i], rng)
set_dice_visible(seat, true)
## Friert alle Würfel ein, damit gesetzte Werte liegen bleiben.
func freeze_dice(seat: int) -> void:
for d in dice[seat]:
d.set_kinematic(true)
## Der Topf liegt als Häufchen Messing in der Tischmitte. Jede Münze trägt
## ihre Wurfnummer; was nicht mehr in der Liste steht, ist beim Gewinner -
## dorthin fliegt sie dann auch (`ziel`), sonst verschwindet sie einfach.
## Lage und Drehung kommen aus der Nummer, damit Host und Clients dasselbe
## Häufchen sehen. Wappen-Münzen tragen einen geprägten Ring und leuchten
## stärker: so sieht man den Wurfausgang auf dem Tisch, nicht nur im Text.
func set_topf(liste: Array, ziel := -1) -> void:
var bleibt := {}
for m in liste:
var nr := int(m.get("nr", 0))
bleibt[nr] = true
if topf_muenzen.has(nr):
continue
var r := RandomNumberGenerator.new()
r.seed = nr * 7919
var ang := r.randf() * TAU
var rad := r.randf_range(0.30, 0.55)
var wappen: bool = m.get("stark", false)
var lage := Vector3(cos(ang) * rad, TABLE_H + 0.007, sin(ang) * rad)
var mi := MeshInstance3D.new()
mi.mesh = _muenz_form()
mi.material_override = _muenz_stoff(wappen)
mi.position = lage + Vector3(0, 0.45, 0)
mi.rotation.y = r.randf() * TAU
add_child(mi)
if wappen:
var ring := MeshInstance3D.new()
ring.mesh = _praegung()
ring.material_override = _praegung_stoff()
ring.position = Vector3(0, 0.008, 0)
mi.add_child(ring)
topf_muenzen[nr] = mi
# Fällt drehend aus der Hand und bleibt flach liegen.
var tw := create_tween().set_parallel(true)
tw.tween_property(mi, "position", lage, 0.55).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN)
tw.tween_property(mi, "rotation:x", TAU * 3.0, 0.55).from(0.0)
for nr in topf_muenzen.keys():
if bleibt.has(nr):
continue
var mi: MeshInstance3D = topf_muenzen[nr]
topf_muenzen.erase(nr)
if ziel < 0 or ziel >= zones.size():
mi.queue_free()
continue
# Zum Gewinner fliegen, dann weg.
var hin: Vector3 = zones[ziel].global_position + Vector3(0, 0.25, 0)
var tw := create_tween()
tw.tween_property(mi, "global_position", hin, 0.6).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN_OUT)
tw.tween_callback(mi.queue_free)
static var _muenz_mat_wappen: StandardMaterial3D
static var _praeg_mesh: TorusMesh
static var _praeg_mat: StandardMaterial3D
static func _muenz_form() -> CylinderMesh:
if _muenz_mesh == null:
_muenz_mesh = CylinderMesh.new()
# Aus Kamerahöhe muss eine Münze fast so gross sein wie ein Würfel,
# sonst ist sie ein Punkt.
_muenz_mesh.top_radius = 0.11
_muenz_mesh.bottom_radius = 0.11
_muenz_mesh.height = 0.014
_muenz_mesh.radial_segments = 24
return _muenz_mesh
static func _muenz_stoff(wappen: bool) -> StandardMaterial3D:
if _muenz_mat == null:
_muenz_mat = StandardMaterial3D.new()
_muenz_mat.albedo_color = Color(0.80, 0.62, 0.26)
_muenz_mat.metallic = 0.85
_muenz_mat.roughness = 0.35
_muenz_mat.emission_enabled = true
_muenz_mat.emission = Color(0.55, 0.40, 0.12)
_muenz_mat.emission_energy_multiplier = 0.35
_muenz_mat_wappen = _muenz_mat.duplicate()
_muenz_mat_wappen.emission_energy_multiplier = 0.9
return _muenz_mat_wappen if wappen else _muenz_mat
static func _praegung() -> TorusMesh:
if _praeg_mesh == null:
_praeg_mesh = TorusMesh.new()
_praeg_mesh.inner_radius = 0.035
_praeg_mesh.outer_radius = 0.065
_praeg_mesh.rings = 20
_praeg_mesh.ring_segments = 6
return _praeg_mesh
static func _praegung_stoff() -> StandardMaterial3D:
if _praeg_mat == null:
_praeg_mat = StandardMaterial3D.new()
_praeg_mat.albedo_color = Color(0.20, 0.14, 0.05)
_praeg_mat.roughness = 0.8
return _praeg_mat
## Beim Zählen leuchten die passenden Würfel nacheinander auf, mit einem
## Tick je Würfel. So sieht man, woher die Zahl im Banner kommt.
func zaehlen_zeigen(face: int, werte: Array) -> void:
var k := 0
for seat in mini(werte.size(), dice.size()):
var dl: Array = dice[seat]
for i in mini(dl.size(), werte[seat].size()):
if int(werte[seat][i]) != face:
continue
var d = dl[i]
get_tree().create_timer(0.12 * k).timeout.connect(func() -> void:
if is_instance_valid(d):
d.hervorheben(true)
Klang.tick())
k += 1
func hervorhebung_weg() -> void:
for dl in dice:
for d in dl:
d.hervorheben(false)
## Der Platz, der dran ist, bekommt einen leuchtenden Rand. In der dunklen
## Szene ist das von jeder Ecke aus zu sehen - ein farbiger Name allein nicht.
func set_active_seat(seat: int) -> void:
for i in rims.size():
var m: StandardMaterial3D = rims[i].material_override
var dran: bool = i == seat
m.albedo_color = RIM_ZUG if dran else RIM_RUHE
m.emission_enabled = dran
if dran:
m.emission = Color(1.0, 0.78, 0.30)
m.emission_energy_multiplier = 1.1
func set_dice_visible(seat: int, on: bool) -> void:
for d in dice[seat]:
d.visible = on
## Becher senken (Würfel verdeckt) oder anheben und ausblenden (Würfel frei).
func set_cup_down(seat: int, down: bool) -> void:
var cup: Node3D = cups[seat]
if cup_tweens[seat] != null and cup_tweens[seat].is_valid():
cup_tweens[seat].kill()
var tw := create_tween()
cup_tweens[seat] = tw
if down:
cup.visible = true
tw.tween_property(cup, "position:y", 0.02, 0.35).set_trans(Tween.TRANS_CUBIC)
else:
tw.tween_property(cup, "position:y", 1.1, 0.35).set_trans(Tween.TRANS_CUBIC)
tw.tween_callback(func() -> void: cup.visible = false)
func set_label(seat: int, text: String, col: Color) -> void:
labels[seat].text = text
labels[seat].modulate = col
_layout_seat(seat)