This commit is contained in:
Team3
2026-09-16 16:15:32 +02:00
parent e813b5f7aa
commit 7c4edaa439
19 changed files with 1244 additions and 636 deletions

View File

@@ -12,8 +12,6 @@ var place: String = ""
var activity: String = ""
var resist_left: float = 0.0
var places: Dictionary = {}
var netz: Netz = null
var anker: Dictionary = {}
var mit_mission := false
var _visual: Node3D
@@ -28,18 +26,17 @@ var _path: Array = []
var _letzte_aktivitaet: String = ""
var boden: Callable = Callable() # Höhe des Bodens (x, z) -> y; leer heißt eben
var slot: int = 0
var spur: Array = [] # letzte Positionen -- der Geist folgt ihr durch Türen
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 _offset: Vector3
func setup(d: Dictionary, index: int, orte: Dictionary,
wegenetz: Netz = null, wegeanker: Dictionary = {}) -> void:
func setup(d: Dictionary, index: int, orte: Dictionary) -> void:
data = d
idx = index
places = orte
netz = wegenetz
anker = wegeanker
mit_mission = bool(d.get("mission", false))
name = "Bewohner_" + str(d["id"])
var winkel := float(index) * TAU / 4.0
@@ -85,20 +82,25 @@ func setup(d: Dictionary, index: int, orte: Dictionary,
place = start[0]
activity = start[1]
_letzte_aktivitaet = activity
global_position = Orte.center(places, place) + _offset + Vector3(0, Orte.hoehe(etage(place)), 0)
global_position = Orte.center(places, place) + _offset
## Punkt auf der Spur, der mindestens `abstand` hinter dem Menschen liegt.
func spur_punkt(abstand: float) -> Vector3:
for i in range(spur.size() - 1, -1, -1):
if (spur[i] as Vector3).distance_to(global_position) >= abstand:
return spur[i]
return spur[0] if not spur.is_empty() else global_position
## In welcher Etage dieser Mensch an einem Ort ist -- 0, wenn er dort nur unten ist.
func etage(ort: String) -> int:
return int((data.get("etage", {}) as Dictionary).get(ort, 0))
## Pfad über das Navigationsnetz zu einem Ort -- leer, wenn es keins gibt.
func _nav_route(nach: String) -> Array:
if not nav_map.is_valid():
return []
var ziel := Orte.center(places, nach) + _offset
var pts := NavigationServer3D.map_get_path(nav_map, global_position, ziel, true)
if pts.size() < 2:
nav_fehl += 1
nav_fehl_info = "%s: %s -> %s von %s nach %s (%d Punkte)" % [data["name"], place, nach,
str(global_position), str(ziel), pts.size()]
print(" [kein Weg] " + nav_fehl_info)
return []
var out: Array = []
for i in range(1, pts.size()):
out.append(pts[i])
return out
## Kopfbedeckung nach Tagestyp des Berufs -- so unterscheiden sich die Leute
@@ -166,11 +168,9 @@ func _process(roh: float) -> void:
var soll := _routine_at(Game.time_h)
slot = int(soll[2])
if soll[0] != place:
var war_unterwegs := not _path.is_empty()
_path = Orte.route_voll(places, place, soll[0], _offset,
data.get("etage", {}), netz, anker, war_unterwegs)
if war_unterwegs and _path.size() > 1:
_path.remove_at(0) # steht schon draußen, alte Tür überspringen
_path = _nav_route(soll[0])
if _path.is_empty(): # kein Netz oder kein Weg: Luftlinie, damit er ankommt
_path = [Orte.center(places, soll[0]) + _offset]
place = soll[0]
activity = "unterwegs"
elif _path.is_empty() and activity != soll[1]:
@@ -200,10 +200,6 @@ func _process(roh: float) -> void:
_haltung(delta, not _path.is_empty())
if boden.is_valid():
global_position.y = float(boden.call(global_position.x, global_position.z))
if spur.is_empty() or (spur.back() as Vector3).distance_to(global_position) > 0.4:
spur.append(global_position)
if spur.size() > 120:
spur.pop_front()
_visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0,
1.0 - pow(0.01, delta))
@@ -224,7 +220,8 @@ func _process(roh: float) -> void:
_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 or Game.markiert.has(str(data["id"]))
_label.visible = (not mit_mission and not bool(data.get("stumm", false))) \
or Game.markiert.has(str(data["id"]))
_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

@@ -254,7 +254,15 @@ static func _platz_frei(p: Vector3, groesse: Vector2) -> bool:
var c := Orte.center(PLACES, key)
var dx: float = absf(p.x - c.x) - (groesse.x + bx) * 0.5
var dz: float = absf(p.z - c.z) - (groesse.y + bz) * 0.5
if dx < HAUS_ABSTAND and dz < HAUS_ABSTAND:
# Zwei Terrassen verschiedener Höhe brauchen Platz für eine gehbare Böschung
# dazwischen -- sonst steht vor der Tür eine Klippe, und niemand kommt hinauf.
# Randstreifen beider plus eine Böschung dazwischen; über 4 m Höhenunterschied
# wird die Böschung zu steil (40°), dann muss der Abstand mitwachsen.
var dh := absf(hang(p.length()) - terrasse(key))
var luft := HAUS_ABSTAND
if dh > 0.6:
luft += TERRASSE_RAND + BOESCHUNG + maxf(0.0, dh - 4.0) * 1.5
if dx < luft and dz < luft:
return false
for k in NETZ.kanten:
if Netz.trifft_rechteck(NETZ.knoten[k[0]], NETZ.knoten[k[1]], p, groesse, WEG_ABSTAND):
@@ -511,10 +519,16 @@ static func _bewohner(i: int) -> Dictionary:
var ablauf: Array = []
for e in ABLAEUFE[i % ABLAEUFE.size()]:
ablauf.append([e[0], haus if e[1] == "haus" else e[1], e[2]])
var von := str(Game.completed[i]) if i < Game.completed.size() else ""
var mensch := HumanDB.by_id(von)
var rolle := _rolle(i)
if not mensch.is_empty():
rolle += " · aus %s Traum" % _besitz(str(mensch["name"]).get_slice(" ", 0))
return {
"id": "wesen_%d" % i,
"name": _name(i),
"rolle": _rolle(i),
"rolle": rolle,
"von": von,
"haus": haus,
"leuchtet": true,
"farbe": Color.from_hsv(fmod(0.72 + float(i) * 0.161, 1.0), 0.55, 0.95),

View File

@@ -7,10 +7,18 @@ class_name DreamWorld
var spawn := Vector3(0, 2.0, 20.0)
var bewohner: Array = []
var spieler: Node3D = null # setzt main, für den Etagenschnitt
var nav_map: RID # Navigationsnetz über Insel, Wege und Häuser
var _bauten: Array = []
var _lichter: Array = []
var _freihalten: Array[Vector3] = []
var _we: WorldEnvironment
var _env: Environment
## Nur die aktive Welt stellt den Himmel; zwei WorldEnvironments vertragen sich nicht.
func aktiv_setzen(an: bool) -> void:
_we.environment = _env if an else null
func _ready() -> void:
@@ -23,13 +31,39 @@ func _ready() -> void:
_lichtung()
_tor_zur_erde()
_gruen()
_navigation()
_wesen()
## Navigationsnetz aus den Kollisionsformen: Insel, Häuser, Bäume, Laternen.
func _navigation() -> void:
var region := NavigationRegion3D.new()
add_child(region)
var nm := NavigationMesh.new()
nm.geometry_parsed_geometry_type = NavigationMesh.PARSED_GEOMETRY_STATIC_COLLIDERS
nm.agent_radius = 0.4
nm.agent_height = 1.6
nm.agent_max_climb = 0.35
nm.agent_max_slope = 40.0
nm.cell_height = 0.1
var quelle := NavigationMeshSourceGeometryData3D.new()
NavigationServer3D.parse_source_geometry_data(nm, quelle, self)
NavigationServer3D.bake_from_source_geometry_data(nm, quelle)
nav_map = NavigationServer3D.map_create()
NavigationServer3D.map_set_active(nav_map, true)
NavigationServer3D.map_set_cell_height(nav_map, 0.1)
region.set_navigation_map(nav_map)
region.navigation_mesh = nm
NavigationServer3D.map_set_use_async_iterations(nav_map, false)
NavigationServer3D.map_force_update(nav_map)
## Dämmerung: tiefes Blau oben, warmes Lavendel am Horizont, Nebel im Tal.
func _environment() -> void:
var we := WorldEnvironment.new()
var env := Environment.new()
_we = WorldEnvironment.new()
var we := _we
_env = Environment.new()
var env := _env
env.background_mode = Environment.BG_SKY
var sky := Sky.new()
var mat := ProceduralSkyMaterial.new()
@@ -182,7 +216,7 @@ func _wege() -> void:
for k in DreamDB.NETZ.knoten:
var p: Vector3 = k
p.y = DreamDB.hoehe_bei(p.x, p.z)
Prims.cyl(self, p + Vector3(0, 0.04, 0), 1.3, 0.06, farbe, false)
Prims.cyl(self, p + Vector3(0, 0.05, 0), 1.3, 0.06, farbe, false) # knapp über den Bändern
if nr % 2 == 0 and Vector2(p.x, p.z).length() > 12.0:
_laterne(p + Vector3(1.6, 0, 0))
nr += 1
@@ -212,7 +246,7 @@ func _haeuser() -> void:
Orte.DIR_VEC[pl["dir"]], 3.4, true)
var dach := Prims.dach(self, c, groesse, farbe)
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": groesse.y + 3.0,
"etagen": [], "dach": [dach], "waende": [waende]})
"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))
_einrichtung(c, groesse, farbe, key)
@@ -463,8 +497,17 @@ func _wesen() -> void:
var w := Bewohner.new()
add_child(w)
w.boden = Callable(DreamDB, "hoehe_bei")
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES, DreamDB.NETZ, DreamDB.ANKER)
w.nav_map = nav_map
w.setup(DreamDB.LIST[i], i, DreamDB.PLACES)
bewohner.append(w)
# ansprechbar: ein Wesen flüstert, was in der Wohnung des nächsten Menschen liegt
var n := Node3D.new()
n.position.y = 1.0
w.add_child(n)
n.add_to_group("interactable")
n.set_meta("kind", "wesen")
n.set_meta("arg", str(i))
n.set_meta("titel", str(DreamDB.LIST[i]["name"]))
func _process(delta: float) -> void:

View File

