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.
* **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.
Alles andere steht als Klotz da, unterschiedlich hoch, damit die Stadt eine
Silhouette hat.
Alles andere steht als flacher Klotz da, ebenfalls nur Erdgeschoss.
* 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,
Schwerkraft gilt. Bist du in einem Haus, verschwinden Dach und die Wände zur
Kamera hin, wie bei den Sims. Ein Haus, das zwischen Kamera und dir steht,
wird durchsichtig.
Schwerkraft gilt. Bist du in einem Haus, verschwinden Dach und Schild, und
alle Wände werden zu kniehohen Stümpfen gekürzt, wie bei den Sims. Nichts
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
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

View File

@@ -22,4 +22,8 @@ window/stretch/aspect="expand"
[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_info := ""
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
@@ -127,19 +131,12 @@ func _hut(kopf: Vector3, c: Color) -> void:
## Angst duckt, Erschöpfung beugt, Unruhe zappelt -- solange die Klemme nicht
## überwunden ist. So sieht man Probleme beim Beobachten am Körper.
func _haltung(delta: float, geht: bool) -> 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
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))
if _takt <= 0.0:
_haltungsziele()
var zappeln := sin(Time.get_ticks_msec() * 0.011) * 0.06 if _zappelt and not geht else 0.0
_ober.rotation.x = lerpf(_ober.rotation.x, -_neigung_ziel, 1.0 - pow(0.02, 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
for i in range(_arme.size()):
var a: Node3D = _arme[i]
@@ -147,6 +144,17 @@ func _haltung(delta: float, geht: bool) -> void:
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,
## ist die Tätigkeit die Ersatzhandlung.
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.z = _aura.scale.x
_label.text = beschriftung()
# Auf der Erde tragen nur Fokus, Verfolgte, Mission und wer unter der Maus
# steht ein Schild -- sonst überdecken sich acht Namen im Café.
_label.visible = (not mit_mission and not bool(data.get("stumm", false))) \
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 (
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_color = Color(1.0, 0.84, 0.74)
sun.shadow_enabled = true
sun.shadow_blur = 2.5
sun.shadow_blur = 1.2
sun.directional_shadow_max_distance = 110.0
add_child(sun)
@@ -245,9 +246,9 @@ func _haeuser() -> void:
var waende: Array = Prims.room(self, c, groesse, farbe, farbe.darkened(0.35),
Orte.DIR_VEC[pl["dir"]], 3.4, true)
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,
"teile": [dach], "waende": waende})
Prims.label(self, c + Vector3(0, groesse.y + 4.2, 0), pl["name"], 0.6, Color(1, 0.95, 0.85))
"teile": [dach, schild], "waende": waende})
_einrichtung(c, groesse, farbe, key)
var licht := OmniLight3D.new()

View File

@@ -7,7 +7,7 @@ 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; Klötze sind Vielfache davon
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
@@ -66,9 +66,8 @@ func _environment() -> void:
_sky = ProceduralSkyMaterial.new()
sky.sky_material = _sky
_env.sky = sky
_env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
_env.glow_enabled = true
_env.glow_intensity = 0.35
_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
@@ -76,7 +75,8 @@ func _environment() -> void:
_sun = DirectionalLight3D.new()
_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)
@@ -132,17 +132,16 @@ func _bebauung() -> void:
var g: Vector2 = p["size"]
if minf(g.x, g.y) < 3.0:
continue
var n := 1 + rng.randi_range(0, 3)
var hoehe := HOEHE * float(n)
var ton := Color.from_hsv(fmod(0.07 + rng.randf() * 0.12, 1.0),
0.10 + rng.randf() * 0.14, 0.62 + rng.randf() * 0.26)
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))
for f in range(n):
_fenster(bau, c + Vector3(0, HOEHE * float(f), 0), g, ton)
_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:
@@ -167,7 +166,9 @@ func _orte() -> void:
add_child(raum)
var waende: Array = Prims.room(raum, c, groesse, farbe, farbe.darkened(0.35),
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)
var wer := HumanDB.bewohner_von(key)
if not wer.is_empty():
@@ -183,19 +184,24 @@ func _orte() -> void:
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(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,
"teile": [dach], "waende": waende})
"teile": [dach, band], "waende": waende, "raum": raum})
## Ein Fensterband je Wand, damit man die Stockwerke von außen zählt.
func _fenster(parent: Node, cf: Vector3, g: Vector2, ton: Color) -> void:
## 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
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))
Prims.box(parent, cf + off, Vector3(laenge, 0.9, 0.08) if quer else Vector3(0.08, 0.9, laenge),
glas, false)
@@ -390,7 +396,9 @@ func _himmel_aktualisieren() -> void:
_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)
_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)
var nacht := 1.0 - sonne

