diff --git a/README.md b/README.md index 94dfbd4..4ab2a2d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/project.godot b/project.godot index efc836a..975eb7c 100644 --- a/project.godot +++ b/project.godot @@ -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 diff --git a/scripts/bewohner.gd b/scripts/bewohner.gd index 4eb7c19..4ab6e5d 100644 --- a/scripts/bewohner.gd +++ b/scripts/bewohner.gd @@ -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)) diff --git a/scripts/dream_world.gd b/scripts/dream_world.gd index 5b8d95e..c21f5fe 100644 --- a/scripts/dream_world.gd +++ b/scripts/dream_world.gd @@ -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() diff --git a/scripts/earth_world.gd b/scripts/earth_world.gd index 1d711f4..bbd23bc 100644 --- a/scripts/earth_world.gd +++ b/scripts/earth_world.gd @@ -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 diff --git a/scripts/main.gd b/scripts/main.gd index 6f119ce..383fe01 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -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 diff --git a/scripts/player.gd b/scripts/player.gd index c311040..4f8f594 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -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) diff --git a/scripts/prims.gd b/scripts/prims.gd index afc431b..9eb3511 100644 --- a/scripts/prims.gd +++ b/scripts/prims.gd @@ -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 diff --git a/scripts/ui.gd b/scripts/ui.gd index d749fe7..08bda08 100644 --- a/scripts/ui.gd +++ b/scripts/ui.gd @@ -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: diff --git a/tests/verify.gd b/tests/verify.gd index b1cc49a..0dc5e27 100644 --- a/tests/verify.gd +++ b/tests/verify.gd @@ -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)