_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).
214 lines
8.4 KiB
GDScript
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)
|