We only ever built the inner region-to-region borders, never a wall at the island's own outer rim (girdle) — nothing stopped the player walking off into the (collision-less) pavilion below. Added _build_edge_barrier(): 6 invisible BoxShape3D walls along the girdle chords. Also found the actual bug behind "one POI is outside": a facet's true outer boundary is a straight hexagon chord, not an arc, so its usable radius varies with angle — girdle_radius only at the two corners, dropping to girdle_radius*cos(30°) (~87%) at the facet's own bisector. Ort6 was placed at exactly the bisector angle with radius 44, just past the true edge (~43.3 there) despite being "inside" the naive [table_radius, girdle_radius] range. Moved it to angle 38°/radius 36, comfortable inside the true boundary at that angle. Verified headless: pushing the player outward with constant velocity for 3 simulated seconds pins it at radius 42.64 (never crosses the ~43.3 true edge), settled and on_floor=true throughout. Re-verified the POI/gate group logic still passes after moving Ort6.
256 lines
10 KiB
GDScript
256 lines
10 KiB
GDScript
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 main.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)
|