Godot 4.6 exploration game with player movement, step-rotation camera rig, and POI interaction system (explore/rest/close menu). main.tscn is a test scene with placeholder props; debug/ holds unwired terrain prototypes (voxel river, procedural island mesh, cross-shaped hub
272 lines
10 KiB
GDScript
272 lines
10 KiB
GDScript
extends Node3D
|
|
|
|
@export var berg_scale : float = .002
|
|
|
|
# Welt-Parameter
|
|
const ISLAND_RADIUS : float = 512.0
|
|
const GRID : float = 1.0 # 1m pro Block
|
|
const CORRIDOR_WIDTH : int = 3 # Korridor-Breite in Blöcken
|
|
const CENTER_RADIUS : float = 120.0 # Radius Mittelzone
|
|
|
|
# Pass-Position: Anteil vom Rand der Mittelzone (0=Mitte, 1=Inselrand)
|
|
# 0.35 = ca. erstes Drittel ab Mittelzone Richtung Rand
|
|
const PASS_POS : float = 0.35
|
|
const PASS_WIDTH : int = 3 # Lücke in Blöcken
|
|
|
|
func _ready() -> void:
|
|
_spawn_kreuz()
|
|
_spawn_mittelring()
|
|
_spawn_ozean()
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# KREUZ — 4 Arme: Nord, Süd, West = Berge | Ost = Fluss
|
|
# ═══════════════════════════════════════════════════════════════
|
|
func _spawn_kreuz() -> void:
|
|
# Jeder Arm geht von CENTER_RADIUS bis ISLAND_RADIUS
|
|
_spawn_arm_berge("Nord", Vector3( 0, 0, -1))
|
|
_spawn_arm_berge("Sued", Vector3( 0, 0, 1))
|
|
_spawn_arm_berge("West", Vector3(-1, 0, 0))
|
|
_spawn_arm_fluss("Ost", Vector3( 1, 0, 0))
|
|
|
|
# ── Berg-Arm ─────────────────────────────────────────────────
|
|
func _spawn_arm_berge(label: String, dir: Vector3) -> void:
|
|
var grp := Node3D.new()
|
|
grp.name = "Arm_" + label
|
|
add_child(grp)
|
|
|
|
var perp := dir.cross(Vector3.UP).normalized()
|
|
var arm_len := ISLAND_RADIUS - CENTER_RADIUS
|
|
var steps := int(arm_len / GRID)
|
|
|
|
# Pass-Bereich berechnen (in Steps ab Mittelzone)
|
|
var pass_start := int(steps * PASS_POS)
|
|
var pass_end := pass_start + PASS_WIDTH
|
|
|
|
# Große Berg-Modelle vorbauen
|
|
var large_meshes := _make_large_berge()
|
|
var small_meshes := _make_small_berge()
|
|
|
|
for step in range(steps):
|
|
# Position entlang des Arms
|
|
var dist := CENTER_RADIUS + step * GRID + GRID * 0.5
|
|
var center := dir * dist
|
|
|
|
var in_pass := (step >= pass_start and step < pass_end)
|
|
|
|
for col in range(-CORRIDOR_WIDTH / 2, CORRIDOR_WIDTH / 2 + 1):
|
|
var pos := center + perp * (col * GRID)
|
|
pos.y = 0.0
|
|
|
|
if in_pass:
|
|
# Im Pass: nur außen kleine Berge, Mitte frei
|
|
if abs(col) == CORRIDOR_WIDTH / 2:
|
|
_place_berg(grp, pos, small_meshes)
|
|
# col == 0 → frei (Durchgang)
|
|
else:
|
|
# Außenreihen → kleine Berge dicht
|
|
if abs(col) >= 1:
|
|
_place_berg(grp, pos, small_meshes)
|
|
# Mittelreihe → große Berge
|
|
else:
|
|
_place_berg(grp, pos, large_meshes)
|
|
|
|
# Pass-Marker (Platzhalter Tor)
|
|
var pass_dist := CENTER_RADIUS + (pass_start + PASS_WIDTH * 0.5) * GRID
|
|
var pass_center := dir * pass_dist
|
|
_spawn_pass_marker(grp, pass_center)
|
|
|
|
func _place_berg(parent: Node3D, pos: Vector3, meshes: Array) -> void:
|
|
var mi := MeshInstance3D.new()
|
|
mi.mesh = meshes[randi() % meshes.size()]
|
|
mi.position = pos
|
|
# Zufällige Y-Rotation für Abwechslung
|
|
mi.rotation.y = randf() * TAU
|
|
parent.add_child(mi)
|
|
|
|
# ── Fluss-Arm ────────────────────────────────────────────────
|
|
func _spawn_arm_fluss(label: String, dir: Vector3) -> void:
|
|
var grp := Node3D.new()
|
|
grp.name = "Arm_" + label
|
|
add_child(grp)
|
|
|
|
var arm_len := ISLAND_RADIUS - CENTER_RADIUS
|
|
var steps := int(arm_len / GRID)
|
|
|
|
var pass_start := int(steps * PASS_POS)
|
|
var pass_end := pass_start + PASS_WIDTH
|
|
|
|
var mat_wasser := StandardMaterial3D.new()
|
|
mat_wasser.albedo_color = Color(0.18, 0.45, 0.78)
|
|
mat_wasser.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
|
|
for step in range(steps):
|
|
var dist := CENTER_RADIUS + step * GRID + GRID * 0.5
|
|
var pos := dir * dist
|
|
pos.y = -0.5 # 1m tief eingeschnitten
|
|
|
|
var in_pass := (step >= pass_start and step < pass_end)
|
|
|
|
if in_pass:
|
|
# Brücken-Block
|
|
_spawn_bruecke_block(grp, dir * dist)
|
|
else:
|
|
# Fluss-Block: 1x1m Wasser
|
|
var mi := MeshInstance3D.new()
|
|
var box := BoxMesh.new()
|
|
box.size = Vector3(GRID, GRID, GRID)
|
|
box.surface_set_material(0, mat_wasser)
|
|
mi.mesh = box
|
|
mi.position = pos
|
|
mi.name = "Fluss_" + str(step)
|
|
grp.add_child(mi)
|
|
|
|
func _spawn_bruecke_block(parent: Node3D, pos: Vector3) -> void:
|
|
var mi := MeshInstance3D.new()
|
|
var box := BoxMesh.new()
|
|
box.size = Vector3(GRID * 3.0, 0.3, GRID * 3.0)
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.55, 0.45, 0.32)
|
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
box.surface_set_material(0, mat)
|
|
mi.mesh = box
|
|
mi.position = pos + Vector3(0, 0.65, 0)
|
|
mi.name = "Bruecke"
|
|
parent.add_child(mi)
|
|
|
|
# ── Pass-Marker ───────────────────────────────────────────────
|
|
func _spawn_pass_marker(parent: Node3D, pos: Vector3) -> void:
|
|
var mi := MeshInstance3D.new()
|
|
var box := BoxMesh.new()
|
|
box.size = Vector3(2.0, 4.0, 2.0)
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.85, 0.78, 0.40)
|
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
box.surface_set_material(0, mat)
|
|
mi.mesh = box
|
|
mi.position = pos + Vector3(0, 2.0, 0)
|
|
mi.name = "PassMarker"
|
|
parent.add_child(mi)
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MITTELRING — Bergring um die Mittelzone
|
|
# ═══════════════════════════════════════════════════════════════
|
|
func _spawn_mittelring() -> void:
|
|
var grp := Node3D.new()
|
|
grp.name = "Mittelring"
|
|
add_child(grp)
|
|
|
|
var large_meshes := _make_large_berge()
|
|
var small_meshes := _make_small_berge()
|
|
|
|
# Umfang des Kreises in 1m-Schritten
|
|
var circumference := TAU * CENTER_RADIUS
|
|
var steps := int(circumference / GRID)
|
|
|
|
# Die 4 Kreuz-Arme blockieren bestimmte Winkel → dort Pässe
|
|
# Arm-Winkel: Nord=270°, Süd=90°, West=180°, Ost=0°
|
|
var arm_angles := [0.0, PI * 0.5, PI, PI * 1.5]
|
|
var pass_half := (float(PASS_WIDTH) * GRID) / CENTER_RADIUS # Winkel-Halbbreite
|
|
|
|
for step in range(steps):
|
|
var angle := float(step) / steps * TAU
|
|
|
|
# Prüfen ob dieser Winkel in einem Arm-Durchgang liegt
|
|
var in_arm := false
|
|
for arm_a in arm_angles:
|
|
var diff: float = abs(angle_difference(angle, arm_a))
|
|
if diff < pass_half * 1.5:
|
|
in_arm = true
|
|
break
|
|
|
|
# Innenring (dicht am Center_radius)
|
|
var r_inner := CENTER_RADIUS - 1.0
|
|
var pos_i := Vector3(cos(angle)*r_inner, 0, sin(angle)*r_inner)
|
|
# Außenring
|
|
var r_outer := CENTER_RADIUS + 2.0
|
|
var pos_o := Vector3(cos(angle)*r_outer, 0, sin(angle)*r_outer)
|
|
|
|
if not in_arm:
|
|
_place_berg(grp, pos_i, large_meshes)
|
|
_place_berg(grp, pos_o, small_meshes)
|
|
|
|
# Hilfsfunktion: Winkel-Differenz -PI..PI
|
|
func angle_difference(a: float, b: float) -> float:
|
|
var d := fmod(a - b + TAU, TAU)
|
|
if d > PI: d -= TAU
|
|
return d
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# BERG-MODELLE — prozedural, mehrere Varianten
|
|
# ═══════════════════════════════════════════════════════════════
|
|
func _make_large_berge() -> Array:
|
|
var result := []
|
|
# 4 große Varianten: breiter, massiver
|
|
var configs := [
|
|
[8.0, 0.05, 22.0, 6],
|
|
[10.0, 0.05, 18.0, 7],
|
|
[7.0, 0.05, 26.0, 5],
|
|
[12.0, 0.05, 20.0, 8],
|
|
]
|
|
for cfg in configs:
|
|
result.append(_make_berg_mesh(cfg[0], cfg[1], cfg[2] * berg_scale, int(cfg[3]), false))
|
|
return result
|
|
|
|
func _make_small_berge() -> Array:
|
|
var result := []
|
|
# 6 kleine Varianten: schmaler, dichter packbar
|
|
var configs := [
|
|
[3.0, 0.02, 10.0, 5],
|
|
[2.5, 0.02, 8.0, 6],
|
|
[4.0, 0.02, 12.0, 5],
|
|
[2.0, 0.02, 7.0, 4],
|
|
[3.5, 0.02, 11.0, 6],
|
|
[2.8, 0.02, 9.0, 5],
|
|
]
|
|
for cfg in configs:
|
|
result.append(_make_berg_mesh(cfg[0], cfg[1], cfg[2], int(cfg[3]), true))
|
|
return result
|
|
|
|
func _make_berg_mesh(bot_r: float, top_r: float, h: float,
|
|
sides: int, small: bool) -> Mesh:
|
|
var st := SurfaceTool.new()
|
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
|
|
|
var tip := Vector3(0, h, 0)
|
|
var gray := randf_range(0.35, 0.55) if not small else randf_range(0.40, 0.58)
|
|
var col := Color(gray, gray * 0.97, gray * 0.93)
|
|
var col_b := Color(gray * 0.7, gray * 0.68, gray * 0.65)
|
|
|
|
for s in range(sides):
|
|
var a0 := float(s) / sides * TAU
|
|
var a1 := float(s+1) / sides * TAU
|
|
var b0 := Vector3(cos(a0)*bot_r, 0, sin(a0)*bot_r)
|
|
var b1 := Vector3(cos(a1)*bot_r, 0, sin(a1)*bot_r)
|
|
var t0 := Vector3(cos(a0)*top_r, h, sin(a0)*top_r)
|
|
var t1 := Vector3(cos(a1)*top_r, h, sin(a1)*top_r)
|
|
|
|
# Seite
|
|
var n := (b1 - b0).cross(tip - b0).normalized()
|
|
st.set_normal(n); st.set_color(col)
|
|
st.add_vertex(b0); st.add_vertex(tip); st.add_vertex(b1)
|
|
# Boden-Cap
|
|
st.set_normal(Vector3(0,-1,0)); st.set_color(col_b)
|
|
st.add_vertex(Vector3(0,0,0)); st.add_vertex(b1); st.add_vertex(b0)
|
|
|
|
return st.commit()
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# OZEAN unter der Insel
|
|
# ═══════════════════════════════════════════════════════════════
|
|
func _spawn_ozean() -> void:
|
|
var mi := MeshInstance3D.new()
|
|
var plane := PlaneMesh.new()
|
|
plane.size = Vector2(4096, 4096)
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.08, 0.25, 0.52)
|
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
plane.surface_set_material(0, mat)
|
|
mi.mesh = plane
|
|
mi.position = Vector3(0, -40, 0)
|
|
mi.name = "Ozean"
|
|
add_child(mi)
|