View File

@@ -15,6 +15,8 @@ var _erde_version := -1
var auf_erde := false
var _fokus: 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:
@@ -119,6 +121,8 @@ func _aktivieren(neu: Node3D) -> void:
w.set("spieler", player if an else null)
w.call("aktiv_setzen", an)
welt = neu
_interaktive = []
_fokus_takt = 0.0
Game.teleport_lock = 1.0
_fokus = null
_fokus_mensch = null
@@ -134,6 +138,10 @@ func _process(delta: float) -> void:
return
Game.notiz_sperre = maxf(0.0, Game.notiz_sperre - delta)
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()
_markieren()
_dabeisein()
@@ -177,9 +185,13 @@ func _fokus_suchen() -> void:
_fokus_mensch = null
var p := player.global_position
var best := REICHWEITE_OBJEKT
for n in get_tree().get_nodes_in_group("interactable"):
if not welt.is_ancestor_of(n):
continue # die andere Welt liegt im selben Raum
if _interaktive.is_empty():
for n in get_tree().get_nodes_in_group("interactable"):
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)
if d < best:
best = d

View File

@@ -68,16 +68,18 @@ func _ready() -> void:
# Schleier: oben schmal, unten weit, halb durchsichtig
_schleier = Prims.cyl(body, Vector3(0, 1.05, 0), 0.6, 1.7, _farbe, false)
(_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.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
# Lichtkern im Schleier
_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
_kopf = Prims.sphere(body, Vector3(0, 2.0, 0), 0.3, _farbe.lightened(0.4), 0.6)
_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]:
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)

View File

@@ -1,7 +1,14 @@
class_name Prims
## 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:
var key := "%s|%.2f|%.2f" % [str(c), emis, rough]
if _materialien.has(key):
return _materialien[key]
var m := StandardMaterial3D.new()
m.albedo_color = c
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
if c.a < 1.0:
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
_materialien[key] = m
return m
@@ -159,35 +167,44 @@ static func _wand(mi: MeshInstance3D, normale: Vector3) -> MeshInstance3D:
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
## (bei Klötzen der ganze Klotz) und die beiden Wände, die zur Kamera zeigen.
## Ein Bau: {c, size, hoch, teile, waende}. Ein Bau zwischen Kamera und
## Spieler wird durchsichtig, sonst verschwände der Spieler hinter jedem Nachbarhaus.
static func schnitt(bauten: Array, spieler: Vector3, kamera: Vector3) -> void:
## und Schild, und alle Wände des Hauses werden zu kniehohen Stümpfen gekürzt.
## Nichts wird durchsichtig. Ein Bau: {c, size, hoch, teile, waende}.
static func schnitt(bauten: Array, spieler: Vector3, _kamera: Vector3) -> void:
for b in bauten:
var c: Vector3 = b["c"]
var g: Vector2 = b["size"]
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 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),
Vector3(g.x, float(b["hoch"]), g.y))
var im_weg: bool = not 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
if drin == bool(b.get("drin", false)):
continue # nur anfassen, wenn sich etwas ändert
b["drin"] = drin
for t in b["teile"]:
(t as Node3D).visible = not drin
for w in b["waende"]:
var n: Vector3 = w.get_meta("normal")
(w as Node3D).visible = not (drin and n.dot(kamera - c) > 0.0)
_stumpf(w as MeshInstance3D, c.y, drin)
## 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

View File

