update
This commit is contained in:
10
README.md
10
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user