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).
This commit is contained in:
Jonas Reith 2026-08-26 20:40:03 +02:00
parent 2ab62278c6
commit eac8fdf042

View File

@ -14,22 +14,34 @@ extends Node3D
# Insel-Maße richten sich nach der Spieler-/Prop-Größe (Capsule ~0.16-0.5, # 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. # Berg/Wald-Modelle bei 0.1-0.2 Skalierung ~2 Einheiten hoch) — nicht umgekehrt.
@export var border_spacing : float = 1.5 # Abstand zwischen Grenz-Props entlang eines Grats @export var border_jitter : float = 0.05 # zufälliger seitlicher Versatz (klein: darf die Überlappungs-Garantie nicht brechen)
@export var border_jitter : float = 0.5 # zufälliger seitlicher Versatz @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_TABLE := Color(0.75, 0.74, 0.70)
const COLOR_FACET_A := Color(0.55, 0.55, 0.48) const COLOR_FACET_A := Color(0.55, 0.55, 0.48)
const COLOR_FACET_B := Color(0.49, 0.49, 0.43) const COLOR_FACET_B := Color(0.49, 0.49, 0.43)
const COLOR_PAVILION := Color(0.30, 0.28, 0.26) const COLOR_PAVILION := Color(0.30, 0.28, 0.26)
# Grenz-Props: gleiche Modelle + Skalierung + Hitbox-Maße wie in main.tscn # Grenz-Props: gleiche Modelle + Skalierung + Hitbox-Maße wie in main.tscn.
const MOUNTAIN_SCENE := preload("res://assets/models/mountain.glb") # "radius"/"height" sind die Shape-Maße VOR der Skalierung (wie im Original);
const MOUNTAIN2_SCENE := preload("res://assets/models/mountain2v5.glb") # der reale Welt-Radius (für die Überlappungsprüfung) ist radius * scale.
const FOREST_SCENE := preload("res://assets/models/forest.glb") const BORDER_KINDS := [
{
const MOUNTAIN_SCALE := 0.1 "scene": preload("res://assets/models/mountain.glb"),
const MOUNTAIN2_SCALE := 0.05 "scale": 0.1,
const FOREST_SCALE := 0.2 "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: func _ready() -> void:
_build_crown() _build_crown()
@ -135,48 +147,64 @@ func _build_region_borders() -> void:
var a := _hex_angle(k) var a := _hex_angle(k)
var dir := Vector3(cos(a), 0.0, sin(a)) var dir := Vector3(cos(a), 0.0, sin(a))
var perp := Vector3(-dir.z, 0.0, dir.x) 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 t := table_radius
var prev_pos := Vector3.ZERO
var prev_radius := 0.0
var first := true
while t <= girdle_radius: while t <= girdle_radius:
var frac := (t - table_radius) / (girdle_radius - table_radius) var kind: Dictionary = BORDER_KINDS[randi() % BORDER_KINDS.size()]
var pos := dir * t 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) pos.y = lerp(0.0, -crown_drop, frac)
pos += perp * randf_range(-border_jitter, border_jitter) return pos
_place_border_prop(borders, pos)
t += border_spacing + randf_range(-border_spacing * 0.25, border_spacing * 0.25)
func _place_border_prop(parent: Node3D, pos: Vector3) -> void:
var roll := randf()
var inst: Node3D
var scale_factor: float
var shape: Shape3D
if roll < 0.4:
inst = MOUNTAIN_SCENE.instantiate() as Node3D
scale_factor = MOUNTAIN_SCALE
var cap := CapsuleShape3D.new()
cap.radius = 7.0
cap.height = 20.0
shape = cap
elif roll < 0.7:
inst = MOUNTAIN2_SCENE.instantiate() as Node3D
scale_factor = MOUNTAIN2_SCALE
var cap2 := CapsuleShape3D.new()
cap2.radius = 8.5078125
cap2.height = 17.015625
shape = cap2
else:
inst = FOREST_SCENE.instantiate() as Node3D
scale_factor = FOREST_SCALE
var cyl := CylinderShape3D.new()
cyl.radius = 0.72021484
cyl.height = 2.2817383
shape = cyl
func _spawn_border_prop(parent: Node3D, kind: Dictionary, pos: Vector3) -> void:
var inst := (kind["scene"] as PackedScene).instantiate() as Node3D
inst.position = pos inst.position = pos
inst.rotation.y = randf() * TAU inst.rotation.y = randf() * TAU
inst.scale = Vector3.ONE * scale_factor 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() var cs := CollisionShape3D.new()
cs.shape = shape cs.shape = shape