This commit is contained in:
Team3
2026-09-16 20:06:38 +02:00
parent 7c4edaa439
commit a86d737d39
10 changed files with 252 additions and 96 deletions

View File

@@ -188,13 +188,13 @@ nicht immer — Übung entscheidet.
damit die Straßen nicht leer sind. damit die Straßen nicht leer sind.
* **Die Stadt ist einstöckig.** Wohnhäuser und Sonderbauten sind geschlossene * **Die Stadt ist einstöckig.** Wohnhäuser und Sonderbauten sind geschlossene
Häuser: ein Raum mit Tür, Fenstern, Möbeln und den Spuren seines Bewohners. Häuser: ein Raum mit Tür, Fenstern, Möbeln und den Spuren seines Bewohners.
Alles andere steht als Klotz da, unterschiedlich hoch, damit die Stadt eine Alles andere steht als flacher Klotz da, ebenfalls nur Erdgeschoss.
Silhouette hat.
* Du bist ein unsichtbarer Geist, der eine Handbreit über dem Boden schwebt, * Du bist ein unsichtbarer Geist, der eine Handbreit über dem Boden schwebt,
sich aber bewegt wie ein Mensch: Wände halten, Türen sind die Wege, sich aber bewegt wie ein Mensch: Wände halten, Türen sind die Wege,
Schwerkraft gilt. Bist du in einem Haus, verschwinden Dach und die Wände zur Schwerkraft gilt. Bist du in einem Haus, verschwinden Dach und Schild, und
Kamera hin, wie bei den Sims. Ein Haus, das zwischen Kamera und dir steht, alle Wände werden zu kniehohen Stümpfen gekürzt, wie bei den Sims. Nichts
wird durchsichtig. wird durchsichtig; bei drei Meter hohen Häusern verdeckt kein Nachbar den
Blick.
* **F** oder ein Klick hängt dich an einen Menschen: du gehst seiner Spur * **F** oder ein Klick hängt dich an einen Menschen: du gehst seiner Spur
nach, durch dieselben Türen und Treppen, so schnell wie nötig — auch bei nach, durch dieselben Türen und Treppen, so schnell wie nötig — auch bei
Tempo 3, wo du ihm zu Fuß nie folgen könntest. Steht er weit weg, planst du Tempo 3, wo du ihm zu Fuß nie folgen könntest. Steht er weit weg, planst du

View File

@@ -22,4 +22,8 @@ window/stretch/aspect="expand"
[rendering] [rendering]
anti_aliasing/quality/msaa_3d=2 anti_aliasing/quality/msaa_3d=0
anti_aliasing/quality/screen_space_aa=1
scaling_3d/scale=0.75
lights_and_shadows/directional_shadow/size=2048
lights_and_shadows/directional_shadow/soft_shadow_filter_quality=1

View File

