wayfarers_wanderer/terrain.gd
Jonas Reith 2ab62278c6 Fix border prop spacing (was leaving massive gaps)
The random step jitter in _build_region_borders() was still ±4.0, a
constant left over from before the island rescale — at the new
border_spacing of 3.5 that could produce near-zero or even negative
steps as often as it produced 7.5-unit gaps, independent of position
along the ridge (hence gaps showing up near the center too, just by
bad luck). Jitter is now proportional to border_spacing (±25%), and
spacing itself tightened to 1.5 for a denser wall.

Verified headless: 142 border props total, max gap along one ridge
now ~1.9 units (was up to 7.5), consistent from table edge to girdle.
2026-08-26 20:11:22 +02:00

186 lines
7.0 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_spacing : float = 1.5 # Abstand zwischen Grenz-Props entlang eines Grats
@export var border_jitter : float = 0.5 # zufälliger seitlicher Versatz
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
const MOUNTAIN_SCENE := preload("res://assets/models/mountain.glb")
const MOUNTAIN2_SCENE := preload("res://assets/models/mountain2v5.glb")
const FOREST_SCENE := preload("res://assets/models/forest.glb")
const MOUNTAIN_SCALE := 0.1
const MOUNTAIN2_SCALE := 0.05
const FOREST_SCALE := 0.2
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)
var t := table_radius
while t <= girdle_radius:
var frac := (t - table_radius) / (girdle_radius - table_radius)
var pos := dir * t
pos.y = lerp(0.0, -crown_drop, frac)
pos += perp * randf_range(-border_jitter, border_jitter)
_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
inst.position = pos
inst.rotation.y = randf() * TAU
inst.scale = Vector3.ONE * scale_factor
var cs := CollisionShape3D.new()
cs.shape = shape
inst.add_child(cs)
parent.add_child(inst)