This commit is contained in:
Team3
2026-09-07 04:29:23 +02:00
parent e299f9d088
commit 6478559252
8 changed files with 708 additions and 197 deletions

View File

@@ -3,14 +3,14 @@ class_name GameTable
## Baut den 3D-Tisch mit Würfelzonen, Bechern, Licht und Kamera.
const TABLE_H := 0.95
const ZONE_W := 1.5 ## Breite der Würfelzone (tangential)
const ZONE_D := 0.95 ## Tiefe der Würfelzone (radial)
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_W + 0.85 ## Bogenlänge, die ein Platz am Tischrand braucht
const SEAT_ARC := ZONE_RAD * 2.0 + 0.85 ## Bogenlänge, die ein Platz am Tischrand braucht
const MIN_ZONE_R := 1.15
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.07 ## Bildanteil unten als Rand
const HUD_BOTTOM := 0.17 ## Bildanteil unten für die Ansage-Leiste
## Tisch und Zonenradius wachsen mit der Spielerzahl.
var table_r := 2.0
@@ -23,6 +23,10 @@ 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 dice: Array = [] ## seat -> Array[Die]
var camera: Camera3D
@@ -32,7 +36,7 @@ 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_D * 0.5 + 0.45
table_r = zone_r + ZONE_RAD + 0.45
_build_room()
seat_dir.clear()
for i in count:
@@ -131,40 +135,46 @@ func _build_seat(i: int) -> void:
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)
# lokale x = radial (Tiefe), lokale z = tangential (Breite)
var specs := [
[Vector3(ZONE_D * 0.5, WALL_H * 0.5, 0), Vector3(0.12, WALL_H, ZONE_W)],
[Vector3(-ZONE_D * 0.5, WALL_H * 0.5, 0), Vector3(0.12, WALL_H, ZONE_W)],
[Vector3(0, WALL_H * 0.5, ZONE_W * 0.5), Vector3(ZONE_D, WALL_H, 0.12)],
[Vector3(0, WALL_H * 0.5, -ZONE_W * 0.5), Vector3(ZONE_D, WALL_H, 0.12)],
]
var rim_mat := _mat(Color(0.22, 0.15, 0.09), 0.6)
for s in specs:
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 = s[1]
b.size = Vector3(0.12, WALL_H, seg_w)
c.shape = b
c.position = s[0]
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)
var m := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = Vector3(s[1].x, 0.05, s[1].z)
m.mesh = bm
m.position = Vector3(s[0].x, 0.025, s[0].z)
m.material_override = rim_mat
walls.add_child(m)
# Filzfläche
var felt := MeshInstance3D.new()
var fm := BoxMesh.new()
fm.size = Vector3(ZONE_D, 0.01, ZONE_W)
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(Color(0.22, 0.15, 0.09), 0.6)
zone.add_child(rim)
# Becher
var cup := Node3D.new()
cup.position = Vector3(0, 0.02, 0)
@@ -196,6 +206,42 @@ func _build_seat(i: int) -> void:
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.
@@ -229,6 +275,113 @@ func _frame_camera() -> void:
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:
@@ -262,12 +415,13 @@ func throw_dice(seat: int, rng: RandomNumberGenerator) -> void:
var arr: Array = dice[seat]
var n := arr.size()
for i in n:
# eigene Spur je Würfel, damit sie nicht übereinander fallen
var lane := ((float(i) + 0.5) / n * 2.0 - 1.0) * ZONE_W * 0.34
# 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(
rng.randf_range(-ZONE_D * 0.14, ZONE_D * 0.14),
cos(ang) * rad,
0.48 + rng.randf_range(0.0, 0.10),
lane + rng.randf_range(-0.03, 0.03)
sin(ang) * rad
)
arr[i].throw_from(zone.global_transform * off, rng)
@@ -276,12 +430,18 @@ func throw_dice(seat: int, rng: RandomNumberGenerator) -> void:
func _layout_idle(seat: int) -> 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 arr.size():
var x := (float(i) - (arr.size() - 1) * 0.5) * 0.24
var ang := PI + (float(i) - (arr.size() - 1) * 0.5) * schritt
var t := zone.global_transform
t.origin = zone.global_transform * Vector3(0.0, Die.HALF + 0.01, x)
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].global_transform = t
arr[i].place(t)
func arrange_and_show(seat: int, values: Array, rng: RandomNumberGenerator) -> void:
@@ -321,3 +481,4 @@ func set_cup_down(seat: int, down: bool) -> void:
func set_label(seat: int, text: String, col: Color) -> void:
labels[seat].text = text
labels[seat].modulate = col
_layout_seat(seat)