@@ -30,6 +30,10 @@ var nav_map: RID # auf der Erde: Pfade vom Navigationsnetz
var nav_fehl := 0 # wie oft das Netz keinen Pfad wusste (Rückfall aufs Wegenetz) var nav_fehl := 0 # wie oft das Netz keinen Pfad wusste (Rückfall aufs Wegenetz)
var nav_fehl_info := "" var nav_fehl_info := ""
var _bob: float = 0.0 var _bob: float = 0.0
var _takt := 0.0 # Schild und Haltungsziele alle 0,3 s, nicht je Bild
var _neigung_ziel := 0.0
var _groesse_ziel := 1.0
var _zappelt := false
var _offset: Vector3 var _offset: Vector3
@@ -127,19 +131,12 @@ func _hut(kopf: Vector3, c: Color) -> void:
## Angst duckt, Erschöpfung beugt, Unruhe zappelt -- solange die Klemme nicht ## Angst duckt, Erschöpfung beugt, Unruhe zappelt -- solange die Klemme nicht
## überwunden ist. So sieht man Probleme beim Beobachten am Körper. ## überwunden ist. So sieht man Probleme beim Beobachten am Körper.
func _haltung(delta: float, geht: bool) -> void: func _haltung(delta: float, geht: bool) -> void:
var id := str(data["id"]) if _takt <= 0.0:
var offen: Dictionary = {} _haltungsziele()
for k in data.get("probleme", []): var zappeln := sin(Time.get_ticks_msec() * 0.011) * 0.06 if _zappelt and not geht else 0.0
if not Game.problem_ueberwunden(id, str(k)): _ober.rotation.x = lerpf(_ober.rotation.x, -_neigung_ziel, 1.0 - pow(0.02, delta))
offen[str(k)] = true
var neigung := 0.38 if offen.has("erschoepfung") else 0.0
var groesse := 0.86 if offen.has("angst") else 1.0
var zappeln := 0.0
if offen.has("unruhe") and not geht:
zappeln = sin(Time.get_ticks_msec() * 0.011) * 0.06
_ober.rotation.x = lerpf(_ober.rotation.x, -neigung, 1.0 - pow(0.02, delta))
_ober.rotation.z = lerpf(_ober.rotation.z, zappeln, 1.0 - pow(0.001, delta)) _ober.rotation.z = lerpf(_ober.rotation.z, zappeln, 1.0 - pow(0.001, delta))
_ober.scale = _ober.scale.lerp(Vector3.ONE * groesse, 1.0 - pow(0.02, delta)) _ober.scale = _ober.scale.lerp(Vector3.ONE * _groesse_ziel, 1.0 - pow(0.02, delta))
var schwung := sin(_bob) * 0.7 if geht else 0.0 var schwung := sin(_bob) * 0.7 if geht else 0.0
for i in range(_arme.size()): for i in range(_arme.size()):
var a: Node3D = _arme[i] var a: Node3D = _arme[i]
@@ -147,6 +144,17 @@ func _haltung(delta: float, geht: bool) -> void:
1.0 - pow(0.001, delta)) 1.0 - pow(0.001, delta))
func _haltungsziele() -> void:
var id := str(data["id"])
var offen: Dictionary = {}
for k in data.get("probleme", []):
if not Game.problem_ueberwunden(id, str(k)):
offen[str(k)] = true
_neigung_ziel = 0.38 if offen.has("erschoepfung") else 0.0
_groesse_ziel = 0.86 if offen.has("angst") else 1.0
_zappelt = offen.has("unruhe")
## [Ort, Tätigkeit, Slotnummer] zur Stunde h. Steht in dem Slot ein System, ## [Ort, Tätigkeit, Slotnummer] zur Stunde h. Steht in dem Slot ein System,
## ist die Tätigkeit die Ersatzhandlung. ## ist die Tätigkeit die Ersatzhandlung.
func _routine_at(h: float) -> Array: func _routine_at(h: float) -> Array:
@@ -217,11 +225,15 @@ func _process(roh: float) -> void:
_aura.scale.x = 1.0 + sin(Time.get_ticks_msec() * 0.003) * 0.08 _aura.scale.x = 1.0 + sin(Time.get_ticks_msec() * 0.003) * 0.08
_aura.scale.z = _aura.scale.x _aura.scale.z = _aura.scale.x
_label.text = beschriftung()
# Auf der Erde tragen nur Fokus, Verfolgte, Mission und wer unter der Maus # Auf der Erde tragen nur Fokus, Verfolgte, Mission und wer unter der Maus
# steht ein Schild -- sonst überdecken sich acht Namen im Café. # steht ein Schild -- sonst überdecken sich acht Namen im Café.
_label.visible = (not mit_mission and not bool(data.get("stumm", false))) \ _label.visible = (not mit_mission and not bool(data.get("stumm", false))) \
or Game.markiert.has(str(data["id"])) or Game.markiert.has(str(data["id"]))
if _takt <= 0.0:
_takt = 0.3
if _label.visible:
_label.text = beschriftung()
_takt -= delta
_label.modulate = Color(0.55, 0.85, 0.6) if erledigt else ( _label.modulate = Color(0.55, 0.85, 0.6) if erledigt else (
Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1)) Color(1, 0.9, 0.6) if aktiv else Color(1, 1, 1))

View File

@@ -91,7 +91,8 @@ func _environment() -> void:
sun.light_energy = 1.3 sun.light_energy = 1.3
sun.light_color = Color(1.0, 0.84, 0.74) sun.light_color = Color(1.0, 0.84, 0.74)
sun.shadow_enabled = true sun.shadow_enabled = true
sun.shadow_blur = 2.5 sun.shadow_blur = 1.2
sun.directional_shadow_max_distance = 110.0
add_child(sun) add_child(sun)
@@ -245,9 +246,9 @@ func _haeuser() -> void:
var waende: Array = Prims.room(self, c, groesse, farbe, farbe.darkened(0.35), var waende: Array = Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
Orte.DIR_VEC[pl["dir"]], 3.4, true) Orte.DIR_VEC[pl["dir"]], 3.4, true)
var dach := Prims.dach(self, c, groesse, farbe) var dach := Prims.dach(self, c, groesse, farbe)
var schild := Prims.label(self, c + Vector3(0, groesse.y + 4.2, 0), pl["name"], 0.6, Color(1, 0.95, 0.85))
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": groesse.y + 3.0, _bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": groesse.y + 3.0,
"teile": [dach], "waende": waende}) "teile": [dach, schild], "waende": waende})
Prims.label(self, c + Vector3(0, groesse.y + 4.2, 0), pl["name"], 0.6, Color(1, 0.95, 0.85))
_einrichtung(c, groesse, farbe, key) _einrichtung(c, groesse, farbe, key)
var licht := OmniLight3D.new() var licht := OmniLight3D.new()

View File

