extends Node3D ## Wurzelknoten: hält Spieler und Oberfläche, wechselt zwischen den Welten ## und wickelt Interaktionen ab. const REICHWEITE_OBJEKT := 4.2 const REICHWEITE_MENSCH := 3.6 var player: Player var ui: UI 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 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: ui = UI.new() add_child(ui) ui.panel_opened.connect(func() -> 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) Game.mission_done.connect(func(_id: String) -> void: Game.notified.emit("Kehre zur Traumwelt zurück — die Säule im Park bringt dich hin.", Color(0.8, 0.95, 1))) _traumwelt() Input.mouse_mode = Input.MOUSE_MODE_VISIBLE func _traumwelt() -> void: auf_erde = false 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.collision_mask = 1 player.global_position = traum.spawn player.yaw = Player.CAM_YAW func _erde() -> void: auf_erde = true 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.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 ## 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 _interaktive = [] _fokus_takt = 0.0 Game.teleport_lock = 1.0 _fokus = null _fokus_mensch = null func _process(delta: float) -> void: var dt := delta * Game.zeit_faktor() Game.advance_time(dt) Game.traum_voranbringen(dt * 24.0 / Game.DAY_SECONDS) Game.teleport_lock = maxf(0.0, Game.teleport_lock - delta) if ui.offen: ui.setze_hinweis("") 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() _hinweis_setzen() ## Wer ein Schild trägt: Mission, Fokus, Verfolgter und wer unter der Maus steht. func _markieren() -> void: Game.markiert = {} if Game.active_mission != "": Game.markiert[Game.active_mission] = true if _fokus_mensch != null: Game.markiert[str(_fokus_mensch.data["id"])] = true if player.folgt != null and is_instance_valid(player.folgt): Game.markiert[str(player.folgt.data["id"])] = true if auf_erde: var unter_maus := _mensch_unter_maus(get_viewport().get_mouse_position()) if unter_maus != null: Game.markiert[str(unter_maus.data["id"])] = true ## Der Mensch, der auf dem Bildschirm am nächsten am Punkt steht -- oder null. func _mensch_unter_maus(pos: Vector2) -> Bewohner: if not auf_erde: return null var bester: Bewohner = null var beste := 48.0 for m in (welt as EarthWorld).humans: var w: Vector3 = (m as Node3D).global_position + Vector3(0, 0.8, 0) if player.cam.is_position_behind(w): continue var d := player.cam.unproject_position(w).distance_to(pos) if d < beste: beste = d bester = m return bester func _fokus_suchen() -> void: _fokus = null _fokus_mensch = null var p := player.global_position var best := REICHWEITE_OBJEKT 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 _fokus = n if auf_erde: var bestm := REICHWEITE_MENSCH for m in (welt as EarthWorld).humans: var mp: Vector3 = (m as Node3D).global_position + Vector3(0, 1.0, 0) var d: float = p.distance_to(mp) if d < bestm: bestm = d _fokus_mensch = m if player.folgt != null and is_instance_valid(player.folgt): _fokus_mensch = player.folgt # wer sich angehängt hat, ist dabei -- auch auf der Treppe if _fokus_mensch != null and (_fokus == null or bestm < best or _fokus_mensch == player.folgt): _fokus = null elif _fokus != null: _fokus_mensch = null func _hinweis_setzen() -> void: if player.folgt != null and is_instance_valid(player.folgt): ui.setze_hinweis("%s · %s · [F] los" % [player.folgt.data["name"], _notiz_hinweis(player.folgt)]) elif _fokus_mensch != null: ui.setze_hinweis("%s · %s · [F] folgen" % [_fokus_mensch.data["name"], _notiz_hinweis(_fokus_mensch)]) elif _fokus != null: var kind: String = _fokus.get_meta("kind") var titel: String = _fokus.get_meta("titel") match kind: "erde": ui.setze_hinweis("[E] Zur Erde reisen") "traum": ui.setze_hinweis("[E] Zurück in die Traumwelt") "lehre": 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: ui.setze_hinweis("") ## Was am Menschen gerade dransteht: notieren, ansprechen oder nur zusehen. func _notiz_hinweis(mensch: Bewohner) -> String: var tat := Orte.text(mensch.activity) if str(mensch.data["id"]) != Game.active_mission: return tat if Game.situation_bei(str(mensch.data["id"])): return "%s · [E] Technik" % \ Probleme.SITUATIONEN[Game.situation["key"]]["name"] if Game.notiz_sperre > 0.0: return "%s (%.0f s)" % [tat, Game.notiz_sperre] if Game.erkannt(str(mensch.data["id"]), int(mensch.slot)): return "%s · [E] ansprechen" % tat return "%s · [R] notieren" % tat func _unhandled_input(event: InputEvent) -> void: if ui.offen: return if event is InputEventMouseButton and event.pressed \ and event.button_index == MOUSE_BUTTON_LEFT: _klick(event.position) return for stufe in [1, 2, 3]: if event.is_action_pressed("tempo_%d" % stufe): Game.tempo_setzen(stufe) get_viewport().set_input_as_handled() return if event.is_action_pressed("journal"): ui.journal_panel() get_viewport().set_input_as_handled() return if event.is_action_pressed("interagieren"): _interagieren() get_viewport().set_input_as_handled() return if event.is_action_pressed("folgen"): _verfolgen() get_viewport().set_input_as_handled() return if event.is_action_pressed("notieren"): _notieren() get_viewport().set_input_as_handled() ## Wer dabeisteht, füllt die Stunde im Ablauf -- mehr nicht. Was dort ## schiefläuft, muss er selbst benennen. func _dabeisein() -> void: if _fokus_mensch == null or not auf_erde: return var id: String = str(_fokus_mensch.data["id"]) if id == Game.active_mission: Game.sehen(id, int(_fokus_mensch.slot)) if Game.situation_bei(id): Game.situation_sehen() ## Den Moment benennen: hier läuft etwas schief. Das ist der Kern des ## Beobachtens -- der Spieler entscheidet, wann er hinschaut. func _notieren() -> void: if not auf_erde: return var ziel: Bewohner = _fokus_mensch if ziel == null and player.folgt != null and is_instance_valid(player.folgt): ziel = player.folgt if ziel == null: Game.notified.emit("Geh näher an den Menschen heran, dem du hilfst.", Color(1, 0.85, 0.5)) return var id: String = str(ziel.data["id"]) if id != Game.active_mission: Game.notified.emit("Für %s hast du keine Mission angenommen." % ziel.data["name"], Color(1, 0.85, 0.5)) return if Game.notiz_sperre > 0.0: Game.notified.emit("Du bist noch beim letzten Fehlgriff (%.0f s)." % Game.notiz_sperre, Color(1, 0.75, 0.55)) return Game.notieren(id, int(ziel.slot)) ## Klick auf einen Menschen: an ihn hängen, noch ein Klick löst wieder. func _klick(pos: Vector2) -> void: var bester := _mensch_unter_maus(pos) if bester == null: return if player.folgt == bester: player.folgt = null Game.notified.emit("Du lässt %s wieder los." % bester.data["name"], Color(0.8, 0.85, 1)) else: player.anhaengen(bester) Game.notified.emit("Du hängst dich an %s." % bester.data["name"], Color(0.7, 0.9, 1)) ## Als Geist an einen Menschen hängen und ihn durch seinen Tag begleiten. func _verfolgen() -> void: if not auf_erde: return if player.folgt != null: var name: String = player.folgt.data["name"] player.folgt = null Game.notified.emit("Du lässt %s wieder los." % name, Color(0.8, 0.85, 1)) return if _fokus_mensch == null: Game.notified.emit("Geh näher an einen Menschen heran, um ihm zu folgen.", Color(1, 0.85, 0.5)) return player.anhaengen(_fokus_mensch) Game.notified.emit("Du hängst dich an %s." % _fokus_mensch.data["name"], Color(0.7, 0.9, 1)) func _interagieren() -> void: if _fokus_mensch != null: # Der offene Moment geht vor: er ist gleich wieder vorbei if Game.situation_bei(str(_fokus_mensch.data["id"])): ui.situation_panel(_fokus_mensch) else: ui.anwenden_panel(_fokus_mensch) return if _fokus == null: return var kind: String = _fokus.get_meta("kind") match kind: "erde": if Game.active_mission == "" and Game.completed.size() < HumanDB.LIST.size(): Game.notified.emit( "Das Tor öffnet sich nur mit einer Mission. Sieh am Schwarzen Brett nach.", Color(1, 0.8, 0.5)) else: _erde() "traum": _traumwelt() "lehre": 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))