@@ -4,12 +4,17 @@ class_name EarthWorld
var spawn := Vector3(0, 1.4, 12.0)
var humans: Array = []
var passanten: Array = [] # Statisten ohne Geschichte
var spieler: Node3D = null # setzt main, für den Etagenschnitt
var nav_map: RID # Navigationsnetz: Straßen, Türen, Möbel, Klötze
const HOEHE := 3.0 # Raumhöhe; Klötze sind Vielfache davon
const NAV_ZELLE_HOCH := 0.1
var _bauten: Array = [] # alles, was der Etagenschnitt aufschneidet
var _sun: DirectionalLight3D
var _sky: ProceduralSkyMaterial
var _env: Environment
var _we: WorldEnvironment
var _nachtlichter: Array = []
@@ -20,12 +25,41 @@ func _ready() -> void:
_orte()
_park()
_saeule()
_navigation()
_menschen()
_himmel_aktualisieren()
## Das Navigationsnetz wird aus allen Kollisionsformen gebacken: Boden,
## Straßen, Wände mit Türlücken, Möbel, Klötze. Wer darauf läuft, findet die
## Türen von allein.
func _navigation() -> void:
var region := NavigationRegion3D.new()
add_child(region)
var nm := NavigationMesh.new()
nm.geometry_parsed_geometry_type = NavigationMesh.PARSED_GEOMETRY_STATIC_COLLIDERS
nm.agent_radius = 0.4
nm.agent_height = 1.6
nm.agent_max_climb = 0.35
nm.agent_max_slope = 40.0
nm.cell_height = NAV_ZELLE_HOCH # fein, sonst schweben die Leute 25 cm über der Straße
var quelle := NavigationMeshSourceGeometryData3D.new()
NavigationServer3D.parse_source_geometry_data(nm, quelle, self)
NavigationServer3D.bake_from_source_geometry_data(nm, quelle)
# eigene Karte: die Traumwelt liegt im selben Raum, ihre Polygone dürfen hier nicht auftauchen
nav_map = NavigationServer3D.map_create()
NavigationServer3D.map_set_active(nav_map, true)
NavigationServer3D.map_set_cell_height(nav_map, NAV_ZELLE_HOCH)
region.set_navigation_map(nav_map)
region.navigation_mesh = nm
# synchron: sonst kommen die ersten Pfade leer zurück
NavigationServer3D.map_set_use_async_iterations(nav_map, false)
NavigationServer3D.map_force_update(nav_map)
func _environment() -> void:
var we := WorldEnvironment.new()
_we = WorldEnvironment.new()
var we := _we
_env = Environment.new()
_env.background_mode = Environment.BG_SKY
var sky := Sky.new()
@@ -46,9 +80,22 @@ func _environment() -> void:
add_child(_sun)
const STADTRAND := 140.0 # unsichtbare Wand: hinter dem Bezirk endet die Welt
func _boden() -> void:
Prims.box(self, Vector3(0, -0.53, 0), Vector3(300, 1, 300), Color(0.42, 0.48, 0.40))
_strassen()
# Stadtrand: wer immer weiter läuft, fiele sonst vom Rand der Welt
for d: Vector3 in [Vector3.FORWARD, Vector3.BACK, Vector3.LEFT, Vector3.RIGHT]:
var wand := StaticBody3D.new()
var cs := CollisionShape3D.new()
var form := BoxShape3D.new()
form.size = Vector3(4.0, 30.0, STADTRAND * 2.0 + 8.0) if absf(d.x) > 0.0 \
else Vector3(STADTRAND * 2.0 + 8.0, 30.0, 4.0)
cs.shape = form
wand.add_child(cs)
wand.position = d * (STADTRAND + 2.0) + Vector3(0, 15.0, 0)
add_child(wand)
## Arterien breit, roji schmal -- beide als flaches Band mit Belag.
@@ -61,7 +108,9 @@ func _strassen() -> void:
continue
var breit := float(st["breite"])
var farbe := Color(0.62, 0.60, 0.58) if breit > 5.0 else Color(0.55, 0.53, 0.52)
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, 0.02, 0),
# gestaffelt: Kreuzungen und Gasseneinfahrten flimmern sonst (Z-Fighting)
var hoch := 0.02 if breit > 5.0 and absf(d.z) > 0.0 else (0.03 if breit > 5.0 else 0.04)
var band := Prims.box(self, (a + b) * 0.5 + Vector3(0, hoch, 0),
Vector3(breit, 0.06, d.length() + breit), farbe, false)
band.rotation.y = atan2(d.x, d.z)
@@ -84,7 +133,7 @@ func _bebauung() -> void:
if minf(g.x, g.y) < 3.0:
continue
var n := 1 + rng.randi_range(0, 3)
var hoehe := Orte.hoehe(n)
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 bau := Node3D.new()
@@ -93,18 +142,17 @@ func _bebauung() -> void:
# 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, Orte.hoehe(f), 0), g, ton)
_fenster(bau, c + Vector3(0, HOEHE * float(f), 0), g, ton)
Prims.box(bau, c + Vector3(0, hoehe + 0.2, 0),
Vector3(g.x + 0.5, 0.4, g.y + 0.5), ton.darkened(0.25))
if rng.randf() < 0.35:
Prims.box(bau, c + Vector3(0, hoehe + 0.9, 0),
Vector3(g.x * 0.4, 1.0, g.y * 0.4), ton.darkened(0.1))
_bauten.append({"c": c, "size": g, "hoch": hoehe, "etagen": [bau], "dach": [],
"waende": [], "voll": true})
_bauten.append({"c": c, "size": g, "hoch": hoehe, "teile": [bau], "waende": []})
## Wohnungen und Sonderbauten: geschlossene Häuser mit Stockwerken, Treppenhaus
## und Möbeln. Der Geist gleitet hinein, der Etagenschnitt öffnet sie ihm.
## Wohnungen und Sonderbauten: geschlossene Häuser, ein Raum mit Tür, Möbeln
## und Spuren. Der Geist geht hinein, dann verschwinden Dach und Vorderwände.
func _orte() -> void:
for key in HumanDB.PLACES:
var pl: Dictionary = HumanDB.PLACES[key]
@@ -112,42 +160,32 @@ func _orte() -> void:
if float(s[0]) <= 0.0:
continue
var c := Orte.center(HumanDB.PLACES, key)
var groesse := Vector3(float(s[0]), Orte.ETAGE, float(s[1]))
var groesse := Vector3(float(s[0]), HOEHE, float(s[1]))
var farbe: Color = pl["color"]
var tuer: float = maxf(1.6, minf(3.0, minf(groesse.x, groesse.z) - 3.0))
var n := Orte.etagen(HumanDB.PLACES, key)
var etagen: Array = []
var waende: Array = []
for f in range(n):
var eb := Node3D.new()
add_child(eb)
var cf := c + Vector3(0, Orte.hoehe(f), 0)
var ton := farbe.lightened(0.06 * float(f))
waende.append(Prims.room(eb, cf, groesse, ton, farbe.darkened(0.35),
Orte.DIR_VEC[pl["dir"]] if f == 0 else Vector3.ZERO, tuer, f == 0))
if f > 0:
_decke(eb, key, cf, groesse, farbe.darkened(0.35))
_fenster(eb, cf, Vector2(groesse.x, groesse.z), ton)
_einrichtung(eb, cf, groesse, farbe, key if f == 0 or HumanDB.ETAGEN_ARBEIT.has(key)
else "heim_", key)
if f + 1 < n:
_treppe(eb, key, f)
var licht := OmniLight3D.new()
licht.position = cf + Vector3(0, 2.2, 0)
licht.omni_range = 12.0
licht.light_color = Color(1.0, 0.85, 0.6)
licht.light_energy = 0.0
eb.add_child(licht)
_nachtlichter.append(licht)
etagen.append(eb)
var raum := Node3D.new()
add_child(raum)
var waende: Array = Prims.room(raum, c, groesse, farbe, farbe.darkened(0.35),
Orte.DIR_VEC[pl["dir"]], tuer, true)
_fenster(raum, c, Vector2(groesse.x, groesse.z), farbe)
_einrichtung(raum, c, groesse, farbe, key)
var wer := HumanDB.bewohner_von(key)
if not wer.is_empty():
_spuren(raum, key, c, groesse, wer)
var licht := OmniLight3D.new()
licht.position = c + Vector3(0, 2.2, 0)
licht.omni_range = 12.0
licht.light_color = Color(1.0, 0.85, 0.6)
licht.light_energy = 0.0
raum.add_child(licht)
_nachtlichter.append(licht)
var dach := Node3D.new()
add_child(dach)
Prims.box(dach, c + Vector3(0, Orte.hoehe(n) + 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))
Prims.label(self, c + Vector3(0, Orte.hoehe(n) + 1.6, 0), str(pl["name"]), 0.55,
Color(1, 1, 0.9))
_bauten.append({"c": c, "size": Vector2(groesse.x, groesse.z), "hoch": Orte.hoehe(n),
"etagen": etagen, "dach": [dach], "waende": waende})
Prims.label(self, 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})
## Ein Fensterband je Wand, damit man die Stockwerke von außen zählt.
@@ -163,51 +201,21 @@ func _fenster(parent: Node, cf: Vector3, g: Vector2, ton: Color) -> void:
glas, false)
## Ein Lauf von Podest zu Podest -- die Treppe, die Bewohner und Geist
## hochgehen. Sie trägt, deshalb mit Kollision.
func _treppe(parent: Node, key: String, f: int) -> void:
var lauf := Orte.treppen_lauf(HumanDB.PLACES, key, f)
var von: Vector3 = lauf[0]
var bis: Vector3 = lauf[1]
var d := bis - von
var stufe := Prims.box(parent, (von + bis) * 0.5, Vector3(1.6, 0.18, d.length()),
Color(0.55, 0.50, 0.46))
stufe.basis = Basis.looking_at(d.normalized(), Vector3.UP)
var r := Orte.treppen_rahmen(HumanDB.PLACES, key)
var podest := Orte.treppe(HumanDB.PLACES, key, f + 1)
var mass := Orte._xz(r["a"], Orte.PODEST * 2.0, Orte.TREPPE_TIEFE - 0.8)
Prims.box(parent, podest + Vector3(0, -0.09, 0), Vector3(mass.x, 0.18, mass.y),
Color(0.55, 0.50, 0.46))
## Der Boden einer oberen Etage lässt über dem Treppenhaus ein Loch frei --
## sonst stieße, wer die Treppe hochkommt, mit dem Kopf an die Decke.
func _decke(parent: Node, key: String, cf: Vector3, _groesse: Vector3, farbe: Color) -> void:
for platte in Orte.treppen_decke(HumanDB.PLACES, key):
var mitte: Vector3 = platte[0]
var mass: Vector2 = platte[1]
mitte.y = cf.y - 0.1
Prims.box(parent, mitte, Vector3(mass.x, 0.2, mass.y), farbe)
## Ein paar Möbel, damit man sieht, wofür ein Raum da ist. Gestellt wird im
## Rahmen der Tür (quer / zur Tür hin); was in der Türzone, im Treppenstreifen
## oder auf dem Weg von der Tür zur Treppe stünde, bleibt weg.
func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String,
ort: String = "") -> void:
if ort == "":
ort = key
## Rahmen der Tür (quer / zur Tür hin); was in der Türzone stünde, bleibt weg.
func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key: String) -> void:
var ort := key
var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[ort]["dir"]]
var hd: float = (groesse.z if absf(d.z) > 0.0 else groesse.x) * 0.5
var hq: float = (groesse.x if absf(d.z) > 0.0 else groesse.z) * 0.5
var w := maxf(0.5, hq - 2.0)
var h := maxf(0.5, hd - 2.0)
if key.begins_with("heim_"):
# an der Türwand, links und rechts der Tür -- die Rückwand gehört der Treppe
# an der Türwand, links und rechts der Tür
_moebel(parent, ort, c, hq - 1.35, hd - 1.6, 0.35, Vector3(1.9, 0.7, 2.4),
Color(0.85, 0.85, 0.90))
if _moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 0.75, Vector3(1.5, 0.15, 2.2),
farbe.darkened(0.5)):
farbe.darkened(0.5)) != null:
_moebel(parent, ort, c, -(hq - 1.15), hd - 1.6, 1.05, Vector3(0.8, 0.55, 0.1),
Color(0.2, 0.25, 0.3))
elif key == "cafe":
@@ -236,38 +244,64 @@ func _einrichtung(parent: Node, c: Vector3, groesse: Vector3, farbe: Color, key:
Color(0.52, 0.48, 0.46))
## Spuren: was von den Ursachen eines Menschen in seiner Wohnung herumliegt.
## Je Ursache eine, an den Seitenwänden hinter Bett und Tisch, ansprechbar mit E.
func _spuren(parent: Node, key: String, c: Vector3, groesse: Vector3, wer: Dictionary) -> void:
var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[key]["dir"]]
var hd: float = (groesse.z if absf(d.z) > 0.0 else groesse.x) * 0.5
var hq: float = (groesse.x if absf(d.z) > 0.0 else groesse.z) * 0.5
var plaetze: Array = [[hq - 0.7, hd - 3.4], [-(hq - 0.7), hd - 3.4], [hq - 0.7, hd - 4.4],
[-(hq - 0.7), hd - 4.4], [hq - 0.7, hd - 5.4], [-(hq - 0.7), hd - 5.4], [0.0, hd - 3.9]]
var formen: Dictionary = {
"anfang": [Vector3(0.28, 0.08, 0.5), Color(0.25, 0.28, 0.36)],
"zeit": [Vector3(0.08, 0.6, 0.7), Color(0.92, 0.88, 0.78)],
"kraft": [Vector3(0.3, 0.28, 0.55), Color(0.75, 0.55, 0.35)],
"halt": [Vector3(0.32, 0.18, 0.42), Color(0.9, 0.86, 0.74)],
"grund": [Vector3(0.48, 0.28, 0.75), Color(0.45, 0.35, 0.28)],
}
var naechster := 0
for u in wer.get("ursachen", []):
var us := str(u)
if not formen.has(us):
continue
var mass: Vector3 = formen[us][0]
var farbe: Color = formen[us][1]
while naechster < plaetze.size():
var pl: Array = plaetze[naechster]
naechster += 1
var y: float = 1.5 if us == "zeit" else mass.y * 0.5
var stueck := _moebel(parent, key, c, float(pl[0]), float(pl[1]), y, mass, farbe, false, 0.9, true)
if stueck == null:
continue
var marke := Node3D.new()
stueck.add_child(marke)
marke.add_to_group("interactable")
marke.set_meta("kind", "spur")
marke.set_meta("arg", us)
marke.set_meta("human", str(wer["id"]))
marke.set_meta("titel", str(Ursachen.LISTE[us]["spur"]))
break
## Ein Möbelstück: aq quer zur Tür, ad zur Tür hin (positiv = zur Tür), Maße
## als (quer, hoch, tief). Steht es im Weg, wird es nicht gestellt.
## als (quer, hoch, tief). Steht es im Weg, wird es nicht gestellt (null).
func _moebel(parent: Node, ort: String, c: Vector3, aq: float, ad: float, y: float,
mass: Vector3, farbe: Color, rund: bool = false) -> bool:
mass: Vector3, farbe: Color, rund: bool = false, glanz: float = 0.0,
klein: bool = false) -> Node3D:
var d: Vector3 = Orte.DIR_VEC[HumanDB.PLACES[ort]["dir"]]
var quer := Vector3(-d.z, 0, d.x)
var pos := c + quer * aq + d * ad
pos.y = c.y + y
var s: Array = HumanDB.PLACES[ort]["size"]
var hd: float = (float(s[1]) if absf(d.z) > 0.0 else float(s[0])) * 0.5
var halb := maxf(mass.x, mass.z) * 0.5
# Türzone: 2,2 tief hinter der Tür, 4 breit
if ad + mass.z * 0.5 > hd - 2.2 and absf(aq) - mass.x * 0.5 < 2.0:
return false
# Treppenstreifen
var r := Orte.treppen_rahmen(HumanDB.PLACES, ort)
var n: Vector3 = r["n"]
var tief_n: float = (mass.z if absf(n.dot(d)) > 0.5 else mass.x) * 0.5
if (pos - c).dot(n) + tief_n > float(r["halb_n"]) - Orte.TREPPE_TIEFE - 0.3:
return false
# Weg von der Tür zur Treppe
var von := Orte.innen_tuer(HumanDB.PLACES, ort)
var bis := Orte.treppe(HumanDB.PLACES, ort, 0)
var flach := Vector3(pos.x, 0, pos.z)
if Geometry3D.get_closest_point_to_segment(flach, von, bis).distance_to(flach) < halb + 0.9:
return false
# Türzone: 2,2 tief hinter der Tür, 4 breit -- Kleinkram darf näher heran
var rand := 1.7 if klein else 2.0
if ad + mass.z * 0.5 > hd - 2.2 and absf(aq) - mass.x * 0.5 < rand:
return null
var welt_mass := mass if absf(d.z) > 0.0 else Vector3(mass.z, mass.y, mass.x)
if rund:
Prims.cyl(parent, pos, mass.x * 0.5, mass.y, farbe)
else:
Prims.box(parent, pos, welt_mass, farbe)
return true
return Prims.cyl(parent, pos, mass.x * 0.5, mass.y, farbe, true, glanz)
return Prims.box(parent, pos, welt_mass, farbe, true, glanz)
## Der mittlere Block bleibt frei: Rasen, Teich, Bäume, Bänke. Der Park ist
@@ -319,8 +353,20 @@ func _menschen() -> void:
for i in range(HumanDB.LIST.size()):
var m := Bewohner.new()
add_child(m)
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES, HumanDB.NETZ, HumanDB.ANKER)
m.setup(HumanDB.LIST[i], i, HumanDB.PLACES)
m.nav_map = nav_map
humans.append(m)
for i in range(HumanDB.PASSANTEN_LISTE.size()):
var pz := Bewohner.new()
add_child(pz)
pz.setup(HumanDB.PASSANTEN_LISTE[i], i + 7, HumanDB.PLACES)
pz.nav_map = nav_map
passanten.append(pz)
## Nur die aktive Welt stellt den Himmel; zwei WorldEnvironments vertragen sich nicht.
func aktiv_setzen(an: bool) -> void:
_we.environment = _env if an else null
func _process(_delta: float) -> void:

View File

@@ -33,6 +33,9 @@ var ueberwunden: Dictionary = {} # human_id -> {problem -> true}
var gesehen_zaehler: Dictionary = {} # "human:problem" -> wie oft dabeigewesen
var completed: Array = []
var markiert: Dictionary = {} # human_id -> true: wer gerade ein Schild trägt
var welt_version := 0 # zählt hoch, wenn Dorf oder Stadt neu erzeugt wurden
var spuren: Dictionary = {} # human_id -> {ursache -> true}: in der Wohnung gelesen
var notizen: Dictionary = {} # human_id -> [Zeilen mit Tag und Uhrzeit], für die Akte
var time_h: float = 7.0
var day: int = 1
var teleport_lock: float = 0.0
@@ -151,6 +154,7 @@ func insel_radius() -> float:
func welt_erzeugen() -> void:
DreamDB.erzeuge(haeuser())
HumanDB.erzeuge()
welt_version += 1
## Das Traumdorf beginnt leer. Mit jedem erfüllten Traum zieht ein Wesen ein.
@@ -352,11 +356,15 @@ func sehen(human_id: String, nr: int) -> void:
## Der Zustand einer Stunde. HUD und Panel zeichnen denselben -- zwei
## Auslegungen würden mit der Zeit auseinanderlaufen.
enum Stunde { UNBEKANNT, GESEHEN, SCHLAF, UNGEKLAERT, BENANNT, GEBAUT }
enum Stunde { UNBEKANNT, GESEHEN, SCHLAF, UNGEKLAERT, BENANNT, GEBAUT, VERDAECHTIG }
func slot_zustand(human_id: String, nr: int) -> int:
if not gesehen_slot(human_id, nr):
# eine gelesene Spur macht die Stunden ihrer Ursache verdächtig: dort lohnt es, dabei zu sein
var g := slot_grund(human_id, nr)
if Ursachen.ist_blocker(g) and spur_bekannt(human_id, Ursachen.ursache_von(g)):
return Stunde.VERDAECHTIG
return Stunde.UNBEKANNT
if not system_in(human_id, nr).is_empty():
return Stunde.GEBAUT
@@ -405,6 +413,7 @@ func notieren(human_id: String, nr: int) -> String:
if not Ursachen.ist_blocker(grund):
notiz_sperre = NOTIZ_SPERRE
notified.emit("Nichts schief: %s" % Orte.text(grund), Color(1, 0.75, 0.55))
notiz(human_id, "Beobachtet: %s — nichts schief" % Orte.text(grund))
return "daneben"
if erkannt(human_id, nr):
notified.emit("Schon notiert: %s" % Orte.text(grund), Color(0.8, 0.85, 1))
@@ -414,13 +423,60 @@ func notieren(human_id: String, nr: int) -> String:
erkannt_slots[human_id][nr] = true
notified.emit("Erkannt: %s — %s" % [Ursachen.BLOCKER[grund]["name"],
Ursachen.LISTE[Ursachen.ursache_von(grund)]["name"]], Color(0.8, 0.95, 1))
notiz(human_id, "Erkannt: %s (%s)" % [Ursachen.BLOCKER[grund]["name"],
Ursachen.LISTE[Ursachen.ursache_von(grund)]["name"]])
state_changed.emit()
return "treffer"
## Eine Ursache gilt als erkannt, sobald eine ihrer Tätigkeiten aufgedeckt ist.
## Nur eine Quelle -- sonst driften Slot und Ursache auseinander.
## Ein Traumwesen flüstert eine Spur des Menschen, um den es gerade geht --
## das Dorf hilft dem, der ihm seine Bewohner gebracht hat.
func wesen_fluestert(index: int) -> String:
var wesen: Dictionary = DreamDB.LIST[index] if index < DreamDB.LIST.size() else {"name": "Ein Wesen"}
var wer := active_mission
if wer == "":
var offen := offene_missionen()
wer = str(offen[0]["id"]) if not offen.is_empty() else ""
if wer == "":
return "%s lächelt nur. Nimm dir am Schwarzen Brett jemanden vor." % wesen["name"]
var mensch := HumanDB.by_id(wer)
for u in ursachen_von(wer):
if spur_lesen(wer, str(u)):
return "%s flüstert: Bei %s liegt etwas -- %s" % [wesen["name"], mensch["name"],
Ursachen.LISTE[u]["spur_text"]]
return "%s flüstert: Was bei %s herumliegt, kennst du schon. Geh hin und sieh, wann es geschieht." % \
[wesen["name"], mensch["name"]]
## Eine Zeile in die Akte: was wann festgestellt wurde.
func notiz(human_id: String, text: String) -> void:
if not notizen.has(human_id):
notizen[human_id] = []
notizen[human_id].append("Tag %d, %02d Uhr — %s" % [day, int(time_h), text])
## Eine Spur in der Wohnung lesen: die Ursache ist damit bekannt, ihre
## Stunden im Tag werden verdächtig. Liefert false, wenn sie schon bekannt war.
func spur_lesen(human_id: String, ursache: String) -> bool:
if spur_bekannt(human_id, ursache):
return false
if not spuren.has(human_id):
spuren[human_id] = {}
spuren[human_id][ursache] = true
notiz(human_id, "Spur: %s" % Ursachen.LISTE[ursache]["spur"])
state_changed.emit()
return true
func spur_bekannt(human_id: String, ursache: String) -> bool:
return spuren.get(human_id, {}).has(ursache)
func ursache_bekannt(human_id: String, ursache: String) -> bool:
if spur_bekannt(human_id, ursache):
return true
for nr in erkannt_slots.get(human_id, {}):
if Ursachen.ursache_von(slot_grund(human_id, int(nr))) == ursache:
return true
@@ -507,7 +563,8 @@ func system_setzen(human_id: String, nr: int, tech_id: String, widerstand: bool)
var guete := float(pruef["guete"]) * (1.0 + 0.1 * float(verhalten(human_id, "guete")))
systeme[human_id][nr] = {"tech": tech_id, "guete": guete}
add_xp(tech_id, 2)
notified.emit("System steht: %s um %02d:00" %
notiz(human_id, "Routine gesetzt: %s" % TechDB.by_id(tech_id)["name"])
notified.emit("Neue Routine steht: %s um %02d:00" %
[TechDB.by_id(tech_id)["name"], int(slots(human_id)[nr][0])], Color(0.6, 1, 0.7))
state_changed.emit()
return {"ok": true, "erfolg": true, "guete": float(pruef["guete"])}
@@ -699,7 +756,7 @@ func situation_anwenden(tech_id: String) -> Dictionary:
ueberwunden[id] = {}
ueberwunden[id][problem] = true
res["ueberwunden"] = true
notified.emit("%s überwunden" % Probleme.name_von(problem), Color(1, 0.85, 0.4))
notified.emit("%s — ein Schritt weiter" % Probleme.name_von(problem), Color(1, 0.85, 0.4))
state_changed.emit()
return res

View File

@@ -9,6 +9,7 @@ class_name HumanDB
## an da und leben ihren Tag.
const EINWOHNER := 16
const PASSANTEN := 12 # Leute ohne Geschichte, die die Straßen beleben
const BLOCKER_MAX := 4 # mehr Blocker, und der Tag wäre nur noch kaputt
const ACHSEN := [-72.0, -24.0, 24.0, 72.0] # Arterien in x wie in z
const ARTERIE := 7.0 # Breite einer Arterie
@@ -32,12 +33,10 @@ static var SONDER: Array = [
["baeckerei", "Bäckerei Keller", Color(0.85, 0.72, 0.50), 2],
["werkstatt", "Werkstatt Loos", Color(0.70, 0.58, 0.55), 1],
]
## Arbeitsorte, an denen die Leute auf verschiedenen Etagen sitzen. Café,
## Bäckerei und Werkstatt spielen sich unten ab.
static var ETAGEN_ARBEIT: Array = ["buero", "uni"]
static var PLACES: Dictionary = {}
static var LIST: Array = []
static var PASSANTEN_LISTE: Array = []
## Arbeitsplätze, die ein erzeugter Mensch bekommen kann.
static var ARBEIT: Array = ["uni", "cafe", "buero", "baeckerei", "werkstatt"]
@@ -200,7 +199,11 @@ static func erzeuge(_ungenutzt: int = 0) -> void:
_park_eintragen()
_sonderbauten_verteilen()
_wohnhaeuser_verteilen()
_ecken_eintragen()
_anker_setzen()
PASSANTEN_LISTE = []
for i in range(PASSANTEN):
PASSANTEN_LISTE.append(_passant(i))
var eigene := Aushaenge.laden()
var n_eigen := mini(eigene.size(), EINWOHNER)
for i in range(EINWOHNER - n_eigen):
@@ -408,6 +411,48 @@ static func _gasse_freischneiden(ab: int, start: Vector3, ende: Vector3, flanke:
PARZELLEN.append_array(neu)
## Straßenecken: die Kreuzungen des Netzes, Ziele für Passanten.
static func _ecken_eintragen() -> void:
var ecken: Array = []
for i in range(NETZ.knoten.size()):
if NETZ.nachbarn(i).size() >= 3:
ecken.append(i)
if ecken.size() < 4:
for i in range(NETZ.knoten.size()):
if not ecken.has(i):
ecken.append(i)
for k in range(mini(8, ecken.size())):
var p: Vector3 = NETZ.knoten[int(ecken[(k * 5) % ecken.size()])]
PLACES["ecke_%d" % k] = {
"name": "Straßenecke", "pos": [p.x, p.z], "size": [0.0, 0.0],
"dir": "none", "color": Color(0.5, 0.5, 0.5),
}
## Ein Passant: stündlich eine andere Ecke, dazwischen Park -- kein Name,
## kein Schild, keine Mission. Er ist da, damit die Straßen nicht leer sind.
static func _passant(i: int) -> Dictionary:
var ecken: Array = []
for key in PLACES:
if str(key).begins_with("ecke_"):
ecken.append(str(key))
var routine: Array = []
var letzte := ""
for h in range(24):
var ort := "park" if (h + i) % 7 == 3 else str(ecken[(i * 3 + h * 5) % ecken.size()])
if ort == letzte:
ort = str(ecken[(i * 3 + h * 5 + 1) % ecken.size()])
routine.append([h, ort, "unterwegs" if h % 2 == 0 else "wartet"])
letzte = ort
return {
"id": "passant_%d" % i,
"name": "Passant",
"stumm": true,
"farbe": Color.from_hsv(0.58 + float(i % 4) * 0.05, 0.12, 0.55 + float(i % 3) * 0.1),
"routine": routine,
}
# ------------------------------------------------------------ Belegung
static func _park_eintragen() -> void:
@@ -455,13 +500,12 @@ static func _sonderbauten_verteilen() -> void:
"size": [p["size"].x, p["size"].y],
"dir": p["dir"],
"color": SONDER[s][2],
"etagen": int(SONDER[s][3]),
}
## Die Wohnhäuser: gleichmäßig über den Bezirk verteilt, damit die Wege
## nicht alle in derselben Ecke enden.
const WOHNUNG_MIN := 7.0 # schmaler passt kein Treppenhaus mehr neben die Tür
const WOHNUNG_MIN := 7.0 # schmaler passt neben die Tür kein Bett mehr
static func _wohnhaeuser_verteilen() -> void:
var frei: Array = []
@@ -487,21 +531,9 @@ static func _wohnhaeuser_verteilen() -> void:
"size": [p["size"].x, p["size"].y],
"dir": p["dir"],
"color": Color.from_hsv(fmod(0.08 + float(i) * 0.113, 1.0), 0.28, 0.78),
"etagen": 2 + (i * 5 + 1) % 3,
}
## In welcher Etage jemand wohnt und arbeitet. Wohnhäuser sind Mehrfamilien-
## häuser: die eigene Wohnung liegt irgendwo im Haus.
static func etagen_fuer(i: int, heim: String, arbeit: String) -> Dictionary:
var out: Dictionary = {}
if PLACES.has(heim):
out[heim] = (i * 7 + 3) % Orte.etagen(PLACES, heim)
if PLACES.has(arbeit) and ETAGEN_ARBEIT.has(arbeit):
out[arbeit] = i % Orte.etagen(PLACES, arbeit)
return out
static func _anker_setzen() -> void:
for key in PLACES:
var s: Array = PLACES[key]["size"]
@@ -635,7 +667,6 @@ static func mensch_aus_profil(pr: Dictionary, i: int) -> Dictionary:
"eigen": true,
"mission": true,
"heim": heim,
"etage": etagen_fuer(i, heim, arbeit),
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
"routine": _tag_belegen(TAGESTYPEN[typ], heim, arbeit, bild["blocker"],
bild["ursachen"], i),
@@ -678,7 +709,6 @@ static func _mensch(i: int) -> Dictionary:
"verhalten": {},
"mission": true,
"heim": heim,
"etage": etagen_fuer(i, heim, arbeit),
"farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8),
"routine": _tagesablauf(i, heim, arbeit),
}
@@ -710,6 +740,13 @@ static func _nachname(i: int) -> String:
return str(NACHNAMEN[(i / VORNAMEN.size() + i) % NACHNAMEN.size()])
static func bewohner_von(heim: String) -> Dictionary:
for m in LIST:
if str(m.get("heim", "")) == heim:
return m
return {}
static func by_id(id: String) -> Dictionary:
for h in LIST:
if h["id"] == id:

View File

@@ -7,7 +7,11 @@ const REICHWEITE_MENSCH := 3.6
var player: Player
var ui: UI
var welt: Node3D
var welt: Node3D # die aktive Welt
var traum: DreamWorld = null # beide Welten bleiben bestehen und leben weiter
var erde: EarthWorld = null
var _traum_version := -1
var _erde_version := -1
var auf_erde := false
var _fokus: Node3D = null
var _fokus_mensch: Node3D = null
@@ -20,6 +24,7 @@ func _ready() -> void:
player.look_enabled = false)
ui.panel_closed.connect(func() -> void:
player.look_enabled = true)
ui.hinfahren.connect(hinfahren)
player = Player.new()
add_child(player)
@@ -34,28 +39,86 @@ func _ready() -> void:
func _traumwelt() -> void:
auf_erde = false
_setze_welt(DreamWorld.new())
if traum == null or _traum_version != Game.welt_version:
traum = _neu_bauen(traum, DreamWorld.new(), 1) as DreamWorld
_traum_version = Game.welt_version
_aktivieren(traum)
player.set_mode(Player.Mode.TRAUM)
player.global_position = (welt as DreamWorld).spawn
player.collision_mask = 1
player.global_position = traum.spawn
player.yaw = Player.CAM_YAW
func _erde() -> void:
auf_erde = true
_setze_welt(EarthWorld.new())
if erde == null or _erde_version != Game.welt_version:
erde = _neu_bauen(erde, EarthWorld.new(), 2) as EarthWorld
_erde_version = Game.welt_version
_aktivieren(erde)
player.set_mode(Player.Mode.GEIST)
player.global_position = (welt as EarthWorld).spawn
player.collision_mask = 2
player.global_position = erde.spawn
player.yaw = Player.CAM_YAW
if Game.active_mission != "":
zum_ziel(Game.active_mission, false) # wer mit Mission reist, landet vor der Tür seines Ziels
func _setze_welt(neu: Node3D) -> void:
if welt != null:
remove_child(welt)
welt.queue_free()
## Vor die Tür eines Menschen setzen -- steht er draußen, neben ihn. Der Fußweg
## quer durch den Bezirk war Leerlauf; das Gehen zählt in der Wohnung und beim Folgen.
func zum_ziel(id: String, melden: bool = true) -> void:
var wer: Bewohner = null
for h in erde.humans:
if str(h.data["id"]) == id:
wer = h
if wer == null:
return
var p: Vector3
var drinnen: bool = HumanDB.PLACES.has(wer.place) \
and float(HumanDB.PLACES[wer.place]["size"][0]) > 0.0 and wer._path.is_empty()
if drinnen:
p = Orte.door(HumanDB.PLACES, wer.place)
else:
p = wer.global_position + Vector3(2.0, 0, 0)
p = NavigationServer3D.map_get_closest_point(erde.nav_map, p)
player.global_position = p + Vector3(0, 0.3, 0)
player.velocity = Vector3.ZERO
player.folgt = null
Game.teleport_lock = 1.0
if melden:
Game.notified.emit("Du stehst %s %s." % ["vor der Tür von" if drinnen else "bei",
wer.data["name"]], Color(0.7, 0.9, 1))
## Knopf in der Akte: zur Erde und vor die Tür.
func hinfahren(id: String) -> void:
if not auf_erde:
_erde()
zum_ziel(id)
## Eine Welt neu bauen -- nur wenn Dorf oder Stadt sich geändert haben. Jede
## Welt liegt auf ihrer eigenen Kollisionsebene, denn beide teilen sich den Raum.
func _neu_bauen(alt: Node3D, neu: Node3D, ebene: int) -> Node3D:
if alt != null:
remove_child(alt)
alt.queue_free()
add_child(neu)
move_child(neu, 0)
for k in neu.find_children("*", "CollisionObject3D", true, false):
(k as CollisionObject3D).collision_layer = 1 << (ebene - 1)
return neu
## Die andere Welt bleibt da und lebt weiter, nur unsichtbar und ohne Himmel.
func _aktivieren(neu: Node3D) -> void:
for w in [traum, erde]:
if w == null:
continue
var an: bool = w == neu
w.visible = an
w.set("spieler", player if an else null)
w.call("aktiv_setzen", an)
welt = neu
add_child(welt)
move_child(welt, 0)
welt.set("spieler", player) # für den Etagenschnitt
Game.teleport_lock = 1.0
_fokus = null
_fokus_mensch = null
@@ -115,6 +178,8 @@ func _fokus_suchen() -> void:
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
var d: float = p.distance_to((n as Node3D).global_position)
if d < best:
best = d
@@ -123,8 +188,6 @@ func _fokus_suchen() -> void:
var bestm := REICHWEITE_MENSCH
for m in (welt as EarthWorld).humans:
var mp: Vector3 = (m as Node3D).global_position + Vector3(0, 1.0, 0)
if absf(mp.y - p.y) > 2.0:
continue # andere Etage: durch die Decke sieht man nichts
var d: float = p.distance_to(mp)
if d < bestm:
bestm = d
@@ -156,6 +219,10 @@ func _hinweis_setzen() -> void:
ui.setze_hinweis("[E] %s — Techniken lernen" % titel)
"brett":
ui.setze_hinweis("[E] Schwarzes Brett — Mission annehmen")
"spur":
ui.setze_hinweis("[E] Spur: %s" % titel)
"wesen":
ui.setze_hinweis("[E] %s — zuhören" % titel)
_:
ui.setze_hinweis("[E] %s" % titel)
else:
@@ -298,3 +365,12 @@ func _interagieren() -> void:
ui.lehre_panel(str(_fokus.get_meta("arg")))
"brett":
ui.brett_panel()
"wesen":
Game.notified.emit(Game.wesen_fluestert(int(str(_fokus.get_meta("arg")))), Color(0.85, 0.8, 1))
"spur":
var wer := str(_fokus.get_meta("human"))
var u := str(_fokus.get_meta("arg"))
var neu := Game.spur_lesen(wer, u)
Game.notified.emit("%s%s — %s" % ["" if neu else "Schon gesehen: ",
Ursachen.LISTE[u]["spur"], Ursachen.LISTE[u]["spur_text"]],
Color(1, 0.9, 0.6) if neu else Color(0.8, 0.85, 1))