@@ -7,7 +7,7 @@ var humans: Array = []
var passanten: Array = [] # Statisten ohne Geschichte var passanten: Array = [] # Statisten ohne Geschichte
var spieler: Node3D = null # setzt main, für den Etagenschnitt var spieler: Node3D = null # setzt main, für den Etagenschnitt
var nav_map: RID # Navigationsnetz: Straßen, Türen, Möbel, Klötze var nav_map: RID # Navigationsnetz: Straßen, Türen, Möbel, Klötze
const HOEHE := 3.0 # Raumhöhe; Klötze sind Vielfache davon const HOEHE := 3.0 # Raumhöhe -- alles in der Stadt ist Erdgeschoss
const NAV_ZELLE_HOCH := 0.1 const NAV_ZELLE_HOCH := 0.1
var _bauten: Array = [] # alles, was der Etagenschnitt aufschneidet var _bauten: Array = [] # alles, was der Etagenschnitt aufschneidet
@@ -66,9 +66,8 @@ func _environment() -> void:
_sky = ProceduralSkyMaterial.new() _sky = ProceduralSkyMaterial.new()
sky.sky_material = _sky sky.sky_material = _sky
_env.sky = sky _env.sky = sky
_env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY _env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR # der Nachthimmel ist zu dunkel als Lichtquelle
_env.glow_enabled = true _env.glow_enabled = false # Tageslicht braucht kein Leuchten; kostet ein Fünftel der Bildrate
_env.glow_intensity = 0.35
_env.fog_enabled = true _env.fog_enabled = true
_env.fog_density = 0.0012 _env.fog_density = 0.0012
we.environment = _env we.environment = _env
@@ -76,7 +75,8 @@ func _environment() -> void:
_sun = DirectionalLight3D.new() _sun = DirectionalLight3D.new()
_sun.shadow_enabled = true _sun.shadow_enabled = true
_sun.shadow_blur = 1.5 _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) add_child(_sun)
@@ -132,17 +132,16 @@ func _bebauung() -> void:
var g: Vector2 = p["size"] var g: Vector2 = p["size"]
if minf(g.x, g.y) < 3.0: if minf(g.x, g.y) < 3.0:
continue continue
var n := 1 + rng.randi_range(0, 3) var hoehe := HOEHE # nur Erdgeschoss, wie alles in der Stadt
var hoehe := HOEHE * float(n) # acht Töne statt achtzig: gleiche Farbe heißt gleiches Material
var ton := Color.from_hsv(fmod(0.07 + rng.randf() * 0.12, 1.0), var ton := Color.from_hsv(0.07 + float(rng.randi_range(0, 3)) * 0.04,
0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26) 0.10 + float(rng.randi_range(0, 1)) * 0.12, 0.62 + float(rng.randi_range(0, 3)) * 0.08)
var bau := Node3D.new() var bau := Node3D.new()
add_child(bau) add_child(bau)
var klotz := Prims.box(bau, c + Vector3(0, hoehe * 0.5, 0), Vector3(g.x, hoehe, g.y), ton, false) 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 # 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)) Prims._add_box_body(klotz, Vector3(g.x + 0.9, hoehe, g.y + 0.9))
for f in range(n): _fenster(bau, c, g, ton)
_fenster(bau, c + Vector3(0, HOEHE * float(f), 0), g, ton)
Prims.box(bau, c + Vector3(0, hoehe + 0.2, 0), 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)) Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25))
if rng.randf() < 0.35: if rng.randf() < 0.35:
@@ -167,7 +166,9 @@ func _orte() -> void:
add_child(raum) add_child(raum)
var waende: Array = Prims.room(raum, c, groesse, farbe, farbe.darkened(0.35), var waende: Array = Prims.room(raum, c, groesse, farbe, farbe.darkened(0.35),
Orte.DIR_VEC[pl["dir"]], tuer, true) Orte.DIR_VEC[pl["dir"]], tuer, true)
_fenster(raum, c, Vector2(groesse.x, groesse.z), farbe) 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) _einrichtung(raum, c, groesse, farbe, key)
var wer := HumanDB.bewohner_von(key) var wer := HumanDB.bewohner_von(key)
if not wer.is_empty(): if not wer.is_empty():
@@ -183,19 +184,24 @@ func _orte() -> void:
add_child(dach) add_child(dach)
Prims.box(dach, c + Vector3(0, HOEHE + 0.1, 0), 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)) Vector3(groesse.x + 0.4, 0.2, groesse.z + 0.4), farbe.darkened(0.3))
Prims.label(self, c + Vector3(0, HOEHE + 1.6, 0), str(pl["name"]), 0.55, Color(1, 1, 0.9)) 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, _bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": HOEHE,
"teile": [dach], "waende": waende}) "teile": [dach, band], "waende": waende, "raum": raum})
## Ein Fensterband je Wand, damit man die Stockwerke von außen zählt. ## Ein Fensterband: bei Klötzen ein Kasten, der rundum eine Handbreit
func _fenster(parent: Node, cf: Vector3, g: Vector2, ton: Color) -> void: ## ü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) 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]: for d: Vector3 in [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]:
var quer := absf(d.z) > 0.0 var quer := absf(d.z) > 0.0
var laenge: float = (g.x if quer else g.y) - 2.4 var laenge: float = (g.x if quer else g.y) - 2.4
if laenge < 1.0:
continue
var off := Vector3(d.x * (g.x * 0.5 + 0.05), 1.75, d.z * (g.y * 0.5 + 0.05)) 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), Prims.box(parent, cf + off, Vector3(laenge, 0.9, 0.08) if quer else Vector3(0.08, 0.9, laenge),
glas, false) glas, false)
@@ -390,7 +396,9 @@ func _himmel_aktualisieren() -> void:
_sky.sky_horizon_color = nacht_hor.lerp(tag_hor, 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_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) _sky.ground_bottom_color = nacht_top.lerp(Color(0.30, 0.38, 0.28), sonne)
_env.ambient_light_energy = 0.45 + sonne * 0.8 # 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) _env.fog_light_color = nacht_hor.lerp(tag_hor, sonne)
var nacht := 1.0 - sonne var nacht := 1.0 - sonne

View File