@@ -144,9 +144,13 @@ func _baue_hud() -> void:
var steuerung := Label.new()
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.position = Vector2(-260, 16)
steuerung.size = Vector2(240, 24)
steuerung.grow_horizontal = Control.GROW_DIRECTION_BEGIN
steuerung.offset_right = -16
steuerung.offset_left = -520
steuerung.offset_top = 16
steuerung.offset_bottom = 40
steuerung.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
steuerung.add_theme_font_size_override("font_size", 14)
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))
_box.add_child(_titel)
_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
_box.add_child(_scroll)
_content = VBoxContainer.new()
@@ -411,6 +418,7 @@ func _baue_wirte() -> void:
_drawer.offset_right = -12
_drawer.offset_top = 56
_drawer.offset_bottom = -(LEISTE_H + 22)
_drawer.clip_contents = true
_drawer.visible = false
_root.add_child(_drawer)
@@ -435,9 +443,20 @@ func _wirt_fuer(kind: String) -> PanelContainer:
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:
_hud_aktualisieren()
if dran:
_hud_aktualisieren()
_ablauf_aktualisieren()
else:
_ablauf_pulsen()
return
var wirt := _wirt_fuer(_kind)
var wunsch := _content.get_combined_minimum_size().y
@@ -461,7 +480,21 @@ func _process(_delta: float) -> void:
_karte_host.reset_size()
_karte_host.position = Vector2((vp.x - _karte_host.size.x) * 0.5,
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
@@ -549,6 +582,9 @@ func _trenner() -> void:
func _zeigen(kind: String, zurueck: Callable = Callable()) -> void:
_kind = kind
_ziel = null
if kind != "akte":
_drawer.remove_theme_stylebox_override("panel")
_titel.remove_theme_color_override("font_color")
var wirt := _wirt_fuer(kind)
if _box.get_parent() != wirt:
if _box.get_parent() != null:
@@ -726,6 +762,7 @@ func akte_panel(human_id: String) -> void:
_akte_text(str(h["problem"]), 15)
_akte_abschnitt("Befund")
var offen_urs := 0
for u in Game.ursachen_von(human_id):
var name: String = str(u)
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,
int(float(Ursachen.LISTE[name]["faktor"]) * 100.0)]
else:
_akte_text("[color=%s]? noch nichts gefunden — Spuren in der Wohnung, Momente im Tag[/color]" %
TINTE_HELL, 15)
offen_urs += 1
continue
_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 = []
for pr in Game.probleme_von(human_id):
var key := str(pr)
@@ -768,19 +807,17 @@ func akte_panel(human_id: String) -> void:
_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:
var p := PanelContainer.new()
var s := _sb(C_PAPIER, 3, Color(0.78, 0.72, 0.6), 1)
s.content_margin_left = 16
s.content_margin_right = 16
s.content_margin_top = 12
var s := _sb(C_PAPIER, 4, Color(0.78, 0.72, 0.6), 1)
s.content_margin_left = 18
s.content_margin_right = 18
s.content_margin_top = 10
s.content_margin_bottom = 14
p.add_theme_stylebox_override("panel", s)
_content.add_child(p)
_blatt = VBoxContainer.new()
_blatt.add_theme_constant_override("separation", 6)
p.add_child(_blatt)
_drawer.add_theme_stylebox_override("panel", s)
_titel.add_theme_color_override("font_color", Color(0.17, 0.15, 0.13))
_blatt = _content
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"),
"Ohne Spuren und Beobachtungen ist der Befund leer")
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])
Game.spur_lesen(id, u)
ui().akte_panel(id)
@@ -1286,34 +1319,64 @@ func test_kamera_und_etagen() -> void:
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])
# 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 = {}
for b in welt()._bauten:
if (b["c"] as Vector3).distance_to(c) < 0.1:
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")
sp.global_position = c + Vector3(0, 0.3, 0)
sp.velocity = Vector3.ZERO
sp.yaw = Player.CAM_YAW
await get_tree().physics_frame
welt()._process(0.05)
var wand_weg := 0
var stuempfe := 0
var hoch := 0
for w in bau["waende"]:
if not (w as Node3D).visible:
wand_weg += 1
var bm := (w as MeshInstance3D).mesh as BoxMesh
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(wand_weg >= 2 and wand_weg < (bau["waende"] as Array).size(),
"Die Wände zur Kamera hin sind weg, die hinteren bleiben (%d von %d Teile)" %
[wand_weg, (bau["waende"] as Array).size()])
p(stuempfe >= 4 and hoch == 0,
"Alle Wände sind kniehohe Stümpfe, keine bleibt hoch, nichts wird durchsichtig (%d Stümpfe)" % stuempfe)
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)
await get_tree().physics_frame
welt()._process(0.05)
var alles := (bau["teile"][0] as Node3D).visible
for w in bau["waende"]:
if not (w as Node3D).visible:
alles = false
p(alles, "Draußen steht das Haus wieder ganz")
var bm := (w as MeshInstance3D).mesh as BoxMesh
if not (w as Node3D).visible or bm.size.y <= Prims.STUMPF + 0.01:
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
sp.global_position = oben.global_position + Vector3(2.0, 1.4, 2.0)