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