@@ -15,6 +15,8 @@ var _erde_version := -1
var auf_erde := false var auf_erde := false
var _fokus: Node3D = null var _fokus: Node3D = null
var _fokus_mensch: Node3D = null var _fokus_mensch: Node3D = null
var _interaktive: Array = [] # Ansprechbares der aktiven Welt, einmal gesammelt
var _fokus_takt := 0.0 # Fokus, Markierung und Hinweis alle 0,05 s
func _ready() -> void: func _ready() -> void:
@@ -119,6 +121,8 @@ func _aktivieren(neu: Node3D) -> void:
w.set("spieler", player if an else null) w.set("spieler", player if an else null)
w.call("aktiv_setzen", an) w.call("aktiv_setzen", an)
welt = neu welt = neu
_interaktive = []
_fokus_takt = 0.0
Game.teleport_lock = 1.0 Game.teleport_lock = 1.0
_fokus = null _fokus = null
_fokus_mensch = null _fokus_mensch = null
@@ -134,6 +138,10 @@ func _process(delta: float) -> void:
return return
Game.notiz_sperre = maxf(0.0, Game.notiz_sperre - delta) Game.notiz_sperre = maxf(0.0, Game.notiz_sperre - delta)
Game.situationen_ticken(dt, auf_erde and not Game.schlaeft_ziel()) Game.situationen_ticken(dt, auf_erde and not Game.schlaeft_ziel())
_fokus_takt -= delta
if _fokus_takt > 0.0:
return
_fokus_takt = 0.05
_fokus_suchen() _fokus_suchen()
_markieren() _markieren()
_dabeisein() _dabeisein()
@@ -177,9 +185,13 @@ func _fokus_suchen() -> void:
_fokus_mensch = null _fokus_mensch = null
var p := player.global_position var p := player.global_position
var best := REICHWEITE_OBJEKT var best := REICHWEITE_OBJEKT
for n in get_tree().get_nodes_in_group("interactable"): if _interaktive.is_empty():
if not welt.is_ancestor_of(n): for n in get_tree().get_nodes_in_group("interactable"):
continue # die andere Welt liegt im selben Raum if welt.is_ancestor_of(n): # die andere Welt liegt im selben Raum
_interaktive.append(n)
for n in _interaktive:
if not is_instance_valid(n):
continue
var d: float = p.distance_to((n as Node3D).global_position) var d: float = p.distance_to((n as Node3D).global_position)
if d < best: if d < best:
best = d best = d

View File

@@ -68,16 +68,18 @@ func _ready() -> void:
# Schleier: oben schmal, unten weit, halb durchsichtig # Schleier: oben schmal, unten weit, halb durchsichtig
_schleier = Prims.cyl(body, Vector3(0, 1.05, 0), 0.6, 1.7, _farbe, false) _schleier = Prims.cyl(body, Vector3(0, 1.05, 0), 0.6, 1.7, _farbe, false)
(_schleier.mesh as CylinderMesh).top_radius = 0.24 (_schleier.mesh as CylinderMesh).top_radius = 0.24
_schleier_mat = Prims.mat(Color(_farbe, 0.55), 0.35) _schleier_mat = Prims.mat(Color(_farbe, 0.55), 0.35).duplicate()
_schleier.material_override = _schleier_mat _schleier.material_override = _schleier_mat
_schleier.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _schleier.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
# Lichtkern im Schleier # Lichtkern im Schleier
_kern = Prims.sphere(body, Vector3(0, 1.15, 0), 0.26, _farbe, 2.2) _kern = Prims.sphere(body, Vector3(0, 1.15, 0), 0.26, _farbe, 2.2)
_kern_mat = _kern.material_override as StandardMaterial3D _kern_mat = (_kern.material_override as StandardMaterial3D).duplicate()
_kern.material_override = _kern_mat
_kern.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _kern.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
_kopf = Prims.sphere(body, Vector3(0, 2.0, 0), 0.3, _farbe.lightened(0.4), 0.6) _kopf = Prims.sphere(body, Vector3(0, 2.0, 0), 0.3, _farbe.lightened(0.4), 0.6)
_kopf.name = "Kopf" _kopf.name = "Kopf"
_kopf_mat = _kopf.material_override as StandardMaterial3D _kopf_mat = (_kopf.material_override as StandardMaterial3D).duplicate()
_kopf.material_override = _kopf_mat
for sx: float in [-1.0, 1.0]: for sx: float in [-1.0, 1.0]:
var auge := Prims.box(body, Vector3(sx * 0.11, 2.04, -0.27), Vector3(0.07, 0.11, 0.05), var auge := Prims.box(body, Vector3(sx * 0.11, 2.04, -0.27), Vector3(0.07, 0.11, 0.05),
Color(1, 1, 1), false, 3.0) Color(1, 1, 1), false, 3.0)

View File