View File

@@ -48,220 +48,6 @@ static func kennt(a: String) -> bool:
return ACTIVITY_TEXT.has(a) or Ursachen.ist_blocker(a)
const ETAGE := 3.0 # Höhe eines Stockwerks
static func etagen(places: Dictionary, key: String) -> int:
return maxi(1, int(places[key].get("etagen", 1)))
static func hoehe(e: int) -> float:
return float(e) * ETAGE
const TREPPE_LAUF := 6.0 # Abstand der Wendepodeste entlang der Wand
const TREPPE_TIEFE := 4.4 # Streifen an der Treppenwand: zwei Läufe nebeneinander
const PODEST := 0.9 # halbe Podestlänge
## Rahmen des Treppenhauses: an der längeren Wand, die nicht die Türwand ist.
## n zeigt von der Mitte zur Treppenwand, a läuft an ihr entlang.
static func treppen_rahmen(places: Dictionary, key: String) -> Dictionary:
var pl: Dictionary = places[key]
var c := center(places, key)
var d: Vector3 = DIR_VEC[pl["dir"]]
var size: Array = pl["size"]
var quer_ist := absf(d.z) > 0.0
var halb_d: float = (float(size[1]) if quer_ist else float(size[0])) * 0.5
var halb_q: float = (float(size[0]) if quer_ist else float(size[1])) * 0.5
var quer := Vector3(-d.z, 0, d.x)
if halb_q >= halb_d:
return {"c": c, "n": -d, "a": quer, "halb_n": halb_d, "halb_a": halb_q}
return {"c": c, "n": -quer, "a": -d, "halb_n": halb_q, "halb_a": halb_d}
static func _lauf(r: Dictionary) -> float:
return minf(TREPPE_LAUF, float(r["halb_a"]) * 2.0 - 2.8)
static func _podest_a(r: Dictionary, e: int) -> float:
var p0: float = float(r["halb_a"]) - 1.4
return p0 if e % 2 == 0 else p0 - _lauf(r)
## Wendepodest der Etage e: zwei Läufe liegen nebeneinander, das Podest
## verbindet sie. Der obere Lauf ist so nie die Decke des unteren.
static func treppe(places: Dictionary, key: String, e: int) -> Vector3:
var r := treppen_rahmen(places, key)
var p: Vector3 = r["c"] + r["n"] * (float(r["halb_n"]) - TREPPE_TIEFE * 0.5) \
+ r["a"] * _podest_a(r, e)
p.y = hoehe(e)
return p
## Ein Lauf: [Anfang auf Etage f, Ende auf Etage f + 1].
static func treppen_lauf(places: Dictionary, key: String, f: int) -> Array:
var r := treppen_rahmen(places, key)
var tiefe: float = float(r["halb_n"]) - (1.4 if f % 2 == 0 else 3.0)
var richtung := -1.0 if f % 2 == 0 else 1.0
var von: Vector3 = r["c"] + r["n"] * tiefe + r["a"] * (_podest_a(r, f) + richtung * PODEST)
var bis: Vector3 = r["c"] + r["n"] * tiefe + r["a"] * (_podest_a(r, f + 1) - richtung * PODEST)
von.y = hoehe(f)
bis.y = hoehe(f + 1)
return [von, bis]
## Wegpunkte durchs Treppenhaus von Etage a nach Etage b, ohne a selbst:
## Laufanfang, Laufende, Podest -- je Etage.
static func treppen_weg(places: Dictionary, key: String, a: int, b: int) -> Array:
var out: Array = []
if b > a:
for f in range(a, b):
var l := treppen_lauf(places, key, f)
out.append(l[0])
out.append(l[1])
out.append(treppe(places, key, f + 1))
else:
for f in range(a - 1, b - 1, -1):
var l := treppen_lauf(places, key, f)
out.append(l[1])
out.append(l[0])
out.append(treppe(places, key, f))
return out
## Deckenplatten einer oberen Etage: alles außer dem Treppenloch.
## Liefert [[mitte, groesse_xz], ...] ohne Höhe.
static func treppen_decke(places: Dictionary, key: String) -> Array:
var r := treppen_rahmen(places, key)
var c: Vector3 = r["c"]
var n: Vector3 = r["n"]
var a: Vector3 = r["a"]
var halb_n := float(r["halb_n"])
var halb_a := float(r["halb_a"])
var out: Array = []
# Hauptplatte: alles außer dem Streifen an der Treppenwand
if halb_n * 2.0 - TREPPE_TIEFE > 0.2:
out.append([c - n * (TREPPE_TIEFE * 0.5), _xz(a, halb_a * 2.0, halb_n * 2.0 - TREPPE_TIEFE)])
# Rest des Streifens jenseits des Lochs
var ende: float = _podest_a(r, 1) - PODEST
var rest := ende + halb_a
if rest > 0.3:
out.append([c + n * (halb_n - TREPPE_TIEFE * 0.5) + a * ((ende - halb_a) * 0.5),
_xz(a, rest, TREPPE_TIEFE)])
return out
## Kastenmaß in Weltachsen: laenge_a entlang a, laenge_n quer dazu.
static func _xz(a: Vector3, laenge_a: float, laenge_n: float) -> Vector2:
return Vector2(laenge_a, laenge_n) if absf(a.x) > 0.0 else Vector2(laenge_n, laenge_a)
## Rückt einen Punkt aus dem Treppenstreifen heraus (für Möbel).
static func frei_von_treppe(places: Dictionary, key: String, pos: Vector3, halb: float) -> Vector3:
var r := treppen_rahmen(places, key)
var n: Vector3 = r["n"]
var pn: float = (pos - r["c"]).dot(n)
var grenze: float = float(r["halb_n"]) - TREPPE_TIEFE - halb - 0.2
if pn > grenze:
pos -= n * (pn - grenze)
return pos
## Punkt gleich hinter der Tür, im Haus.
static func innen_tuer(places: Dictionary, key: String) -> Vector3:
var pl: Dictionary = places[key]
var c := center(places, key)
var d: Vector3 = DIR_VEC[pl["dir"]]
var size: Array = pl["size"]
var halb: float = (float(size[1]) if absf(d.z) > 0.0 else float(size[0])) * 0.5
return c + d * (halb - 1.2)
## Ort, in dessen Grundriss der Punkt liegt -- oder "".
static func ort_bei(places: Dictionary, p: Vector3) -> String:
for key in places:
var s: Array = places[key]["size"]
if float(s[0]) <= 0.0:
continue
var c := center(places, key)
if absf(p.x - c.x) <= float(s[0]) * 0.5 and absf(p.z - c.z) <= float(s[1]) * 0.5:
return key
return ""
## Weg für einen Körper von einem freien Punkt zu einem Menschen: aus dem
## eigenen Haus heraus, über das Netz, durch seine Tür und in seine Etage.
static func route_zu(places: Dictionary, netz: Netz, von: Vector3, nach: Vector3,
nach_ort: String, nach_etage: int) -> Array:
var pts: Array = []
var start := von
var von_ort := ort_bei(places, von)
if von_ort != "" and gebaeude(places, von_ort):
var e := int(round(von.y / ETAGE))
if e > 0:
pts.append(treppe(places, von_ort, e))
pts.append_array(treppen_weg(places, von_ort, e, 0))
pts.append(innen_tuer(places, von_ort))
start = door(places, von_ort)
pts.append(start)
var drin := nach_ort != "" and gebaeude(places, nach_ort)
var ende := door(places, nach_ort) if drin else nach
if netz != null:
var a: int = int(netz.andocken(start)["knoten"])
var b: int = int(netz.andocken(ende)["knoten"])
for idx in netz.pfad(a, b):
pts.append(netz.knoten[idx])
if drin:
pts.append(door(places, nach_ort))
pts.append(innen_tuer(places, nach_ort))
if nach_etage > 0:
pts.append(treppe(places, nach_ort, 0))
pts.append_array(treppen_weg(places, nach_ort, 0, nach_etage))
var ziel := nach
if drin:
ziel.y = hoehe(nach_etage)
pts.append(ziel)
return pts
## Ein Ort ist ein Gebäude, wenn er Grundriss und Tür hat.
static func gebaeude(places: Dictionary, key: String) -> bool:
return places.has(key) and float(places[key]["size"][0]) > 0.0 \
and DIR_VEC[places[key]["dir"]] != Vector3.ZERO
## Route mit allem, was ein Körper braucht: aus der Etage runter, gerade durch
## die Tür, über das Netz, durch die nächste Tür und die Treppe hinauf.
## `etagen` sagt, in welcher Etage der Mensch an welchem Ort ist.
static func route_voll(places: Dictionary, a: String, b: String, offset: Vector3,
etagen: Dictionary, netz: Netz = null, anker: Dictionary = {},
war_unterwegs: bool = false) -> Array:
var pts := route(places, a, b, offset, netz, anker)
if pts.is_empty():
return pts
if gebaeude(places, a) and not war_unterwegs:
var e_von := int(etagen.get(a, 0))
var runter: Array = []
if e_von > 0:
runter.append(treppe(places, a, e_von))
runter.append_array(treppen_weg(places, a, e_von, 0))
runter.append(innen_tuer(places, a))
runter.append(door(places, a))
runter.append_array(pts)
pts = runter
if gebaeude(places, b):
var e_nach := int(etagen.get(b, 0))
var ziel: Vector3 = pts.pop_back()
pts.append(door(places, b))
pts.append(innen_tuer(places, b))
if e_nach > 0:
pts.append(treppe(places, b, 0))
pts.append_array(treppen_weg(places, b, 0, e_nach))
ziel.y = hoehe(e_nach)
pts.append(ziel)
return pts
static func center(places: Dictionary, key: String) -> Vector3:
var p: Array = places[key]["pos"]
return Vector3(p[0], 0.0, p[1])
@@ -277,31 +63,3 @@ static func door(places: Dictionary, key: String) -> Vector3:
var size: Array = pl["size"]
var half: float = size[1] * 0.5 if absf(d.z) > 0.0 else size[0] * 0.5
return c + d * (half + 2.5)
## Route von Ort a nach Ort b. Gibt es ein Wegenetz, wird darauf gelaufen --
## sonst über den freien Platz in der Mitte.
static func route(places: Dictionary, a: String, b: String, offset: Vector3,
netz: Netz = null, anker: Dictionary = {}) -> Array:
var pts: Array = []
if a == b:
return pts
if netz != null and anker.has(a) and anker.has(b):
var weg := netz.pfad(int(anker[a]), int(anker[b]))
if not weg.is_empty():
for idx in weg:
var k: Vector3 = netz.knoten[idx]
if pts.is_empty() or pts[pts.size() - 1].distance_to(k) > 0.6:
pts.append(k)
pts.append(center(places, b) + offset)
return pts
var da := door(places, a)
var db := door(places, b)
pts.append(da)
if da.length() > 17.0:
pts.append(da.normalized() * 15.0)
if db.length() > 17.0:
pts.append(db.normalized() * 15.0)
pts.append(db)
pts.append(center(places, b) + offset)
return pts

