324 lines
9.7 KiB
GDScript
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
|