@@ -1,7 +1,14 @@
class_name Prims class_name Prims
## Kleine Helfer, um die Welt aus Godot-Primitiven zusammenzusetzen. ## Kleine Helfer, um die Welt aus Godot-Primitiven zusammenzusetzen.
## Gleiche Farbe, gleiches Material: tausend Klötze brauchen keine tausend
## Materialien. Wer ein Material verändern will, dupliziert es sich.
static var _materialien: Dictionary = {}
static func mat(c: Color, emis: float = 0.0, rough: float = 0.85) -> StandardMaterial3D: static func mat(c: Color, emis: float = 0.0, rough: float = 0.85) -> StandardMaterial3D:
var key := "%s|%.2f|%.2f" % [str(c), emis, rough]
if _materialien.has(key):
return _materialien[key]
var m := StandardMaterial3D.new() var m := StandardMaterial3D.new()
m.albedo_color = c m.albedo_color = c
m.roughness = rough m.roughness = rough
@@ -12,6 +19,7 @@ static func mat(c: Color, emis: float = 0.0, rough: float = 0.85) -> StandardMat
m.emission_energy_multiplier = emis m.emission_energy_multiplier = emis
if c.a < 1.0: if c.a < 1.0:
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
_materialien[key] = m
return m return m
@@ -159,35 +167,44 @@ static func _wand(mi: MeshInstance3D, normale: Vector3) -> MeshInstance3D:
return mi return mi
const STUMPF := 0.6 # so hoch bleiben die Wände, wenn man drinnen steht
## Schnitt wie bei den Sims: steht der Spieler in einem Bau, verschwinden Dach ## Schnitt wie bei den Sims: steht der Spieler in einem Bau, verschwinden Dach
## (bei Klötzen der ganze Klotz) und die beiden Wände, die zur Kamera zeigen. ## und Schild, und alle Wände des Hauses werden zu kniehohen Stümpfen gekürzt.
## Ein Bau: {c, size, hoch, teile, waende}. Ein Bau zwischen Kamera und ## Nichts wird durchsichtig. Ein Bau: {c, size, hoch, teile, waende}.
## Spieler wird durchsichtig, sonst verschwände der Spieler hinter jedem Nachbarhaus. static func schnitt(bauten: Array, spieler: Vector3, _kamera: Vector3) -> void:
static func schnitt(bauten: Array, spieler: Vector3, kamera: Vector3) -> void:
for b in bauten: for b in bauten:
var c: Vector3 = b["c"] var c: Vector3 = b["c"]
var g: Vector2 = b["size"] var g: Vector2 = b["size"]
var drin: bool = absf(spieler.x - c.x) < g.x * 0.5 + 0.3 \ var drin: bool = absf(spieler.x - c.x) < g.x * 0.5 + 0.3 \
and absf(spieler.z - c.z) < g.y * 0.5 + 0.3 \ and absf(spieler.z - c.z) < g.y * 0.5 + 0.3 \
and spieler.y > c.y - 0.5 and spieler.y < c.y + float(b["hoch"]) + 0.5 and spieler.y > c.y - 0.5 and spieler.y < c.y + float(b["hoch"]) + 0.5
var kasten := AABB(Vector3(c.x - g.x * 0.5, c.y, c.z - g.y * 0.5), if drin == bool(b.get("drin", false)):
Vector3(g.x, float(b["hoch"]), g.y)) continue # nur anfassen, wenn sich etwas ändert
var im_weg: bool = not drin \ b["drin"] = drin
and kasten.intersects_segment(kamera, spieler + Vector3(0, 0.8, 0)) != null
if bool(b.get("im_weg", false)) != im_weg:
b["im_weg"] = im_weg
if not b.has("meshes"):
b["meshes"] = _meshes_von(b)
for mi in b["meshes"]:
(mi as GeometryInstance3D).transparency = 0.82 if im_weg else 0.0
(mi as GeometryInstance3D).cast_shadow = \
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF if im_weg \
else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
for t in b["teile"]: for t in b["teile"]:
(t as Node3D).visible = not drin (t as Node3D).visible = not drin
for w in b["waende"]: for w in b["waende"]:
var n: Vector3 = w.get_meta("normal") _stumpf(w as MeshInstance3D, c.y, drin)
(w as Node3D).visible = not (drin and n.dot(kamera - c) > 0.0)
## Eine Wand kürzen oder wiederherstellen. Was ganz oben sitzt (der Türsturz),
## verschwindet drinnen ganz.
static func _stumpf(w: MeshInstance3D, boden: float, kurz: bool) -> void:
var bm := w.mesh as BoxMesh
if bm == null:
w.visible = not kurz
return
if not w.has_meta("voll"):
w.set_meta("voll", Vector2(bm.size.y, w.position.y))
var voll: Vector2 = w.get_meta("voll")
var unten: float = voll.y - voll.x * 0.5 - boden
if unten > 0.3:
w.visible = not kurz
return
w.visible = true
bm.size.y = STUMPF if kurz else voll.x
w.position.y = (boden + STUMPF * 0.5) if kurz else voll.y
## Ein Satteldach: ein geschlossenes Prisma über den Wänden. Der First läuft ## Ein Satteldach: ein geschlossenes Prisma über den Wänden. Der First läuft

View File