View File

@@ -11,7 +11,6 @@ const GRAVITY := 24.0
const JUMP := 9.0
const FOLGE_ABSTAND := 2.5 # so weit bleibt der Geist auf der Spur zurück
const LEINE := 14.0 # weiter weg reißt die Leine: er wird nachgezogen
const FOLGE_NAH := 5.0 # näher dran braucht es keinen Anlauf
const SCHWEBE := 0.25 # eine Handbreit über dem Boden
const PITCH := -0.96 # 55° von oben, fest wie in Diablo
const CAM_YAW := PI * 0.25 # Blick diagonal über den Bezirk
@@ -29,9 +28,12 @@ var arm: Node3D
var cam: Camera3D
var _dreh_rest := 0.0 # noch zu drehender Winkel (Q)
var _zoom_stufe := 0
var _anlauf: Array = [] # Weg zu dem Menschen, an den man sich gehängt hat
var _stockt := 0.0 # Sekunden ohne Vorankommen
var spruenge := 0 # wie oft die Leine nachziehen musste
var _leine_pfad: Array = [] # Pfad zum Verfolgten, alle 0,4 s neu
var _leine_uhr := 0.0
var _verloren := 0.0 # Sekunden zu weit weg vom Verfolgten
var _stockt := 0.0 # Sekunden ohne Vorankommen (eingeklemmt)
var _vorher := Vector3.ZERO # Position im letzten Physikbild
var spruenge := 0 # wie oft Leine oder Stadt nachziehen mussten
var body: Node3D
var _schleier: MeshInstance3D
var _kern: MeshInstance3D
@@ -111,8 +113,7 @@ func _ready() -> void:
func set_mode(m: int) -> void:
mode = m
folgt = null
collision_layer = 1
collision_mask = 1
collision_layer = 1 # die Maske stellt main je Welt: jede Welt hat ihre Ebene
if m == Mode.GEIST:
body.scale = Vector3.ONE * 0.6
_schleier_mat.albedo_color.a = 0.3
@@ -177,7 +178,11 @@ func _physics_process(delta: float) -> void:
## Wer mit gedrückter Taste zwei Sekunden nicht vorankommt, steckt fest --
## dann setzt ihn die Stadt auf die nächste Straße.
func _eingeklemmt(delta: float, will: bool) -> void:
if mode != Mode.GEIST or not will or Vector2(velocity.x, velocity.z).length() > 0.3:
# gemessen am Weg, nicht an der Geschwindigkeit: in einer Ecke schiebt man mit
# voller Geschwindigkeit gegen die Wand und kommt doch nicht vom Fleck
var weg := Vector2(global_position.x - _vorher.x, global_position.z - _vorher.z).length()
_vorher = global_position
if mode != Mode.GEIST or not will or weg > 0.3 * delta:
_stockt = 0.0
return
_stockt += delta
@@ -210,37 +215,39 @@ func _fallen(delta: float) -> void:
velocity.y -= GRAVITY * delta
## An einen Menschen hängen. Steht er weit weg, wird erst der Weg zu ihm
## geplant: aus dem Haus, über die Straßen, in seine Wohnung.
## An einen Menschen hängen. Den Weg zu ihm findet das Navigationsnetz.
func anhaengen(m: Bewohner) -> void:
folgt = m
_anlauf = []
_stockt = 0.0
if global_position.distance_to(m.global_position) > FOLGE_NAH:
var ort: String = m.place if m._path.is_empty() else ""
_anlauf = Orte.route_zu(m.places, m.netz, global_position, m.global_position,
ort, m.etage(ort) if ort != "" else 0)
_leine_pfad = []
_leine_uhr = 0.0
_verloren = 0.0
## Folgen heißt: seiner Spur nachgehen, durch dieselben Türen und Treppen.
## Er läuft so schnell wie nötig, um dranzubleiben -- auch bei hohem Tempo.
## Folgen heißt: ihm nachgehen, durch dieselben Türen und Treppen. Der Pfad
## kommt vom Navigationsnetz und wird laufend neu geplant. Er läuft so schnell
## wie nötig, um dranzubleiben -- auch bei hohem Tempo.
func _an_der_leine(delta: float) -> void:
var mensch := folgt as Bewohner
var ziel: Vector3
if not _anlauf.is_empty():
ziel = _anlauf[0]
var bis := ziel - global_position
bis.y = 0.0
if bis.length() < 0.6 and absf(ziel.y - global_position.y) < 1.6:
_anlauf.pop_front()
if _anlauf.is_empty():
ziel = mensch.spur_punkt(FOLGE_ABSTAND)
else:
ziel = _anlauf[0]
_leine_uhr -= delta
if _leine_uhr <= 0.0 or _leine_pfad.is_empty():
_leine_uhr = 0.4
_leine_pfad = []
if mensch.nav_map.is_valid():
var pts := NavigationServer3D.map_get_path(mensch.nav_map, global_position,
mensch.global_position, true)
for i in range(1, pts.size()):
_leine_pfad.append(pts[i])
if pts.size() < 2:
print(" [kein Weg] Geist von %s zu %s bei %s" % [str(global_position),
mensch.data["name"], str(mensch.global_position)])
var abstand := global_position.distance_to(mensch.global_position)
var ziel: Vector3 = _leine_pfad[0] if not _leine_pfad.is_empty() else mensch.global_position
var d := ziel - global_position
d.y = 0.0
if not _leine_pfad.is_empty() and d.length() < 0.5 and absf(ziel.y - global_position.y) < 1.6:
_leine_pfad.pop_front()
var tempo := maxf(SPEED_GEIST, Bewohner.GEHTEMPO * Game.zeit_faktor() * 1.4)
if d.length() > 0.3:
if abstand > FOLGE_ABSTAND and d.length() > 0.05:
var v := d.normalized() * minf(tempo, d.length() / delta)
velocity.x = v.x
velocity.z = v.z
@@ -250,16 +257,13 @@ func _an_der_leine(delta: float) -> void:
velocity.z = 0.0
_fallen(delta)
move_and_slide()
if d.length() > 0.6 and Vector2(velocity.x, velocity.z).length() < 0.3:
_stockt += delta
else:
_stockt = 0.0
var zu_weit: bool = _anlauf.is_empty() \
and global_position.distance_to(mensch.global_position) > LEINE
if _stockt > 2.0 or zu_weit:
global_position = ziel # hängen geblieben: die Leine zieht ihn nach
# Verloren? Nur wenn das Netz keinen Weg weiß und er zu weit weg ist,
# wird er nachgezogen -- ein Notnagel, kein Spielzug.
_verloren = _verloren + delta if abstand > LEINE and _leine_pfad.is_empty() else 0.0
if _verloren > 3.0:
global_position = mensch.global_position + Vector3(0, 0.3, 0)
velocity = Vector3.ZERO
_stockt = 0.0
_verloren = 0.0
spruenge += 1

