class_name Main extends Node ## The single home for the game's screen sequence: Title → Creation → Shell. ## Script-first by design — a pure coordinator with no Control layout, so ADR 0001's ## editor-first rule does not apply (that rule governs layout scenes). It owns one ## ContentDB + one DmService and swaps screens via instantiate → inject → add_child, ## the injection seam change_scene_to_file cannot provide. ## ## §2: the flow carries only plain data; NewGame.construct re-rolls the character ## from the creation seed itself. ⛨ saga: _show_creation and _start_campaign are ## separate, so a fourth entry path is an added arm, not surgery on existing edges. const HttpTransport = preload("res://scripts/net/http_transport.gd") const TITLE := preload("res://scenes/title/TitleScreen.tscn") const CREATION := preload("res://scenes/creation/CharacterCreation.tscn") const SHELL := preload("res://scenes/shell/MainWindowShell.tscn") # Injection seams — set BETWEEN instantiate() and add_child(), as the shell/creation # do for their own deps. _ready builds the real ones when nothing was supplied. var world: ContentDB = null var service: DmService = null var origin: Dictionary = {} var _current: Node = null var _http: HTTPRequest func _ready() -> void: if world == null: world = ContentDB.new() world.load_from(ContentDB.default_content_root()) if origin.is_empty(): origin = ContentDB.load_json(ContentDB.origin_path("deserter")) if service == null: _http = HTTPRequest.new() add_child(_http) service = DmService.new(HttpTransport.new(_http), FallbackLibrary.new()) _show_title() func _show(next: PackedScene, inject: Callable) -> void: ## The one swap primitive. inject runs after instantiate() (which does NOT run ## _ready) and before add_child (which does) — the injection window. The outgoing ## screen is queue_free'd (deferred): a signal handler must never free the node ## still executing up its own call stack. var old := _current var screen := next.instantiate() inject.call(screen) _current = screen add_child(screen) if old != null: old.queue_free() func _show_title() -> void: _show(TITLE, func(s): s.menu_activated.connect(_on_menu)) func _show_creation() -> void: _show(CREATION, func(s): s.world = world s.origin = origin s.creation_confirmed.connect(_start_campaign)) func _show_shell(log: CanonLog, state: GameState) -> void: _show(SHELL, func(s): s.service = service s.injected_log = log s.injected_state = state) func _on_menu(id: StringName) -> void: match id: &"new_game": _show_creation() &"quit": get_tree().quit() _: pass # continue / load land in later milestones (M9); inert for now func _start_campaign(creation: Dictionary) -> void: ## Takes a plain Dictionary — does NOT assume it came from the creation screen, so ## a saga can synthesize one and call this directly (skipping creation). var result := NewGame.construct(origin, world, creation) if not result.get("ok", false): # Unreachable via the UI (the CTA gates on validate), but §2 forbids trusting # the caller and §13 forbids surfacing an error. Log and stay put. push_error("Main: construct rejected a creation: %s" % str(result.get("errors", []))) return _show_shell(result["log"], result["state"])