update
This commit is contained in:
225
scripts/table.gd
225
scripts/table.gd
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user