Files
pirate-dice/scripts/table.gd
2026-09-05 23:19:46 +02:00

324 lines
9.7 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_W := 1.5 ## Breite der Würfelzone (tangential)
const ZONE_D := 0.95 ## Tiefe der Würfelzone (radial)
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 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
## 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 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_D * 0.5 + 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)
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 c := CollisionShape3D.new()
var b := BoxShape3D.new()
b.size = s[1]
c.shape = b
c.position = s[0]
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)
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)
# 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)
## 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)
func _mat(col: Color, rough: float) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.albedo_color = col
m.roughness = rough
return m
## --- Würfel ---
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()
while arr.size() < count:
var d := Die.new()
add_child(d)
d.global_position = zones[seat].global_position + Vector3(0, 0.4, 0)
arr.append(d)
_layout_idle(seat)
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:
# 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
var off := Vector3(
rng.randf_range(-ZONE_D * 0.14, ZONE_D * 0.14),
0.48 + rng.randf_range(0.0, 0.10),
lane + rng.randf_range(-0.03, 0.03)
)
arr[i].throw_from(zone.global_transform * off, rng)
## Legt die Würfel ordentlich nebeneinander (für Clients/Reveal).
func _layout_idle(seat: int) -> void:
var zone: Node3D = zones[seat]
var arr: Array = dice[seat]
for i in arr.size():
var x := (float(i) - (arr.size() - 1) * 0.5) * 0.24
var t := zone.global_transform
t.origin = zone.global_transform * Vector3(0.0, Die.HALF + 0.01, x)
arr[i].set_kinematic(true)
arr[i].global_transform = 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)
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