661 lines
21 KiB
GDScript
661 lines
21 KiB
GDScript
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 Stoffe = preload("res://scripts/stoffe.gd") ## Materialien aus Regeln, ohne Dateien
|
||
const Formen = preload("res://scripts/formen.gd") ## Becher, Fass, Laterne aus Primitiven
|
||
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([])
|
||
_deko()
|
||
_build_camera()
|
||
|
||
|
||
## Eine Laterne am Tischrand - weit genug draussen, dass sie keine Zone
|
||
## verdeckt, nah genug, dass ihr Schein den Filz erreicht. Das Fass aus den
|
||
## Formregeln bleibt weg: vom Boden aus ist es aus Kamerahöhe nur ein Fleck.
|
||
func _deko() -> void:
|
||
var laterne := Formen.laterne()
|
||
laterne.position = Vector3(-(table_r + 0.35), TABLE_H, table_r * 0.55)
|
||
add_child(laterne)
|
||
|
||
|
||
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.glow_enabled = true
|
||
e.glow_intensity = 0.45
|
||
e.glow_bloom = 0.05
|
||
e.glow_hdr_threshold = 0.9
|
||
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)
|
||
|
||
# Kein sichtbarer Lampenschirm: die Kamera schaut fast senkrecht von oben,
|
||
# alles über der Tischmitte hinge ihr vor der Linse.
|
||
|
||
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
|
||
# Bewusst glatt und dunkel: eine Maserung auf der riesigen Fläche wird aus
|
||
# Kamerahöhe zu Rauschen, und der Nebel soll den Boden ohnehin schlucken.
|
||
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 = Stoffe.filz()
|
||
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 = Stoffe.holz()
|
||
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 = Stoffe.filz()
|
||
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 = Stoffe.rand()
|
||
zone.add_child(rim)
|
||
rims.append(rim)
|
||
|
||
# Becher
|
||
var cup := Node3D.new()
|
||
cup.position = Vector3(0, 0.02, 0)
|
||
zone.add_child(cup)
|
||
cup.add_child(Formen.becher())
|
||
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)
|
||
|
||
|
||
## Fangnetz: ein Würfel, der über die Bande oder durch den Filz gefallen ist,
|
||
## fiele sonst ewig - und das Würfeln käme nie zur Ruhe. Er kommt zurück in
|
||
## seine Zone und fällt dort noch einmal. Gibt zurück, ob einer gefangen wurde.
|
||
func fange_verirrte(seat: int) -> bool:
|
||
var zone: Node3D = zones[seat]
|
||
var gefangen := false
|
||
for d in dice[seat]:
|
||
if d.global_position.y > TABLE_H - 0.3 and d.global_position.y < TABLE_H + 3.0:
|
||
continue
|
||
var ang := randf() * TAU
|
||
var off := Vector3(cos(ang), 0.0, sin(ang)) * ZONE_RAD * 0.3 + Vector3(0, 0.35, 0)
|
||
d.freeze = false
|
||
d.sleeping = false
|
||
d.linear_velocity = Vector3.ZERO
|
||
d.angular_velocity = Vector3.ZERO
|
||
var t := Transform3D(Basis(), zone.global_transform * off)
|
||
d.place(t)
|
||
gefangen = true
|
||
return gefangen
|
||
|
||
|
||
## 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 = Stoffe.messing(0.25)
|
||
_muenz_mat_wappen = Stoffe.messing(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
|
||
m.emission_enabled = i == seat
|
||
m.emission_energy_multiplier = 1.2
|
||
|
||
|
||
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)
|