@@ -144,9 +144,13 @@ func _baue_hud() -> void:
var steuerung := Label.new() var steuerung := Label.new()
steuerung.text = "J Journal · 1-3 Tempo · Q Kamera · Mausrad Zoom · Esc" steuerung.text = "J Journal · 1-3 Tempo · Q Kamera · Mausrad Zoom · Esc"
# rechts verankert und nach links wachsend: sonst ragt der Text aus dem Fenster
steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT) steuerung.set_anchors_preset(Control.PRESET_TOP_RIGHT)
steuerung.position = Vector2(-260, 16) steuerung.grow_horizontal = Control.GROW_DIRECTION_BEGIN
steuerung.size = Vector2(240, 24) steuerung.offset_right = -16
steuerung.offset_left = -520
steuerung.offset_top = 16
steuerung.offset_bottom = 40
steuerung.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT steuerung.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
steuerung.add_theme_font_size_override("font_size", 14) steuerung.add_theme_font_size_override("font_size", 14)
steuerung.add_theme_color_override("font_color", Color(1, 1, 1, 0.4)) steuerung.add_theme_color_override("font_color", Color(1, 1, 1, 0.4))
@@ -397,7 +401,10 @@ func _baue_wirte() -> void:
_titel.add_theme_color_override("font_color", Color(1, 0.93, 0.75)) _titel.add_theme_color_override("font_color", Color(1, 0.93, 0.75))
_box.add_child(_titel) _box.add_child(_titel)
_scroll = ScrollContainer.new() _scroll = ScrollContainer.new()
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED # nie querscrollen, aber auch nie die Leiste verbreitern: zu breiter Inhalt
# wird abgeschnitten statt aus dem Fenster zu ragen
_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_SHOW_NEVER
_scroll.clip_contents = true
_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL _scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
_box.add_child(_scroll) _box.add_child(_scroll)
_content = VBoxContainer.new() _content = VBoxContainer.new()
@@ -411,6 +418,7 @@ func _baue_wirte() -> void:
_drawer.offset_right = -12 _drawer.offset_right = -12
_drawer.offset_top = 56 _drawer.offset_top = 56
_drawer.offset_bottom = -(LEISTE_H + 22) _drawer.offset_bottom = -(LEISTE_H + 22)
_drawer.clip_contents = true
_drawer.visible = false _drawer.visible = false
_root.add_child(_drawer) _root.add_child(_drawer)
@@ -435,9 +443,20 @@ func _wirt_fuer(kind: String) -> PanelContainer:
return _modal return _modal
func _process(_delta: float) -> void: var _takt := 0.0 # HUD- und Leistentexte alle 0,1 s; der Puls läuft je Bild
func _process(delta: float) -> void:
_takt -= delta
var dran := _takt <= 0.0
if dran:
_takt = 0.1
if not offen: if not offen:
_hud_aktualisieren() if dran:
_hud_aktualisieren()
_ablauf_aktualisieren()
else:
_ablauf_pulsen()
return return
var wirt := _wirt_fuer(_kind) var wirt := _wirt_fuer(_kind)
var wunsch := _content.get_combined_minimum_size().y var wunsch := _content.get_combined_minimum_size().y
@@ -461,7 +480,21 @@ func _process(_delta: float) -> void:
_karte_host.reset_size() _karte_host.reset_size()
_karte_host.position = Vector2((vp.x - _karte_host.size.x) * 0.5, _karte_host.position = Vector2((vp.x - _karte_host.size.x) * 0.5,
vp.y - LEISTE_H - 22.0 - _karte_host.size.y) vp.y - LEISTE_H - 22.0 - _karte_host.size.y)
_ablauf_aktualisieren() if dran:
_ablauf_aktualisieren()
else:
_ablauf_pulsen()
## Nur die laufende Stunde pulsiert -- das darf jedes Bild passieren.
func _ablauf_pulsen() -> void:
var id := Game.active_mission
if id == "" or not _ablauf.visible or not Game.situation_bei(id):
return
var jetzt := int(Game.time_h)
var puls := 0.5 + 0.5 * sin(Time.get_ticks_msec() * 0.008)
var zelle: ColorRect = _zellen[jetzt]
zelle.color = _ablauf_farbe(id, Game.slot_bei_stunde(id, jetzt)).lightened(0.3 + 0.4 * puls)
# ------------------------------------------------------------------ Baukasten # ------------------------------------------------------------------ Baukasten
@@ -549,6 +582,9 @@ func _trenner() -> void:
func _zeigen(kind: String, zurueck: Callable = Callable()) -> void: func _zeigen(kind: String, zurueck: Callable = Callable()) -> void:
_kind = kind _kind = kind
_ziel = null _ziel = null
if kind != "akte":
_drawer.remove_theme_stylebox_override("panel")
_titel.remove_theme_color_override("font_color")
var wirt := _wirt_fuer(kind) var wirt := _wirt_fuer(kind)
if _box.get_parent() != wirt: if _box.get_parent() != wirt:
if _box.get_parent() != null: if _box.get_parent() != null:
@@ -726,6 +762,7 @@ func akte_panel(human_id: String) -> void:
_akte_text(str(h["problem"]), 15) _akte_text(str(h["problem"]), 15)
_akte_abschnitt("Befund") _akte_abschnitt("Befund")
var offen_urs := 0
for u in Game.ursachen_von(human_id): for u in Game.ursachen_von(human_id):
var name: String = str(u) var name: String = str(u)
var stand: String var stand: String
@@ -736,10 +773,12 @@ func akte_panel(human_id: String) -> void:
stand = "[color=%s]%s · bremst auf %d %%[/color]" % [TINTE_ROT, woher, stand = "[color=%s]%s · bremst auf %d %%[/color]" % [TINTE_ROT, woher,
int(float(Ursachen.LISTE[name]["faktor"]) * 100.0)] int(float(Ursachen.LISTE[name]["faktor"]) * 100.0)]
else: else:
_akte_text("[color=%s]? noch nichts gefunden — Spuren in der Wohnung, Momente im Tag[/color]" % offen_urs += 1
TINTE_HELL, 15)
continue continue
_akte_text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15) _akte_text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15)
if offen_urs > 0:
_akte_text("[color=%s]? %s noch nichts gefunden — Spuren in der Wohnung, Momente im Tag[/color]" %
[TINTE_HELL, "eine Ursache," if offen_urs == 1 else "%d Ursachen," % offen_urs], 15)
var probleme: Array = [] var probleme: Array = []
for pr in Game.probleme_von(human_id): for pr in Game.probleme_von(human_id):
var key := str(pr) var key := str(pr)
@@ -768,19 +807,17 @@ func akte_panel(human_id: String) -> void:
_scroll_frames = 4 _scroll_frames = 4
## Ein Blatt Papier im Drawer; alles Weitere der Akte landet darauf. ## Die Leiste selbst wird zum Blatt Papier. Ein Container im Container bricht
## den Text nicht um -- er richtet sich an der längsten Zeile aus und ragt hinaus.
func _papier() -> void: func _papier() -> void:
var p := PanelContainer.new() var s := _sb(C_PAPIER, 4, Color(0.78, 0.72, 0.6), 1)
var s := _sb(C_PAPIER, 3, Color(0.78, 0.72, 0.6), 1) s.content_margin_left = 18
s.content_margin_left = 16 s.content_margin_right = 18
s.content_margin_right = 16 s.content_margin_top = 10
s.content_margin_top = 12
s.content_margin_bottom = 14 s.content_margin_bottom = 14
p.add_theme_stylebox_override("panel", s) _drawer.add_theme_stylebox_override("panel", s)
_content.add_child(p) _titel.add_theme_color_override("font_color", Color(0.17, 0.15, 0.13))
_blatt = VBoxContainer.new() _blatt = _content
_blatt.add_theme_constant_override("separation", 6)
p.add_child(_blatt)
func _akte_text(bb: String, groesse: int = 15) -> RichTextLabel: func _akte_text(bb: String, groesse: int = 15) -> RichTextLabel:

