extends Node3D class_name EarthWorld ## Die Erde: eine Kleinstadt mit vier Menschen, die ihren Tag leben. var spawn := Vector3(0, 1.4, 12.0) var humans: Array = [] var passanten: Array = [] # Statisten ohne Geschichte var spieler: Node3D = null # setzt main, für den Etagenschnitt var nav_map: RID # Navigationsnetz: Straßen, Türen, Möbel, Klötze const HOEHE := 3.0 # Raumhöhe -- alles in der Stadt ist Erdgeschoss const NAV_ZELLE_HOCH := 0.1 var _bauten: Array = [] # alles, was der Etagenschnitt aufschneidet var _sun: DirectionalLight3D var _sky: ProceduralSkyMaterial var _env: Environment var _we: WorldEnvironment var _nachtlichter: Array = [] func _ready() -> void: _environment() _boden() _bebauung() _orte() _park() _saeule() _navigation() _menschen() _himmel_aktualisieren() ## Das Navigationsnetz wird aus allen Kollisionsformen gebacken: Boden, ## Straßen, Wände mit Türlücken, Möbel, Klötze. Wer darauf läuft, findet die ## Türen von allein. func _navigation() -> void: var region := NavigationRegion3D.new() add_child(region) var nm := NavigationMesh.new() nm.geometry_parsed_geometry_type = NavigationMesh.PARSED_GEOMETRY_STATIC_COLLIDERS nm.agent_radius = 0.4 nm.agent_height = 1.6 nm.agent_max_climb = 0.35 nm.agent_max_slope = 40.0 nm.cell_height = NAV_ZELLE_HOCH # fein, sonst schweben die Leute 25 cm über der Straße var quelle := NavigationMeshSourceGeometryData3D.new() NavigationServer3D.parse_source_geometry_data(nm, quelle, self) NavigationServer3D.bake_from_source_geometry_data(nm, quelle) # eigene Karte: die Traumwelt liegt im selben Raum, ihre Polygone dürfen hier nicht auftauchen nav_map = NavigationServer3D.map_create() NavigationServer3D.map_set_active(nav_map, true) NavigationServer3D.map_set_cell_height(nav_map, NAV_ZELLE_HOCH) region.set_navigation_map(nav_map) region.navigation_mesh = nm # synchron: sonst kommen die ersten Pfade leer zurück NavigationServer3D.map_set_use_async_iterations(nav_map, false) NavigationServer3D.map_force_update(nav_map) func _environment() -> void: _we = WorldEnvironment.new() var we := _we _env = Environment.new() _env.background_mode = Environment.BG_SKY var sky := Sky.new() _sky = ProceduralSkyMaterial.new() sky.sky_material = _sky _env.sky = sky _env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR # der Nachthimmel ist zu dunkel als Lichtquelle _env.glow_enabled = false # Tageslicht braucht kein Leuchten; kostet ein Fünftel der Bildrate _env.fog_enabled = true _env.fog_density = 0.0012 we.environment = _env add_child(we) _sun = DirectionalLight3D.new() _sun.shadow_enabled = true _sun.shadow_blur = 1.0 _sun.directional_shadow_max_distance = 110.0 # die Kamera sieht 60 m weit; nah und scharf statt weit und weich add_child(_sun) const STADTRAND := 140.0 # unsichtbare Wand: hinter dem Bezirk endet die Welt func _boden() -> void: Prims.box(self, Vector3(0, -0.53, 0), Vector3(300, 1, 300), Color(0.42, 0.48, 0.40)) _strassen() # Stadtrand: wer immer weiter läuft, fiele sonst vom Rand der Welt for d: Vector3 in [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]: var wand := StaticBody3D.new() var cs := CollisionShape3D.new() var form := BoxShape3D.new() form.size = Vector3(4.0, 30.0, STADTRAND * 2.0 + 8.0) if absf(d.x) > 0.0 \ else Vector3(STADTRAND * 2.0 + 8.0, 30.0, 4.0) cs.shape = form wand.add_child(cs) wand.position = d * (STADTRAND + 2.0) + Vector3(0, 15.0, 0) add_child(wand) ## Arterien breit, roji schmal -- beide als flaches Band mit Belag. func _strassen() -> void: for st in HumanDB.STRASSEN: var a: Vector3 = st["a"] var b: Vector3 = st["b"] var d := b - a if d.length() < 0.2: continue var breit := float(st["breite"]) var farbe := Color(0.62, 0.60, 0.58) if breit > 5.0 else Color(0.55, 0.53, 0.52) # gestaffelt: Kreuzungen und Gasseneinfahrten flimmern sonst (Z-Fighting) var hoch := 0.02 if breit > 5.0 and absf(d.z) > 0.0 else (0.03 if breit > 5.0 else 0.04) var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, hoch, 0), Vector3(breit, 0.06, d.length() + breit), farbe, false) band.rotation.y = atan2(d.x, d.z) ## Alles, was nicht Wohnung oder Sonderbau ist, steht als geschlossener Klotz ## mit ein bis vier Stockwerken da. Erst dadurch wird der Bezirk dicht. func _bebauung() -> void: var rng := RandomNumberGenerator.new() rng.seed = 8123 var belegt: Dictionary = {} for key in HumanDB.PLACES: var s: Array = HumanDB.PLACES[key]["size"] if float(s[0]) > 0.0: belegt[Orte.center(HumanDB.PLACES, key).snapped(Vector3.ONE * 0.1)] = true for p in HumanDB.PARZELLEN: var c: Vector3 = p["pos"] if belegt.has(c.snapped(Vector3.ONE * 0.1)): continue var g: Vector2 = p["size"] if minf(g.x, g.y) < 3.0: continue var hoehe := HOEHE # nur Erdgeschoss, wie alles in der Stadt # acht Töne statt achtzig: gleiche Farbe heißt gleiches Material var ton := Color.from_hsv(0.07 + float(rng.randi_range(0, 3)) * 0.04, 0.10 + float(rng.randi_range(0, 1)) * 0.12, 0.62 + float(rng.randi_range(0, 3)) * 0.08) var bau := Node3D.new() add_child(bau) var klotz := Prims.box(bau, c + Vector3(0, hoehe * 0.5, 0), Vector3(g.x, hoehe, g.y), ton, false) # Kollision etwas größer: so schließt sich die Fuge zum Nachbarn, niemand keilt sich hinein Prims._add_box_body(klotz, Vector3(g.x + 0.9, hoehe, g.y + 0.9)) _fenster(bau, c, g, ton) Prims.box(bau, c + Vector3(0, hoehe + 0.2, 0), Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25)) if rng.randf() < 0.35: Prims.box(bau, c + Vector3(0, hoehe + 0.9, 0), Vector3(g.x * 0.4, 1.0, g.y * 0.4), ton.darkened(0.1)) _bauten.append({"c": c, "size": g, "hoch": hoehe, "teile": [bau], "waende": []}) ## Wohnungen und Sonderbauten: geschlossene Häuser, ein Raum mit Tür, Möbeln ## und Spuren. Der Geist geht hinein, dann verschwinden Dach und Vorderwände. func _orte() -> void: for key in HumanDB.PLACES: var pl: Dictionary = HumanDB.PLACES[key] var s: Array = pl["size"] if float(s[0]) <= 0.0: continue var c := Orte.center(HumanDB.PLACES, key) var groesse := Vector3(float(s[0]), HOEHE, float(s[1])) var farbe: Color = pl["color"] var tuer: float = maxf(1.6, minf(3.0, minf(groesse.x, groesse.z) - 3.0)) var raum := Node3D.new() add_child(raum) var waende: Array = Prims.room(raum, c, groesse, farbe, farbe.darkened(0.35), Orte.DIR_VEC[pl["dir"]], tuer, true) var band := Node3D.new() # verschwindet drinnen mit dem Dach add_child(band) _fenster(band, c, Vector2(groesse.x, groesse.z), farbe, true) _einrichtung(raum, c, groesse, farbe, key) var wer := HumanDB.bewohner_von(key) if not wer.is_empty(): _spuren(raum, key, c, groesse, wer) var licht := OmniLight3D.new() licht.position = c + Vector3(0, 2.2, 0) licht.omni_range = 12.0 licht.light_color = Color(1.0, 0.85, 0.6) licht.light_energy = 0.0 raum.add_child(licht) _nachtlichter.append(licht) var dach := Node3D.new() add_child(dach) Prims.box(dach, c + Vector3(0, HOEHE + 0.1, 0), Vector3(groesse.x + 0.4, 0.2, groesse.z + 0.4), farbe.darkened(0.3)) Prims.label(dach, c + Vector3(0, HOEHE + 1.6, 0), str(pl["name"]), 0.55, Color(1, 1, 0.9)) _bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": HOEHE, "teile": [dach, band], "waende": waende, "raum": raum}) ## Ein Fensterband: bei Klötzen ein Kasten, der rundum eine Handbreit ## übersteht (ein Netz statt vier); bei Häusern vier dünne Streifen außen an ## den Wänden -- ein Kasten läge als Decke über dem Raum. func _fenster(parent: Node, cf: Vector3, g: Vector2, ton: Color, hohl: bool = false) -> void: var glas := ton.darkened(0.55).lerp(Color(0.35, 0.45, 0.6), 0.5) if minf(g.x, g.y) < 3.4: return if not hohl: Prims.box(parent, cf + Vector3(0, 1.75, 0), Vector3(g.x + 0.1, 0.9, g.y + 0.1), glas, false) return for d: Vector3 in [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]: var quer := absf(d.z) > 0.0 var laenge: float = (g.x if quer else g.y) - 2.4 var off := Vector3(d.x * (g.x * 0.5 + 0.05), 1.75, d.z * (g.y * 0.5 + 0.05)) Prims.box(parent, cf + off, Vector3(laenge, 0.9, 0.08) if quer else Vector3(0.08, 0.9, laenge), glas, false) ## Ein paar Möbel, damit man sieht, wofür ein Raum da ist. Gestellt wird im ## Rahmen der Tür (quer / zur Tür hin); was in der Türzone stünde, bleibt weg. func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void: var ort := key var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[ort]["dir"]] var hd: float = (groesse.z if absf(d.z) > 0.0 else groesse.x) * 0.5 var hq: float = (groesse.x if absf(d.z) > 0.0 else groesse.z) * 0.5 var w := maxf(0.5, hq - 2.0) var h := maxf(0.5, hd - 2.0) if key.begins_with("heim_"): # an der Türwand, links und rechts der Tür _moebel(parent, ort, c, hq - 1.35, hd - 1.6, 0.35, Vector3(1.9, 0.7, 2.4), Color(0.85, 0.85, 0.90)) if _moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 0.75, Vector3(1.5, 0.15, 2.2), farbe.darkened(0.5)) != null: _moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 1.05, Vector3(0.8, 0.55, 0.1), Color(0.2, 0.25, 0.3)) elif key == "cafe": for i in range(3): _moebel(parent, ort, c, -w * 0.5 + float(i) * w * 0.5, 0.4, 0.4, Vector3(1.4, 0.8, 1.4), Color(0.55, 0.40, 0.30), true) _moebel(parent, ort, c, -hq * 0.5, -(h * 0.7), 0.55, Vector3(hq - 1.5, 1.1, 0.8), Color(0.45, 0.32, 0.24)) elif key == "buero": for i in range(3): _moebel(parent, ort, c, -w * 0.5 + float(i) * w * 0.5, h * 0.3, 0.75, Vector3(1.6, 0.15, 1.2), Color(0.75, 0.75, 0.78)) elif key == "baeckerei": _moebel(parent, ort, c, -hq * 0.45, hd - 3.4, 0.55, Vector3(hq - 1.2, 1.1, 1.0), Color(0.70, 0.55, 0.35)) _moebel(parent, ort, c, -(hq - 1.6), -(h * 0.6), 0.85, Vector3(2.6, 1.7, 1.2), Color(0.4, 0.4, 0.42)) elif key == "uni": for i in range(3): _moebel(parent, ort, c, -hq * 0.4, -h * 0.6 + float(i) * h * 0.6, 0.5, Vector3(hq - 1.5, 0.2, 0.7), Color(0.66, 0.62, 0.58)) _moebel(parent, ort, c, -hq * 0.4, -h, 1.1, Vector3(hq * 0.8, 1.6, 0.15), Color(0.15, 0.25, 0.20)) elif key == "werkstatt": _moebel(parent, ort, c, -hq * 0.4, 0.0, 0.6, Vector3(hq * 0.8 - 1.0, 1.2, 1.4), Color(0.52, 0.48, 0.46)) ## Spuren: was von den Ursachen eines Menschen in seiner Wohnung herumliegt. ## Je Ursache eine, an den Seitenwänden hinter Bett und Tisch, ansprechbar mit E. func _spuren(parent: Node, key: String, c: Vector3, groesse: Vector3, wer: Dictionary) -> void: var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[key]["dir"]] var hd: float = (groesse.z if absf(d.z) > 0.0 else groesse.x) * 0.5 var hq: float = (groesse.x if absf(d.z) > 0.0 else groesse.z) * 0.5 var plaetze: Array = [[hq - 0.7, hd - 3.4], [-(hq - 0.7), hd - 3.4], [hq - 0.7, hd - 4.4], [-(hq - 0.7), hd - 4.4], [hq - 0.7, hd - 5.4], [-(hq - 0.7), hd - 5.4], [0.0, hd - 3.9]] var formen: Dictionary = { "anfang": [Vector3(0.28, 0.08, 0.5), Color(0.25, 0.28, 0.36)], "zeit": [Vector3(0.08, 0.6, 0.7), Color(0.92, 0.88, 0.78)], "kraft": [Vector3(0.3, 0.28, 0.55), Color(0.75, 0.55, 0.35)], "halt": [Vector3(0.32, 0.18, 0.42), Color(0.9, 0.86, 0.74)], "grund": [Vector3(0.48, 0.28, 0.75), Color(0.45, 0.35, 0.28)], } var naechster := 0 for u in wer.get("ursachen", []): var us := str(u) if not formen.has(us): continue var mass: Vector3 = formen[us][0] var farbe: Color = formen[us][1] while naechster < plaetze.size(): var pl: Array = plaetze[naechster] naechster += 1 var y: float = 1.5 if us == "zeit" else mass.y * 0.5 var stueck := _moebel(parent, key, c, float(pl[0]), float(pl[1]), y, mass, farbe, false, 0.9, true) if stueck == null: continue var marke := Node3D.new() stueck.add_child(marke) marke.add_to_group("interactable") marke.set_meta("kind", "spur") marke.set_meta("arg", us) marke.set_meta("human", str(wer["id"])) marke.set_meta("titel", str(Ursachen.LISTE[us]["spur"])) break ## Ein Möbelstück: aq quer zur Tür, ad zur Tür hin (positiv = zur Tür), Maße ## als (quer, hoch, tief). Steht es im Weg, wird es nicht gestellt (null). func _moebel(parent: Node, ort: String, c: Vector3, aq: float, ad: float, y: float, mass: Vector3, farbe: Color, rund: bool = false, glanz: float = 0.0, klein: bool = false) -> Node3D: var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[ort]["dir"]] var quer := Vector3(-d.z, 0, d.x) var pos := c + quer * aq + d * ad pos.y = c.y + y var s: Array = HumanDB.PLACES[ort]["size"] var hd: float = (float(s[1]) if absf(d.z) > 0.0 else float(s[0])) * 0.5 # Türzone: 2,2 tief hinter der Tür, 4 breit -- Kleinkram darf näher heran var rand := 1.7 if klein else 2.0 if ad + mass.z * 0.5 > hd - 2.2 and absf(aq) - mass.x * 0.5 < rand: return null var welt_mass := mass if absf(d.z) > 0.0 else Vector3(mass.z, mass.y, mass.x) if rund: return Prims.cyl(parent, pos, mass.x * 0.5, mass.y, farbe, true, glanz) return Prims.box(parent, pos, welt_mass, farbe, true, glanz) ## Der mittlere Block bleibt frei: Rasen, Teich, Bäume, Bänke. Der Park ist ## der einzige Ort im Bezirk, an dem nicht gebaut wird. func _park() -> void: var rng := RandomNumberGenerator.new() rng.seed = 3 var halb: float = (float(HumanDB.ACHSEN[2]) - float(HumanDB.ACHSEN[1])) * 0.5 \ - HumanDB.ARTERIE * 0.5 Prims.box(self, Vector3(0, 0.01, 0), Vector3(halb * 2.0, 0.05, halb * 2.0), Color(0.40, 0.58, 0.34), false) Prims.cyl(self, Vector3(-halb * 0.4, 0.06, halb * 0.25), halb * 0.28, 0.1, Color(0.30, 0.50, 0.72), false) for i in range(26): var p := Vector3(rng.randf_range(-halb, halb), 0, rng.randf_range(-halb, halb)) if p.distance_to(Vector3(-halb * 0.4, 0, halb * 0.25)) < halb * 0.38: continue if p.distance_to(Vector3(0, 0, 12.0)) < 6.0: continue var hoehe := rng.randf_range(2.6, 5.0) Prims.cyl(self, p + Vector3(0, hoehe * 0.5, 0), 0.3, hoehe, Color(0.35, 0.26, 0.18)) Prims.sphere(self, p + Vector3(0, hoehe + 0.9, 0), rng.randf_range(1.3, 2.2), Color(0.24, 0.48 + rng.randf() * 0.22, 0.22)) for i in range(5): var a := TAU * float(i) / 5.0 + 0.4 var p := Vector3(cos(a) * halb * 0.62, 0.45, sin(a) * halb * 0.62) var b := Prims.box(self, p, Vector3(2.4, 0.15, 0.7), Color(0.55, 0.40, 0.28)) b.rotation.y = -a Prims.label(self, Vector3(0, 5.0, halb - 3.0), "Stadtpark", 0.6, Color(1, 1, 0.9)) func _saeule() -> void: var pos := Vector3(0, 0, 12.0) Prims.cyl(self, pos + Vector3(0, 0.2, 0), 2.6, 0.4, Color(0.50, 0.48, 0.55), false) Prims.cyl(self, pos + Vector3(0, 2.2, 0), 0.7, 4.0, Color(0.55, 0.80, 1.0), false, 1.6) Prims.sphere(self, pos + Vector3(0, 4.6, 0), 0.9, Color(0.75, 0.92, 1.0), 2.2) Prims.label(self, pos + Vector3(0, 6.0, 0), "Rückkehr in die Traumwelt", 0.4, Color(0.75, 0.95, 1)) var n := Node3D.new() n.position = pos + Vector3(0, 1.6, 0) add_child(n) n.add_to_group("interactable") n.set_meta("kind", "traum") n.set_meta("arg", "") n.set_meta("titel", "Rückkehr in die Traumwelt") func _menschen() -> void: for i in range(HumanDB.LIST.size()): var m := Bewohner.new() add_child(m) m.setup(HumanDB.LIST[i], i, HumanDB.PLACES) m.nav_map = nav_map humans.append(m) for i in range(HumanDB.PASSANTEN_LISTE.size()): var pz := Bewohner.new() add_child(pz) pz.setup(HumanDB.PASSANTEN_LISTE[i], i + 7, HumanDB.PLACES) pz.nav_map = nav_map passanten.append(pz) ## Nur die aktive Welt stellt den Himmel; zwei WorldEnvironments vertragen sich nicht. func aktiv_setzen(an: bool) -> void: _we.environment = _env if an else null func _process(_delta: float) -> void: _himmel_aktualisieren() if spieler != null: Prims.schnitt(_bauten, spieler.global_position, spieler.cam.global_position) func _himmel_aktualisieren() -> void: var t: float = Game.time_h var sonne := clampf(sin((t - 6.0) / 12.0 * PI), 0.0, 1.0) _sun.rotation_degrees = Vector3(-((t - 6.0) / 12.0) * 180.0, -35.0, 0.0) _sun.light_energy = 0.25 + sonne * 1.0 _sun.light_color = Color(1.0, 0.75 + sonne * 0.25, 0.55 + sonne * 0.4) var tag_top := Color(0.30, 0.55, 0.90) var nacht_top := Color(0.03, 0.04, 0.12) var tag_hor := Color(0.75, 0.85, 0.95) var nacht_hor := Color(0.10, 0.10, 0.25) _sky.sky_top_color = nacht_top.lerp(tag_top, sonne) _sky.sky_horizon_color = nacht_hor.lerp(tag_hor, sonne) _sky.ground_horizon_color = nacht_hor.lerp(Color(0.55, 0.60, 0.55), sonne) _sky.ground_bottom_color = nacht_top.lerp(Color(0.30, 0.38, 0.28), sonne) # nachts mondblau statt schwarz: die Stadt bleibt lesbar, die Lichter setzen Akzente _env.ambient_light_color = Color(0.30, 0.34, 0.52).lerp(Color(0.62, 0.66, 0.74), sonne) _env.ambient_light_energy = 1.0 - sonne * 0.35 # tags übernimmt die Sonne, sonst wäscht es aus _env.fog_light_color = nacht_hor.lerp(tag_hor, sonne) var nacht := 1.0 - sonne for l in _nachtlichter: l.light_energy = clampf(nacht * 2.4 - 0.3, 0.0, 2.2)