View File

@@ -139,14 +139,11 @@ static func room(parent: Node, center: Vector3, size: Vector3, wall_c: Color,
static func _meshes_von(b: Dictionary) -> Array:
var out: Array = []
for n in b["etagen"]:
for n in b["teile"]:
_sammle(n, out)
for n in b["dach"]:
_sammle(n, out)
for liste in b["waende"]:
for w in liste:
if not out.has(w):
out.append(w)
for w in b["waende"]:
if not out.has(w):
out.append(w)
return out
@@ -162,12 +159,10 @@ static func _wand(mi: MeshInstance3D, normale: Vector3) -> MeshInstance3D:
return mi
## Etagenschnitt wie bei den Sims: steht der Spieler in einem Bau, verschwinden
## das Dach, die Etagen über ihm und die beiden Wände, die zur Kamera zeigen.
## Ein Bau: {c, size, hoch, etagen, dach, waende, voll}. "voll" heißt: ein
## geschlossener Klotz, der ganz verschwindet.
## Ein Bau zwischen Kamera und Spieler wird durchsichtig, sonst verschwände
## der Spieler hinter jedem Nachbarhaus.
## 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:
for b in bauten:
var c: Vector3 = b["c"]
@@ -188,17 +183,11 @@ static func schnitt(bauten: Array, spieler: Vector3, kamera: Vector3) -> void:
(mi as GeometryInstance3D).cast_shadow = \
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF if im_weg \
else GeometryInstance3D.SHADOW_CASTING_SETTING_ON
var etagen: Array = b["etagen"]
var pf := clampi(int(floor((spieler.y - c.y + 0.6) / Orte.ETAGE)), 0, maxi(0, etagen.size() - 1))
for f in range(etagen.size()):
(etagen[f] as Node3D).visible = not drin or (f <= pf and not bool(b.get("voll", false)))
for d in b["dach"]:
(d as Node3D).visible = not drin
var waende: Array = b["waende"]
for f in range(waende.size()):
for w in waende[f]:
var n: Vector3 = w.get_meta("normal")
(w as Node3D).visible = not (drin and f == pf and n.dot(kamera - c) > 0.0)
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)
## Ein Satteldach: ein geschlossenes Prisma über den Wänden. Der First läuft

View File

