wayfarers_wanderer/terrain.gd
Jonas Reith eac8fdf042 Replace guesswork border spacing with a real overlap check
_build_ridge() now picks each prop's real-world collision radius
(shape radius * scale, same values as elsewhere in main.tscn) and
places it so its hitbox is guaranteed to overlap the previous one's
(border_overlap=0.8 of the combined radii), correcting the step
iteratively since the facet is sloped so distance isn't purely
radial. Lateral jitter shrunk to 0.05 so it can't itself exceed the
smallest possible combined radius (two forest props) and break the
guarantee.

Verified headless by measuring actual 3D distance between every
consecutive pair of props on one ridge against their combined radii:
83/83 pairs genuinely overlap, worst case still -0.13 units of
overlap (no gap anywhere). 504 border props total across all 6
ridges (was 142 with the old fixed-spacing approach).
2026-08-26 20:40:03 +02:00

214 lines
8.4 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()
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 der 6 Grate zwischen den Regionen
# Grat k = Strecke von Tisch-Ecke T[k] zu Rundiste-Ecke G[k]
# (gemeinsame Kante von Facette k-1 und Facette k)
# ═══════════════════════════════════════════════
func _build_region_borders() -> void:
var borders := Node3D.new()
borders.name = "Borders"
add_child(borders)
for k in range(6):
var a := _hex_angle(k)
var dir := Vector3(cos(a), 0.0, sin(a))
var perp := Vector3(-dir.z, 0.0, dir.x)
_build_ridge(borders, dir, perp)
# Kettet Props entlang eines Grats: 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.
func _build_ridge(borders: Node3D, dir: Vector3, perp: Vector3) -> void:
var t := table_radius
var prev_pos := Vector3.ZERO
var prev_radius := 0.0
var first := true
while t <= girdle_radius:
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 := _ridge_point(dir, perp, t, lateral)
if not first:
var min_dist := (prev_radius + radius) * border_overlap
# Iterativ korrigieren statt blind zu vertrauen: die Facette ist geneigt,
# daher ist die Distanz nicht rein radial (Höhe hängt am Abstand mit dran).
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 = _ridge_point(dir, perp, t, 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 _ridge_point(dir: Vector3, perp: Vector3, t: float, lateral: float) -> Vector3:
var frac: float = clamp((t - table_radius) / (girdle_radius - table_radius), 0.0, 1.0)
var pos := dir * t + perp * lateral
pos.y = lerp(0.0, -crown_drop, frac)
return pos
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)