extends Node3D # ═══════════════════════════════════════════════════════════════ # HEX-DIAMANT-INSEL — Blockout # Krone (begehbar): flaches 6-Eck-Tisch + 6 Trapez-Facetten zur Rundiste # Pavillon (rein optisch): steile 6-seitige Pyramide unter der Rundiste # ═══════════════════════════════════════════════════════════════ @export var table_radius : float = 15.0 # flaches Sechseck in der Mitte @export var girdle_radius : float = 50.0 # äußerer Sechseck-Rand (Rundiste) @export var crown_drop : float = 6.0 # Höhenabfall Tisch -> Rundiste @export var pavilion_depth : float = 35.0 # Höhenabfall Rundiste -> Spitze @export var angle_offset_deg: float = 0.0 # Insel-Maße richten sich nach der Spieler-/Prop-Größe (Capsule ~0.16-0.5, # Berg/Wald-Modelle bei 0.1-0.2 Skalierung ~2 Einheiten hoch) — nicht umgekehrt. @export var border_jitter : float = 0.05 # zufälliger seitlicher Versatz (klein: darf die Überlappungs-Garantie nicht brechen) @export var border_overlap : float = 0.8 # <1.0 = Nachbar-Hitboxen überlappen sich garantiert statt sich nur zu berühren const COLOR_TABLE := Color(0.75, 0.74, 0.70) const COLOR_FACET_A := Color(0.55, 0.55, 0.48) const COLOR_FACET_B := Color(0.49, 0.49, 0.43) const COLOR_PAVILION := Color(0.30, 0.28, 0.26) # Grenz-Props: gleiche Modelle + Skalierung + Hitbox-Maße wie in world/island.tscn. # "radius"/"height" sind die Shape-Maße VOR der Skalierung (wie im Original); # der reale Welt-Radius (für die Überlappungsprüfung) ist radius * scale. const BORDER_KINDS := [ { "scene": preload("res://assets/models/mountain.glb"), "scale": 0.1, "shape": "capsule", "radius": 7.0, "height": 20.0, }, { "scene": preload("res://assets/models/mountain2v5.glb"), "scale": 0.05, "shape": "capsule", "radius": 8.5078125, "height": 17.015625, }, { "scene": preload("res://assets/models/forest.glb"), "scale": 0.2, "shape": "cylinder", "radius": 0.72021484, "height": 2.2817383, }, ] func _ready() -> void: _build_crown() _build_pavilion() _build_region_borders() _build_edge_barrier() func _hex_angle(k: int) -> float: return deg_to_rad(angle_offset_deg) + float(k) / 6.0 * TAU func _add_tri(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, col: Color) -> void: var n := (b - a).cross(c - a).normalized() for v in [a, b, c]: st.set_normal(n) st.set_color(col) st.add_vertex(v) # ═══════════════════════════════════════════════ # KRONE — Tisch + 6 Facetten (begehbar, mit Kollision) # ═══════════════════════════════════════════════ func _build_crown() -> void: var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) var center := Vector3.ZERO var table_pts : Array[Vector3] = [] var girdle_pts : Array[Vector3] = [] for k in range(6): var a := _hex_angle(k) table_pts.append(Vector3(cos(a) * table_radius, 0.0, sin(a) * table_radius)) girdle_pts.append(Vector3(cos(a) * girdle_radius, -crown_drop, sin(a) * girdle_radius)) # Tisch-Fan (Zentrum-Region) for k in range(6): var k1 := (k + 1) % 6 _add_tri(st, center, table_pts[k1], table_pts[k], COLOR_TABLE) # 6 Facetten (Außenregionen), abwechselnd eingefärbt für sichtbare Nähte for k in range(6): var k1 := (k + 1) % 6 var col := COLOR_FACET_A if k % 2 == 0 else COLOR_FACET_B _add_tri(st, table_pts[k], table_pts[k1], girdle_pts[k], col) _add_tri(st, table_pts[k1], girdle_pts[k1], girdle_pts[k], col) var mesh := st.commit() var mat := StandardMaterial3D.new() mat.vertex_color_use_as_albedo = true mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.cull_mode = BaseMaterial3D.CULL_DISABLED mesh.surface_set_material(0, mat) var body := StaticBody3D.new() body.name = "Crown" var mi := MeshInstance3D.new() mi.mesh = mesh body.add_child(mi) var cs := CollisionShape3D.new() var trimesh := mesh.create_trimesh_shape() trimesh.backface_collision = true # dünnes Mesh: von beiden Seiten kollidieren, kein Durchfallen cs.shape = trimesh body.add_child(cs) add_child(body) # ═══════════════════════════════════════════════ # PAVILLON — Pyramide unter der Rundiste (rein optisch) # ═══════════════════════════════════════════════ func _build_pavilion() -> void: var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) var apex := Vector3(0, -crown_drop - pavilion_depth, 0) var girdle_pts: Array[Vector3] = [] for k in range(6): var a := _hex_angle(k) girdle_pts.append(Vector3(cos(a) * girdle_radius, -crown_drop, sin(a) * girdle_radius)) for k in range(6): var k1 := (k + 1) % 6 _add_tri(st, girdle_pts[k], girdle_pts[k1], apex, COLOR_PAVILION) var mesh := st.commit() var mat := StandardMaterial3D.new() mat.vertex_color_use_as_albedo = true mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED mat.cull_mode = BaseMaterial3D.CULL_DISABLED mesh.surface_set_material(0, mat) var mi := MeshInstance3D.new() mi.name = "Pavilion" mi.mesh = mesh add_child(mi) # ═══════════════════════════════════════════════ # GRENZEN — Berg/Wald entlang aller Kanten zwischen den 7 Regionen: # 6 Grate Tisch-Ecke T[k] -> Rundiste-Ecke G[k] (zwischen den Außenregionen) # + 6 Tisch-Kanten T[k] -> T[k+1] (zwischen Zentrum und jeder Außenregion) # ═══════════════════════════════════════════════ func _build_region_borders() -> void: var borders := Node3D.new() borders.name = "Borders" add_child(borders) var table_pts : Array[Vector3] = [] var girdle_pts : Array[Vector3] = [] for k in range(6): var a := _hex_angle(k) table_pts.append(Vector3(cos(a) * table_radius, 0.0, sin(a) * table_radius)) girdle_pts.append(Vector3(cos(a) * girdle_radius, -crown_drop, sin(a) * girdle_radius)) for k in range(6): var k1 := (k + 1) % 6 _build_border_segment(borders, table_pts[k], girdle_pts[k]) # Grat: Außenregion <-> Außenregion _build_border_segment(borders, table_pts[k], table_pts[k1]) # Tisch-Kante: Zentrum <-> Außenregion # Kettet Props entlang einer geraden Strecke p0->p1: jeder neue Prop wird so nah # an den vorigen gesetzt, dass sich ihre Welt-Radien garantiert überlappen # (Distanz zwischen den Mittelpunkten <= (r1+r2) * border_overlap). Kein # Rätselraten über feste Abstände mehr — die Lücke wird pro Prop-Paar # explizit geprüft und korrigiert. Funktioniert für geneigte (Grat) wie # flache (Tisch-Kante) Strecken gleichermaßen, da einfach entlang der echten # 3D-Linie zwischen den beiden Punkten gelaufen wird. func _build_border_segment(borders: Node3D, p0: Vector3, p1: Vector3) -> void: var length := p0.distance_to(p1) if length < 0.001: return var dir := (p1 - p0) / length var perp := Vector3(-dir.z, 0.0, dir.x) var t := 0.0 var prev_pos := Vector3.ZERO var prev_radius := 0.0 var first := true while t <= length: var kind: Dictionary = BORDER_KINDS[randi() % BORDER_KINDS.size()] var radius: float = kind["radius"] * kind["scale"] var lateral := randf_range(-border_jitter, border_jitter) var pos := p0 + dir * t + perp * lateral if not first: var min_dist := (prev_radius + radius) * border_overlap # Iterativ korrigieren statt blind zu vertrauen (seitlicher Jitter # verschiebt den Punkt leicht von der reinen Geraden weg). for _i in range(4): var actual_dist := pos.distance_to(prev_pos) if actual_dist <= min_dist: break t -= (actual_dist - min_dist) pos = p0 + dir * t + perp * lateral _spawn_border_prop(borders, kind, pos) prev_pos = pos prev_radius = radius first = false t += radius # nächster Kandidat startet grob hinter diesem Prop; obige Prüfung zieht ihn bei Bedarf näher func _spawn_border_prop(parent: Node3D, kind: Dictionary, pos: Vector3) -> void: var inst := (kind["scene"] as PackedScene).instantiate() as Node3D inst.position = pos inst.rotation.y = randf() * TAU inst.scale = Vector3.ONE * float(kind["scale"]) var shape: Shape3D if kind["shape"] == "capsule": var cap := CapsuleShape3D.new() cap.radius = kind["radius"] cap.height = kind["height"] shape = cap else: var cyl := CylinderShape3D.new() cyl.radius = kind["radius"] cyl.height = kind["height"] shape = cyl var cs := CollisionShape3D.new() cs.shape = shape inst.add_child(cs) parent.add_child(inst) # ═══════════════════════════════════════════════ # RAND-BARRIERE — unsichtbare Wand entlang der Rundiste, damit man # an der Außenkante der Insel nicht in den Pavillon (rein optisch, # ohne Kollision) hinunterfällt. # ═══════════════════════════════════════════════ func _build_edge_barrier() -> void: var barrier := StaticBody3D.new() barrier.name = "EdgeBarrier" add_child(barrier) var girdle_pts: Array[Vector3] = [] for k in range(6): var a := _hex_angle(k) girdle_pts.append(Vector3(cos(a) * girdle_radius, -crown_drop, sin(a) * girdle_radius)) for k in range(6): var k1 := (k + 1) % 6 var p0 := girdle_pts[k] var p1 := girdle_pts[k1] var length := p0.distance_to(p1) var dir := (p1 - p0) / length var mid := (p0 + p1) * 0.5 var box := BoxShape3D.new() box.size = Vector3(1.0, 6.0, length) var cs := CollisionShape3D.new() cs.shape = box cs.position = mid + Vector3(0.0, 1.0, 0.0) cs.rotation.y = atan2(dir.x, dir.z) barrier.add_child(cs)