View File

@@ -891,6 +891,39 @@ func test_akte() -> void:
p(text.contains("noch keine") and text.contains("noch nichts gefunden"), p(text.contains("noch keine") and text.contains("noch nichts gefunden"),
"Ohne Spuren und Beobachtungen ist der Befund leer") "Ohne Spuren und Beobachtungen ist der Befund leer")
p(_knopf_da("Hinfahren"), "Ein Knopf bringt einen vor die Tür") p(_knopf_da("Hinfahren"), "Ein Knopf bringt einen vor die Tür")
await get_tree().process_frame
await get_tree().process_frame
var breit := 0.0
for c in _alle_kinder(ui()._content):
if c is Control:
breit = maxf(breit, (c as Control).size.x)
p(ui()._drawer.size.x <= UI.DRAWER_BREITE + 1.0 and breit <= ui()._content.size.x + 1.0,
"Nichts ragt aus der Leiste (Leiste %.0f, breitestes Element %.0f)" % [ui()._drawer.size.x, breit])
ui().journal_panel("techniken")
p(not ui()._drawer.has_theme_stylebox_override("panel"), "Andere Seiten sind wieder dunkel")
# keine Seite der Leiste darf breiter sein als die Leiste -- und die Leiste nicht als das Fenster
var zu_breit: Array = []
for seite in ["techniken", "missionen"]:
ui().journal_panel(seite)
await get_tree().process_frame
var mind := ui()._content.get_combined_minimum_size().x
if mind > UI.DRAWER_BREITE - 30.0 or ui()._drawer.size.x > UI.DRAWER_BREITE + 1.0:
zu_breit.append("%s (%.0f)" % [seite, mind])
ui().akte_panel(id)
await get_tree().process_frame
if ui()._content.get_combined_minimum_size().x > UI.DRAWER_BREITE - 30.0 \
or ui()._drawer.size.x > UI.DRAWER_BREITE + 1.0:
zu_breit.append("akte (%.0f)" % ui()._content.get_combined_minimum_size().x)
p(zu_breit.is_empty(), "Journal und Akte bleiben in der Leistenbreite%s" %
("" if zu_breit.is_empty() else " — zu breit: " + ", ".join(zu_breit)))
var rand: float = ui()._drawer.position.x + ui()._drawer.size.x
p(rand <= ui()._root.size.x, "Die Leiste endet im Fenster (%.0f von %.0f)" % [rand, ui()._root.size.x])
var hinweis_rechts := 0.0
for c in ui()._root.get_children():
if c is Label and (c as Label).text.begins_with("J Journal"):
hinweis_rechts = (c as Control).position.x + (c as Control).size.x
p(hinweis_rechts > 0.0 and hinweis_rechts <= ui()._root.size.x,
"Der Steuerungshinweis endet im Fenster (%.0f von %.0f)" % [hinweis_rechts, ui()._root.size.x])
var u := str((h["ursachen"] as Array)[0]) var u := str((h["ursachen"] as Array)[0])
Game.spur_lesen(id, u) Game.spur_lesen(id, u)
ui().akte_panel(id) ui().akte_panel(id)
@@ -1286,34 +1319,64 @@ func test_kamera_und_etagen() -> void:
p(zuhause and absf(oben.global_position.y) < 0.3, p(zuhause and absf(oben.global_position.y) < 0.3,
"Nachts liegt %s zu Hause, ebenerdig (y = %.1f)" % [oben.data["name"], oben.global_position.y]) "Nachts liegt %s zu Hause, ebenerdig (y = %.1f)" % [oben.data["name"], oben.global_position.y])
# Schnitt: drinnen verschwinden Dach und die Wände zur Kamera # Schnitt wie bei den Sims: drinnen verschwindet das Dach, alle Wände werden Stümpfe
var bau: Dictionary = {} var bau: Dictionary = {}
for b in welt()._bauten: for b in welt()._bauten:
if (b["c"] as Vector3).distance_to(c) < 0.1: if (b["c"] as Vector3).distance_to(c) < 0.1:
bau = b bau = b
p(not bau.is_empty() and (bau["teile"] as Array).size() == 1 and (bau["waende"] as Array).size() >= 4, p(not bau.is_empty() and (bau["teile"] as Array).size() >= 1 and (bau["waende"] as Array).size() >= 4,
"Sein Haus steht mit Dach und Wänden da") "Sein Haus steht mit Dach und Wänden da")
sp.global_position = c + Vector3(0, 0.3, 0) sp.global_position = c + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO sp.velocity = Vector3.ZERO
sp.yaw = Player.CAM_YAW sp.yaw = Player.CAM_YAW
await get_tree().physics_frame await get_tree().physics_frame
welt()._process(0.05) welt()._process(0.05)
var wand_weg := 0 var stuempfe := 0
var hoch := 0
for w in bau["waende"]: for w in bau["waende"]:
if not (w as Node3D).visible: var bm := (w as MeshInstance3D).mesh as BoxMesh
wand_weg += 1 if (w as Node3D).visible and bm.size.y <= Prims.STUMPF + 0.01:
stuempfe += 1
elif (w as Node3D).visible:
hoch += 1
p(not (bau["teile"][0] as Node3D).visible, "Steht der Geist im Haus, verschwindet das Dach") p(not (bau["teile"][0] as Node3D).visible, "Steht der Geist im Haus, verschwindet das Dach")
p(wand_weg >= 2 and wand_weg < (bau["waende"] as Array).size(), p(stuempfe >= 4 and hoch == 0,
"Die Wände zur Kamera hin sind weg, die hinteren bleiben (%d von %d Teile)" % "Alle Wände sind kniehohe Stümpfe, keine bleibt hoch, nichts wird durchsichtig (%d Stümpfe)" % stuempfe)
[wand_weg, (bau["waende"] as Array).size()]) var durchsichtig := 0
for b in welt()._bauten:
for mi in Prims._meshes_von(b):
if (mi as GeometryInstance3D).transparency > 0.0:
durchsichtig += 1
p(durchsichtig == 0, "Kein Bau ist durchsichtig")
# nichts über Kniehöhe verdeckt drinnen den Blick von oben: kein Band, keine Decke
var hoch_sichtbar: Array = []
var knoten: Array = (bau["teile"] as Array).duplicate()
knoten.append(bau["raum"])
for k in knoten:
for mi in (k as Node).find_children("*", "MeshInstance3D", true, false):
var m := mi as MeshInstance3D
if not m.is_visible_in_tree():
continue
var kasten := m.get_aabb()
var kante: float = m.global_position.y + kasten.end.y * m.scale.y
var ist_wand := (bau["waende"] as Array).has(m)
if kante > 1.9 and not ist_wand:
hoch_sichtbar.append("%s (bis %.1f m)" % [m.name, kante])
p(hoch_sichtbar.is_empty(), "Drinnen verdeckt nichts über Kniehöhe die Sicht%s" %
("" if hoch_sichtbar.is_empty() else " — " + ", ".join(hoch_sichtbar)))
sp.global_position = c + Vector3(float(g[0]) + 4.0, 0.3, 0) sp.global_position = c + Vector3(float(g[0]) + 4.0, 0.3, 0)
await get_tree().physics_frame await get_tree().physics_frame
welt()._process(0.05) welt()._process(0.05)
var alles := (bau["teile"][0] as Node3D).visible var alles := (bau["teile"][0] as Node3D).visible
for w in bau["waende"]: for w in bau["waende"]:
if not (w as Node3D).visible: var bm := (w as MeshInstance3D).mesh as BoxMesh
alles = false if not (w as Node3D).visible or bm.size.y <= Prims.STUMPF + 0.01:
p(alles, "Draußen steht das Haus wieder ganz") alles = false # der Türsturz ist niedrig, aber kein Stumpf
p(alles, "Draußen steht das Haus wieder ganz, Wände in voller Höhe")
var hoechster_bau := 0.0
for b in welt()._bauten:
hoechster_bau = maxf(hoechster_bau, float(b["hoch"]))
p(hoechster_bau <= 3.5, "Kein Bau ist höher als ein Erdgeschoss (%.1f m)" % hoechster_bau)
# Klick auf einen Menschen # Klick auf einen Menschen
sp.global_position = oben.global_position + Vector3(2.0, 1.4, 2.0) sp.global_position = oben.global_position + Vector3(2.0, 1.4, 2.0)