Files
dreamer-game/scripts/dream_world.gd
2026-08-31 11:07:01 +02:00

341 lines
13 KiB
GDScript

extends Node3D
class_name DreamWorld
## Die Traumwelt: eine schwebende Insel mit einem Dorf. Vier Wesen wohnen hier,
## jedes in seinem Haus, jedes mit eigenem Tagesablauf.
var spawn := Vector3(0, 2.0, 20.0)
var bewohner: Array = []
var _deko: Array = []
var _freihalten: Array[Vector3] = []
func _ready() -> void:
_environment()
_insel()
_haeuser()
_lehrpulte()
_schwarzes_brett()
_lichtung()
_tor_zur_erde()
_gruen()
_wesen()
func _environment() -> void:
var we := WorldEnvironment.new()
var env := Environment.new()
env.background_mode = Environment.BG_SKY
var sky := Sky.new()
var mat := ProceduralSkyMaterial.new()
mat.sky_top_color = Color(0.10, 0.05, 0.24)
mat.sky_horizon_color = Color(0.42, 0.24, 0.55)
mat.ground_bottom_color = Color(0.06, 0.03, 0.14)
mat.ground_horizon_color = Color(0.32, 0.18, 0.45)
mat.sun_angle_max = 40.0
sky.sky_material = mat
env.sky = sky
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
env.ambient_light_energy = 2.1
env.glow_enabled = true
env.glow_intensity = 0.32
env.glow_bloom = 0.05
env.fog_enabled = true
env.fog_light_color = Color(0.30, 0.18, 0.45)
env.fog_density = 0.0015
we.environment = env
add_child(we)
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-46, 25, 0)
sun.light_energy = 1.5
sun.light_color = Color(0.85, 0.80, 1.0)
sun.shadow_enabled = true
sun.shadow_blur = 2.0
add_child(sun)
func _insel() -> void:
var radius := Game.insel_radius()
Prims.cyl(self, Vector3(0, -1.03, 0), radius, 2.0, Color(0.48, 0.40, 0.64))
Prims.cyl(self, Vector3(0, -4.0, 0), radius * 0.77, 4.0, Color(0.34, 0.26, 0.50), false)
Prims.cyl(self, Vector3(0, -9.0, 0), radius * 0.42, 6.0, Color(0.24, 0.17, 0.38), false)
_wege()
# schwebende Deko
var rng := RandomNumberGenerator.new()
rng.seed = 7
for i in range(34):
var a := rng.randf() * TAU
var r := rng.randf_range(18.0, maxf(24.0, Game.insel_radius() - 4.0))
var p := Vector3(cos(a) * r, rng.randf_range(5.0, 24.0), sin(a) * r)
var s := rng.randf_range(0.6, 2.0)
var c := Color(0.6 + rng.randf() * 0.3, 0.5 + rng.randf() * 0.4, 1.0)
var wuerfel := Prims.box(self, p, Vector3(s, s, s), c, false, 1.1)
wuerfel.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_deko.append(wuerfel)
## Die gewachsenen Wege: je Kante ein flaches Band, an den Knoten ein Fleck,
## damit Ecken nicht ausfransen.
## Begrünung nach Regel, nicht von Hand: Poisson-Scheibe über der freien
## Fläche. Nichts wächst auf einem Weg, in einem Haus oder auf dem Dorfplatz.
func _gruen() -> void:
_freihalten.append(Vector3(spawn.x, 0.0, spawn.z))
var radius := Game.insel_radius() - 4.0
var rng := RandomNumberGenerator.new()
rng.seed = 91
var gesetzt: Array[Vector3] = []
var abstand := 9.0
for versuch in range(1400):
var a := rng.randf() * TAU
var r := sqrt(rng.randf()) * radius
var p := Vector3(cos(a) * r, 0.0, sin(a) * r)
if p.length() < 20.0:
continue
var frei := true
for g in gesetzt:
if g.distance_to(p) < abstand:
frei = false
break
if not frei:
continue
if not _freie_stelle(p, 3.2):
continue
gesetzt.append(p)
_baum(p, rng)
func _freie_stelle(p: Vector3, luft: float) -> bool:
for frei in _freihalten:
if p.distance_to(frei) < 7.0:
return false
for key in DreamDB.PLACES:
var s: Array = DreamDB.PLACES[key]["size"]
if float(s[0]) <= 0.0 or not Game.ort_offen(key):
continue
var c := Orte.center(DreamDB.PLACES, key)
if absf(p.x - c.x) < float(s[0]) * 0.5 + luft \
and absf(p.z - c.z) < float(s[1]) * 0.5 + luft:
return false
for k in DreamDB.NETZ.kanten:
if Netz.trifft_rechteck(DreamDB.NETZ.knoten[k[0]], DreamDB.NETZ.knoten[k[1]],
p, Vector2(luft * 2.0, luft * 2.0), 0.6):
return false
return true
## Traumbäume: schlanker Stamm, zwei versetzte Kronen, leuchtende Spitze.
func _baum(p: Vector3, rng: RandomNumberGenerator) -> void:
var hoehe := rng.randf_range(3.0, 6.0)
# Grün gegen den lila Grund -- das macht die Insel erst gemütlich.
var ton := Color.from_hsv(0.26 + rng.randf() * 0.12, 0.48 + rng.randf() * 0.18,
0.58 + rng.randf() * 0.26)
Prims.cyl(self, p + Vector3(0, hoehe * 0.5, 0), 0.24, hoehe,
Color(0.34, 0.26, 0.30))
var k1 := Prims.sphere(self, p + Vector3(0, hoehe + 0.4, 0),
rng.randf_range(1.1, 2.0), ton)
k1.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
if rng.randf() < 0.6:
var k2 := Prims.sphere(self, p + Vector3(rng.randf_range(-0.8, 0.8),
hoehe - 0.7, rng.randf_range(-0.8, 0.8)), rng.randf_range(0.8, 1.5),
ton.lightened(0.12))
k2.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
if rng.randf() < 0.3:
Prims.sphere(self, p + Vector3(0, hoehe + 1.6, 0), 0.28,
Color(0.9, 0.95, 1.0), 1.6)
func _wege() -> void:
var farbe := Color(0.66, 0.58, 0.84)
for k in DreamDB.NETZ.kanten:
var a: Vector3 = DreamDB.NETZ.knoten[k[0]]
var b: Vector3 = DreamDB.NETZ.knoten[k[1]]
var d := b - a
if d.length() < 0.2:
continue
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.03, 0),
Vector3(2.6, 0.06, d.length()), farbe, false)
band.rotation.y = atan2(d.x, d.z)
for k in DreamDB.NETZ.knoten:
Prims.cyl(self, k + Vector3(0, 0.035, 0), 1.3, 0.06, farbe, false)
func _haeuser() -> void:
for key in DreamDB.PLACES:
var pl: Dictionary = DreamDB.PLACES[key]
var s: Array = pl["size"]
if s[0] <= 0.0 or not Game.ort_offen(key):
continue
var c := Orte.center(DreamDB.PLACES, key)
var groesse := Vector3(s[0], 3.2, s[1])
var farbe: Color = pl["color"]
Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
Orte.DIR_VEC[pl["dir"]], 3.4, true)
Prims.label(self, c + Vector3(0, 5.0, 0), pl["name"], 0.6, Color(1, 0.95, 0.85))
_einrichtung(c, groesse, farbe, key)
var licht := OmniLight3D.new()
licht.position = c + Vector3(0, 2.4, 0)
licht.omni_range = 18.0
licht.light_energy = 2.2
licht.light_color = farbe.lightened(0.5)
add_child(licht)
# Weg von der Tür zum Ring
var tuer := Orte.door(DreamDB.PLACES, key)
var weg := tuer.normalized() * 15.0 - tuer
var b := Prims.box(self, (tuer + tuer.normalized() * 15.0) * 0.5 + Vector3(0, 0.03, 0),
Vector3(2.4, 0.06, weg.length()), Color(0.66, 0.58, 0.84), false)
b.rotation.y = atan2(weg.x, weg.z)
func _einrichtung(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
if key == "hoersaal":
_hoersaal_einrichten(c, groesse, farbe, key)
return
var w := groesse.x * 0.5 - 1.8
var h := groesse.z * 0.5 - 1.8
Prims.cyl(self, c + Vector3(-w, 0.3, -h), 1.4, 0.5, farbe.darkened(0.2), false)
match int(key.substr(5)) % 4:
0: # Webstuhl
Prims.box(self, c + Vector3(w, 1.1, 0), Vector3(0.3, 2.2, 3.0), farbe.darkened(0.45))
for i in range(5):
Prims.box(self, c + Vector3(w, 1.1, -1.2 + i * 0.6), Vector3(0.1, 1.8, 0.06),
Color(0.9, 0.85, 1.0), false, 1.4)
1: # Regale voller Fundstücke
for i in range(3):
Prims.box(self, c + Vector3(w, 0.6 + i * 0.7, 0), Vector3(1.2, 0.12, 4.0),
farbe.darkened(0.45))
for j in range(4):
Prims.box(self, c + Vector3(w, 0.85 + i * 0.7, -1.5 + j * 1.0),
Vector3(0.4, 0.4, 0.4), Color(0.8, 0.9, 1.0), false, 0.8)
2: # Karten und ein Fernrohr
Prims.box(self, c + Vector3(w, 0.75, 0), Vector3(1.6, 0.12, 2.6), farbe.darkened(0.45))
var rohr := Prims.cyl(self, c + Vector3(w, 1.6, 0), 0.18, 2.0,
Color(0.85, 0.9, 1.0), false, 0.6)
rohr.rotation.x = 0.9
_: # Schale mit Licht
Prims.cyl(self, c + Vector3(w, 0.5, 0), 1.0, 1.0, farbe.darkened(0.4), false)
Prims.sphere(self, c + Vector3(w, 1.4, 0), 0.55, Color(0.8, 1.0, 1.0), 2.0)
## Bankreihen mit Mittelgang, Tafel an der Rückwand.
func _hoersaal_einrichten(c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
var richtung: Vector3 = Orte.DIR_VEC[DreamDB.PLACES[key]["dir"]] * -1.0
var quer := absf(richtung.z) > 0.0
var bank := ((groesse.x if quer else groesse.z) - 5.0) * 0.5
var laengs: Vector3 = Vector3(1, 0, 0) if quer else Vector3(0, 0, 1)
for i in range(3):
var tief := -3.6 + float(i) * 1.8
for sx: float in [-1.0, 1.0]:
var mitte: Vector3 = c + richtung * tief + laengs * (sx * (bank * 0.5 + 1.0))
var sitz: Vector3 = Vector3(bank, 0.25, 0.9) if quer else Vector3(0.9, 0.25, bank)
var lehne: Vector3 = Vector3(bank, 0.9, 0.15) if quer else Vector3(0.15, 0.9, bank)
Prims.box(self, mitte + Vector3(0, 0.5, 0), sitz, farbe.darkened(0.35))
Prims.box(self, mitte + richtung * 0.7 + Vector3(0, 0.9, 0), lehne,
farbe.darkened(0.45))
var tafel: Vector3 = Vector3(groesse.x - 5.0, 2.0, 0.15) if quer \
else Vector3(0.15, 2.0, groesse.z - 5.0)
Prims.box(self, c + richtung * 4.4 + Vector3(0, 1.4, 0), tafel, Color(0.16, 0.18, 0.30))
## Vorn in jedem offenen Lehrort steht ein Pult.
func _lehrpulte() -> void:
for key in DreamDB.lehrorte():
if not Game.ort_offen(key) or DreamDB.techniken_von(key).is_empty():
continue
var pl: Dictionary = DreamDB.PLACES[key]
var tief: float = float(pl["size"][1])
var innen: Vector3 = Orte.DIR_VEC[pl["dir"]] * -1.0
var c := Orte.center(DreamDB.PLACES, key)
var pos := c + innen * (tief * 0.5 - 2.6)
var farbe: Color = pl["color"]
Prims.box(self, pos + Vector3(0, 0.55, 0), Vector3(2.4, 1.1, 0.9), farbe.darkened(0.35))
Prims.box(self, pos + Vector3(0, 1.18, 0), Vector3(2.8, 0.16, 1.2), farbe.darkened(0.2))
Prims.sphere(self, pos + Vector3(0, 1.7, 0), 0.34, Color(0.85, 0.92, 1.0), 2.0)
var lehrt: Array = DreamDB.techniken_von(key)
Prims.label(self, pos + Vector3(0, 2.6, 0),
TechDB.by_id(str(lehrt[0]))["name"], 0.42, Color(0.85, 0.92, 1))
var n := _marke(pos - innen * 1.8 + Vector3(0, 1.4, 0), "lehre", str(pl["name"]))
n.set_meta("arg", key)
## An der Lichtung: hier hängen die Menschen aus, die Hilfe brauchen.
func _schwarzes_brett() -> void:
var pos := Orte.center(DreamDB.PLACES, "lichtung") + Vector3(-6.0, 0, 9.0)
for s: float in [-1.4, 1.4]:
Prims.cyl(self, pos + Vector3(s, 1.3, 0), 0.16, 2.6, Color(0.45, 0.38, 0.60))
Prims.box(self, pos + Vector3(0, 2.2, 0), Vector3(3.6, 2.4, 0.18), Color(0.18, 0.16, 0.26))
for i in range(HumanDB.LIST.size()):
var x := -1.2 + float(i % 2) * 2.4
var y := 2.8 - float(i / 2) * 1.1
Prims.box(self, pos + Vector3(x, y, 0.12), Vector3(1.1, 0.8, 0.04),
Color(0.92, 0.90, 0.80), false, 0.5)
Prims.label(self, pos + Vector3(0, 4.2, 0), "Schwarzes Brett", 0.5, Color(1, 0.95, 0.85))
_marke(pos + Vector3(0, 1.6, 1.2), "brett", "Schwarzes Brett")
## Unsichtbarer Punkt, an dem die Interaktion hängt. Um jede Marke bleibt ein
## Stück frei, damit später kein Baum davorsteht.
func _marke(pos: Vector3, kind: String, titel: String) -> Node3D:
_freihalten.append(Vector3(pos.x, 0.0, pos.z))
var n := Node3D.new()
n.position = pos
add_child(n)
n.add_to_group("interactable")
n.set_meta("kind", kind)
n.set_meta("arg", "")
n.set_meta("titel", titel)
return n
func _lichtung() -> void:
var c := Orte.center(DreamDB.PLACES, "lichtung")
Prims.cyl(self, c + Vector3(0, 0.04, 0), 11.0, 0.12, Color(0.60, 0.52, 0.78), false)
Prims.cyl(self, c + Vector3(0, 0.14, 0), 3.2, 0.2, Color(0.55, 0.80, 1.0, 0.5), false, 1.2)
for i in range(8):
var a := TAU * float(i) / 8.0
var p := c + Vector3(cos(a) * 8.5, 0, sin(a) * 8.5)
Prims.cyl(self, p + Vector3(0, 1.6, 0), 0.28, 3.2, Color(0.72, 0.64, 0.88))
Prims.sphere(self, p + Vector3(0, 3.6, 0), 0.42, Color(0.75, 0.85, 1.0), 2.0)
Prims.label(self, c + Vector3(0, 5.2, 0), str(DreamDB.PLACES["lichtung"]["name"]), 0.6,
Color(1, 0.95, 0.85))
func _tor_zur_erde() -> void:
var pos := Orte.center(DreamDB.PLACES, "tor")
# flach und ohne Kollision wie die Lichtung — eine 0,6 hohe Stufe hätte den
# Spieler vor dem Tor stehen lassen
Prims.cyl(self, pos + Vector3(0, 0.05, 0), 5.5, 0.1, Color(0.35, 0.30, 0.50), false)
for i in range(10):
var a := PI * float(i) / 9.0
Prims.box(self, pos + Vector3(cos(a) * 4.2, sin(a) * 4.2 + 0.6, 0),
Vector3(0.7, 0.7, 0.7), Color(0.5, 0.85, 1.0), false, 1.8)
var scheibe := Prims.cyl(self, pos + Vector3(0, 2.6, 0), 3.0, 0.2,
Color(0.45, 0.75, 1.0, 0.28), false, 1.0)
scheibe.rotation.x = PI * 0.5
Prims.label(self, pos + Vector3(0, 6.4, 0), "Tor zur Erde", 0.8, Color(0.7, 0.95, 1))
_marke(pos + Vector3(0, 1.6, 0), "erde", "Tor zur Erde")
## Am Anfang ist das Dorf leer; mit jedem erfüllten Traum zieht ein Wesen ein.
func _wesen() -> void:
for i in range(DreamDB.LIST.size()):
if not Game.wesen_eingezogen(i):
continue
var w := Bewohner.new()
add_child(w)
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES, DreamDB.NETZ, DreamDB.ANKER)
bewohner.append(w)
func _process(delta: float) -> void:
for i in range(_deko.size()):
var n: Node3D = _deko[i]
n.rotation.y += delta * (0.2 + 0.05 * (i % 5))
n.rotation.x += delta * 0.13
n.position.y += sin(Time.get_ticks_msec() * 0.0007 + i) * delta * 0.6