Flat root layout replaced with a domain-based structure:
- autoload/ the two existing singleton scripts (InteractionManager,
InteractionUI) — grouped by role, matching project.godot's
[autoload] section
- world/ the flying island: terrain.gd, and world/poi/ for the POI
marker + gate scripts/scenes
- player/ everything about the player as a character: player.gd/.tscn,
camera_rig.gd (it only exists to follow the player)
- boot/ created empty for now, will hold the opening/menu screens
main.tscn renamed to world/island.tscn — "main" stops making sense once
the actual run/main_scene becomes a boot screen (next commit). Verified
via grep that main.tscn was referenced nowhere else by path (only by UID
elsewhere, which self-heals).
Every .gd got its .uid sidecar moved alongside it. Two ext_resource
entries had no UID (poi_gate.tscn's own script ref, and island.tscn's
ref to poi_gate.tscn) and needed their path= fixed by hand; everything
else is UID-based and resolved itself after a project reimport.
Also removed two untracked-looking but actually committed editor temp
files (preview_scene.tscn*.tmp, leftovers from the unused hterrain
addon, referenced nowhere).
Verified headless: all 8 affected scenes (island, poi, poi_gate,
player, interaction_ui, and the three debug prototypes that reference
moved files) load with exit code 0. No behavior change, pure move.
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 world/island.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)
|