extends Control ## Throwaway harness — the bounded-NPC loop on screen, not shipped UI. Type an ## utterance to Fenn, see his voiced prose, watch moves apply and disposition ## change, until he ends the conversation. Mirrors narrate_harness. Run this ## scene directly (F6) with the proxy + Ollama up. const NpcService = preload("res://scripts/net/npc_service.gd") const NpcContent = preload("res://scripts/npc/npc_content.gd") const MoveApplier = preload("res://scripts/npc/move_applier.gd") const ContentDB = preload("res://scripts/content/content_db.gd") const HttpTransport = preload("res://scripts/net/http_transport.gd") const FallbackLibrary = preload("res://scripts/net/fallback_library.gd") const NPC_ID := "fenn" var _service: NpcService var _content: ContentDB var _game_state: GameState var _canon_log: CanonLog var _output: RichTextLabel var _entry: LineEdit var _speak_btn: Button func _ready() -> void: var http := HTTPRequest.new() add_child(http) _service = NpcService.new(HttpTransport.new(http), FallbackLibrary.new()) _content = ContentDB.new() _content.load_from(ContentDB.default_content_root()) _game_state = GameState.new() _canon_log = CanonLog.new() _canon_log.set_location("greywater_docks", "Greywater Docks") _canon_log.player.name = "Aldric" _canon_log.player.class_id = "sellsword" # Deliberately no find_the_ledger quest here — leave it un-started so Fenn # can offer_quest it during the conversation. var vbox := VBoxContainer.new() vbox.set_anchors_preset(Control.PRESET_FULL_RECT) vbox.add_theme_constant_override("separation", 12) add_child(vbox) _output = RichTextLabel.new() _output.bbcode_enabled = true _output.fit_content = true _output.custom_minimum_size = Vector2(600, 400) vbox.add_child(_output) _entry = LineEdit.new() _entry.placeholder_text = "Say something to Fenn…" vbox.add_child(_entry) _speak_btn = Button.new() _speak_btn.text = "Speak" _speak_btn.pressed.connect(_on_speak) vbox.add_child(_speak_btn) func _on_speak() -> void: var utterance := _entry.text if utterance.strip_edges() == "": return _entry.text = "" _speak_btn.disabled = true _append("[b]You:[/b] %s" % utterance) var disposition := int(_game_state.npc_dispositions.get(NPC_ID, 0)) var available := NpcContent.available_moves(_content.npc(NPC_ID), _game_state, _canon_log) var r: NpcResult = await _service.speak( NPC_ID, utterance, _canon_log, disposition, available) # Apply state explicitly (§2) — the service applied nothing. MoveApplier.apply(r.valid_moves, _game_state, _canon_log, _content, NPC_ID) for f in r.facts: _canon_log.add_fact(f) _append("[b]Fenn:[/b] %s" % r.display_text) _append("[i]moves: %d applied, %d dropped · disposition %d%s[/i]" % [ r.valid_moves.size(), r.dropped_moves.size(), int(_game_state.npc_dispositions.get(NPC_ID, 0)), " (degraded)" if r.degraded else ""]) if r.ends_conversation: _append("[i]— Fenn ends the conversation. —[/i]") else: _speak_btn.disabled = false func _append(bbcode: String) -> void: _output.append_text(bbcode + "\n\n")