@@ -6,6 +6,7 @@ class_name UI
## der Mitte für Orte. Die Zeitleiste unten ist der Tag des Menschen -- alles
## im Spiel hängt daran.
signal hinfahren(id: String)
signal panel_opened
signal panel_closed
@@ -25,8 +26,15 @@ const C_UNBEKANNT := Color(0.22, 0.23, 0.30)
const C_GESEHEN := Color(0.46, 0.50, 0.62)
const C_SCHLAF := Color(0.30, 0.33, 0.44)
const C_WARN := Color(0.85, 0.66, 0.32)
const C_VERDAECHTIG := Color(0.58, 0.46, 0.26) # Spur gelesen, Stunde noch nicht gesehen
const C_BLOCKER := Color(0.84, 0.42, 0.35)
const C_SYSTEM := Color(0.44, 0.78, 0.50)
const C_PAPIER := Color(0.95, 0.92, 0.85) # die Akte: Papier, dunkle Tinte
const C_PAPIER_ZEILE := Color(0.90, 0.86, 0.78)
const TINTE := "#2b2620"
const TINTE_HELL := "#6b6257"
const TINTE_ROT := "#a83a2e"
const TINTE_GRUEN := "#3d7a45"
var offen := false
var _kind := ""
@@ -299,6 +307,8 @@ func _ablauf_farbe(human_id: String, nr: int) -> Color:
return C_BLOCKER
Game.Stunde.SCHLAF:
return C_SCHLAF
Game.Stunde.VERDAECHTIG:
return C_VERDAECHTIG
return C_GESEHEN
@@ -310,20 +320,24 @@ func _ablauf_aktualisieren() -> void:
return
var jetzt := int(Game.time_h)
var offen_std := 0
var verdaechtig := 0
var puls := 0.5 + 0.5 * sin(Time.get_ticks_msec() * 0.008)
for stunde in range(24):
var nr := Game.slot_bei_stunde(id, stunde)
var f := _ablauf_farbe(id, nr)
if not Game.gesehen_slot(id, nr):
offen_std += 1
if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG:
verdaechtig += 1
var zelle: ColorRect = _zellen[stunde]
if stunde == jetzt:
f = f.lightened(0.55 if not Game.situation_bei(id) else 0.3 + 0.4 * puls)
zelle.color = f
zelle.custom_minimum_size.y = 24 if stunde == jetzt else 16
zelle.tooltip_text = "%02d Uhr — %s" % [stunde, _stunden_text(id, nr)]
_ablauf_titel.text = "Ihr Tag 0 – 23 Uhr%s" % \
("" if offen_std == 0 else " · %d Stunden unbekannt" % offen_std)
_ablauf_titel.text = "Ihr Tag 0 – 23 Uhr%s%s" % [
"" if offen_std == 0 else " · %d Stunden unbekannt" % offen_std,
"" if verdaechtig == 0 else " · %d verdächtig" % verdaechtig]
var urs: Array = []
for u in Game.ursachen_von(id):
@@ -414,7 +428,7 @@ func _baue_wirte() -> void:
func _wirt_fuer(kind: String) -> PanelContainer:
match kind:
"journal", "technik", "systeme":
"journal", "technik", "akte":
return _drawer
"slot", "situation", "anwenden":
return _karte_host
@@ -609,7 +623,7 @@ func _reiter() -> void:
_content.add_child(hb)
var seiten := [
["techniken", "Techniken (%d)" % Game.known.size()],
["missionen", "Missionen (%d)" % Game.progress.size()],
["missionen", "Akten (%d)" % Game.progress.size()],
]
for eintrag in seiten:
var b := Button.new()
@@ -675,44 +689,145 @@ func _journal_missionen() -> void:
stand = "aktiv · %d %%" % int(Game.mission_progress(id))
farbe = C_AKZENT
_karte("%s · %s" % [h["name"], stand], farbe,
func() -> void: systeme_panel(id))
func() -> void: akte_panel(id))
# ---------------------------------------------- Diagnose und Systeme
# ---------------------------------------------- Die Akte
## Alles zu einem Menschen auf einem Blatt: Person und Traum, was im Weg steht,
## der Befund aus Spuren und Beobachtungen, der Tag, die Routinen, die Notizen.
## Keine Patientenakte -- die Leute sind nicht krank, sie haben Gewohnheiten.
var _blatt: VBoxContainer = null
## Der Traum scheitert an Ursachen. Die stehen nicht im Panel -- man findet sie,
## indem man dabeisteht. Gegen jede gefundene Ursache baut man ein System in
## den Tag. Je mehr abgedeckt ist, desto schneller kommt der Traum voran.
func systeme_panel(human_id: String) -> void:
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "systeme" else 0
var h := HumanDB.by_id(human_id)
_leeren("%s — Tagesablauf" % h["name"])
_text("%d %% · +%d %% je Tag · [color=#aaaaaa]%s[/color]" %
[int(Game.mission_progress(human_id)), int(round(Game.traum_tempo(human_id))),
h["traum"]], 16)
_trenner()
akte_panel(human_id)
_text("[b]Woran es scheitert[/b]", 17)
var offene := 0
func akte_panel(human_id: String) -> void:
var scroll_alt := _scroll.scroll_vertical if offen and _kind == "akte" else 0
var h := HumanDB.by_id(human_id)
_leeren("Akte")
_papier()
var erfuellt := Game.completed.has(human_id)
_akte_text("[b]%s[/b] [color=%s]%d · %s[/color]" % [h["name"], TINTE_HELL,
int(h.get("alter", 0)), h.get("beruf", "")], 20)
_akte_text("[color=%s]Traum[/color] „%s“" % [TINTE_HELL, h["traum"]], 16)
if erfuellt:
_stempel("TRAUM ERFÜLLT")
else:
_akte_text("[color=%s]Stand[/color] %d %% · +%d %% je Tag" % [TINTE_HELL,
int(Game.mission_progress(human_id)), int(round(Game.traum_tempo(human_id)))], 16)
_knopf("Hinfahren — vor die Tür", func() -> void:
schliessen()
hinfahren.emit(human_id), true, _blatt)
_akte_abschnitt("Was im Weg steht")
_akte_text(str(h["problem"]), 15)
_akte_abschnitt("Befund")
for u in Game.ursachen_von(human_id):
var name: String = str(u)
if not Game.ursache_bekannt(human_id, name):
_text("[color=#8e93a8]? — noch nicht erkannt[/color]", 15)
offene += 1
var stand: String
if Game.ursache_geloest(human_id, name) > 0.0:
stand = "[color=%s]Routine steht[/color]" % TINTE_GRUEN
elif Game.ursache_bekannt(human_id, name):
var woher := "Spur gelesen" if Game.spur_bekannt(human_id, name) else "beobachtet"
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)
continue
var g := Game.ursache_geloest(human_id, name)
var stand := "[color=#88ff99]gelöst[/color]" if g > 0.0 else \
"[color=#ff9977]offen · bremst auf %d %%[/color]" % \
int(float(Ursachen.LISTE[name]["faktor"]) * 100.0)
_text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15)
if offene > 0: _trenner()
_akte_text("[b]%s[/b] %s" % [Ursachen.LISTE[name]["name"], stand], 15)
var probleme: Array = []
for pr in Game.probleme_von(human_id):
var key := str(pr)
if not Game.problem_bekannt(human_id, key):
probleme.append("[color=%s]?[/color]" % TINTE_HELL)
elif Game.problem_ueberwunden(human_id, key):
probleme.append("[color=%s]%s ✓[/color]" % [TINTE_GRUEN, Probleme.name_von(key)])
else:
probleme.append("%s %d/%d" % [Probleme.name_von(key),
Game.problem_fortschritt(human_id, key), Game.PROBLEM_UEBERWINDEN])
if not probleme.is_empty():
_akte_text("[color=%s]In Momenten[/color] %s" % [TINTE_HELL, " · ".join(probleme)], 15)
_akte_abschnitt("Tag")
_tagesablauf(human_id)
_zeigen("systeme", journal_panel.bind("missionen"))
_akte_abschnitt("Notizen")
var zeilen: Array = Game.notizen.get(human_id, [])
if zeilen.is_empty():
_akte_text("[color=%s]noch keine[/color]" % TINTE_HELL, 14)
for i in range(maxi(0, zeilen.size() - 8), zeilen.size()):
_akte_text("[color=%s]%s[/color]" % [TINTE, zeilen[i]], 14)
_zeigen("akte", journal_panel.bind("missionen"))
_scroll_wunsch = scroll_alt
_scroll_frames = 4
## Ein Blatt Papier im Drawer; alles Weitere der Akte landet darauf.
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
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)
func _akte_text(bb: String, groesse: int = 15) -> RichTextLabel:
var r := _text("[color=%s]%s[/color]" % [TINTE, bb], groesse, _blatt)
return r
func _akte_abschnitt(titel: String) -> void:
var r := _text("[color=%s]%s[/color]" % [TINTE_HELL, titel.to_upper()], 12, _blatt)
r.add_theme_constant_override("line_separation", 2)
var linie := HSeparator.new()
linie.add_theme_stylebox_override("separator", _sb(Color(0.78, 0.72, 0.6, 0.6), 0))
linie.custom_minimum_size.y = 2
_blatt.add_child(linie)
func _stempel(text: String) -> void:
var l := Label.new()
l.text = text
l.add_theme_font_size_override("font_size", 18)
l.add_theme_color_override("font_color", Color(0.66, 0.2, 0.15))
l.add_theme_stylebox_override("normal", _sb(Color(0, 0, 0, 0), 2, Color(0.66, 0.2, 0.15), 2))
l.rotation_degrees = -4.0
_blatt.add_child(l)
## Eine Zeile der Akte, die sich aufschlagen lässt (Stunden des Tages).
func _akte_karte(beschriftung: String, farbe: Color, cb: Callable, aktiv: bool = true) -> Button:
var b := Button.new()
b.text = beschriftung
b.alignment = HORIZONTAL_ALIGNMENT_LEFT
b.disabled = not aktiv
b.custom_minimum_size = Vector2(0, 30)
b.add_theme_font_size_override("font_size", 14)
b.add_theme_color_override("font_color", Color(0.17, 0.15, 0.13))
b.add_theme_color_override("font_hover_color", Color(0.1, 0.08, 0.06))
b.add_theme_color_override("font_disabled_color", Color(0.3, 0.27, 0.24))
b.add_theme_stylebox_override("normal", _sb(C_PAPIER_ZEILE, 3, farbe, 0, 4))
b.add_theme_stylebox_override("hover", _sb(C_PAPIER_ZEILE.darkened(0.06), 3, farbe, 0, 4))
b.add_theme_stylebox_override("pressed", _sb(C_PAPIER_ZEILE.darkened(0.1), 3, farbe, 0, 4))
b.add_theme_stylebox_override("disabled", _sb(C_PAPIER_ZEILE, 3, farbe.darkened(0.2), 0, 4))
_blatt.add_child(b)
if aktiv:
b.pressed.connect(cb)
return b
## Der Ablauf von 0 bis 23 Uhr. Jeder Tag ist gleich -- es gibt keine
## Wochentage. Zu Beginn ist jede Stunde leer; wer dabeisteht, füllt sie.
func _tagesablauf(human_id: String) -> void:
@@ -720,8 +835,8 @@ func _tagesablauf(human_id: String) -> void:
for stunde in range(24):
if not Game.gesehen_slot(human_id, Game.slot_bei_stunde(human_id, stunde)):
offen += 1
_text("[b]Ihr Tag[/b]%s" %
("" if offen == 0 else " [color=#8e93a8]%d Stunden leer[/color]" % offen), 17)
if offen > 0:
_akte_text("[color=%s]%d Stunden leer[/color]" % [TINTE_HELL, offen], 14)
# eine Zeile je Abschnitt des Tages -- auch wenn noch keiner bekannt ist,
# steht der Tag vollständig da
@@ -756,8 +871,8 @@ func _stunden_zeile(human_id: String, nr: int, von: int, bis: int, text: String)
var spanne := "%02d – %02d" % [von, bis + 1 if bis < 23 else 24]
var offen: bool = Game.gesehen_slot(human_id, nr) and Game.erkannt(human_id, nr) \
and Game.slot_grund(human_id, nr) != "schlaeft"
_karte("%s %s" % [spanne, text], _ablauf_farbe(human_id, nr),
func() -> void: slot_panel(human_id, nr, systeme_panel.bind(human_id)), offen)
_akte_karte("%s %s" % [spanne, text], _ablauf_farbe(human_id, nr),
func() -> void: slot_panel(human_id, nr, akte_panel.bind(human_id)), offen)
## Der offene Moment: hier zählt, welche Technik zu diesem Problem passt.
@@ -808,13 +923,13 @@ func slot_panel(human_id: String, nr: int, zurueck: Callable = Callable()) -> vo
_text("Sie %s.\n[b]Auslöser:[/b] %s\n[b]Belohnung:[/b] %s" %
[b["name"], b["ausloeser"], Ursachen.belohnung_text(str(b["belohnung"]))], 16)
else:
_text("Hier ist Platz. Ein neues System passt in diesen Slot.", 16)
_text("Hier ist Platz für eine neue Routine.", 16)
_trenner()
var sys := Game.system_in(human_id, nr)
if not sys.is_empty():
_text("[color=#88ff99]Hier steht: %s[/color]" %
TechDB.by_id(str(sys["tech"]))["name"], 16)
_knopf("System wieder herausnehmen", func() -> void:
_knopf("Routine wieder herausnehmen", func() -> void:
Game.system_entfernen(human_id, nr)
systeme_panel(human_id))
_trenner()
@@ -1043,7 +1158,7 @@ func _vorschau_aktualisieren() -> void:
var wert := int(bild["verhalten"][v])
if wert != 0:
zuege.append("%s %s" % [namen[v], "+" if wert > 0 else "−"])
_vorschau.text = "[color=#ff9a8a]%s[/color]\n[color=#ffcc66]%s[/color] · %d Blocker im Tag\n[color=#8e93a8]%s[/color]" % \
_vorschau.text = "[color=#ff9a8a]%s[/color]\n[color=#ffcc66]%s[/color] · %d Gewohnheiten im Tag\n[color=#8e93a8]%s[/color]" % \
[" · ".join(urs), " · ".join(prob), (bild["blocker"] as Array).size(),
" ".join(zuege) if not zuege.is_empty() else "unauffällig"]

View File

@@ -10,26 +10,38 @@ class_name Ursachen
## bemerken, dann ersetzen. Und Duhiggs goldene Regel: Auslöser und Belohnung
## bleiben, nur die Handlung dazwischen wird getauscht.
## spur: was davon in der Wohnung herumliegt. Wer hineingeht und hinsieht,
## weiß, wonach er im Tag suchen muss.
static var LISTE: Dictionary = {
"anfang": {
"name": "Kein Anfang", "kurz": "kommt nie in Gang",
"faktor": 0.30,
"spur": "Handy auf dem Boden",
"spur_text": "Das Handy liegt neben dem Tisch, der Bildschirm noch warm. Angefangen wird hier nicht.",
},
"zeit": {
"name": "Keine Zeit", "kurz": "der Tag ist voll",
"faktor": 0.30,
"spur": "Vollgeschriebener Kalender",
"spur_text": "Jede Stunde im Kalender ist belegt. Für den Traum steht keine drin.",
},
"kraft": {
"name": "Keine Kraft", "kurz": "abends ist nichts mehr übrig",
"faktor": 0.40,
"spur": "Leere Dosen am Bett",
"spur_text": "Drei leere Dosen neben dem Bett. Abends ist nichts mehr übrig.",
},
"halt": {
"name": "Kein Halt", "kurz": "fängt an und hört wieder auf",
"faktor": 0.50,
"spur": "Stapel ungeöffneter Briefe",
"spur_text": "Ein Stapel ungeöffneter Briefe. Angefangen und liegen gelassen.",
},
"grund": {
"name": "Kein Grund", "kurz": "glaubt nicht mehr daran",
"faktor": 0.40,
"spur": "Verstaubter Koffer",
"spur_text": "Ein Koffer voller alter Pläne, längst verstaubt. Daran glaubt hier niemand mehr.",
},
}