feat(shell): NarrationBook wired to /dm/narrate with refire + fact-harvest

This commit is contained in:
2026-07-11 09:07:24 -05:00
parent bca04e3a2e
commit 5f1ebdf840
5 changed files with 151 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
class_name NarrationBook
extends Control
## The permanent DM narration book (mock 2a, right panel). §2: text — it consumes
## /dm/narrate prose and never treats it as truth; only harvested [FACT] tags are
## written back to the canon log (§11). Prose-only this milestone: the numbered
## response choices and the "describe your own action" line are faithful but inert
## (no action system yet); one in-world refire affordance re-runs narration.
var last_degraded: bool = false
var _service: DmService
var _log: CanonLog
var _state: ShellState
var _header: Label
var _prose: RichTextLabel
var _status: Label
var _refire: Button
var _degraded_tag: Label
var _indicator: ConsideringIndicator
func _ready() -> void:
var col := VBoxContainer.new()
col.set_anchors_preset(Control.PRESET_FULL_RECT)
col.add_theme_constant_override("separation", 18)
add_child(col)
_header = Label.new()
_header.theme_type_variation = ThemeKeys.ACCENT
col.add_child(_header)
_prose = RichTextLabel.new()
_prose.bbcode_enabled = true
_prose.fit_content = true
_prose.size_flags_vertical = Control.SIZE_EXPAND_FILL
_prose.custom_minimum_size = Vector2(660, 320)
col.add_child(_prose)
_degraded_tag = Label.new()
_degraded_tag.theme_type_variation = ThemeKeys.MONO
_degraded_tag.text = "THE MASTER'S VOICE COMES THIN" # shown only when degraded
_degraded_tag.visible = false
col.add_child(_degraded_tag)
# Faithful-but-inert placeholders (charter: prose-only this milestone).
var prompt := Label.new()
prompt.theme_type_variation = ThemeKeys.ACCENT
prompt.text = "▸ Your turn. What do you do?"
col.add_child(prompt)
for line in [
"1 · Pay the fence double and keep your hand where he can see it",
"2 · Draw your blade — he's closer than his guards",
"3 · Lie — tell him the rest is coming with your dwarf"]:
var choice := Button.new()
choice.theme_type_variation = ThemeKeys.PARCHMENT_BUTTON
choice.text = line
choice.disabled = true # inert until combat/dialogue give choices meaning
col.add_child(choice)
var free_text := Label.new()
free_text.theme_type_variation = ThemeKeys.MONO
free_text.text = "⌨ …or describe your own action"
col.add_child(free_text)
# The one live control: refire the narration.
_refire = Button.new()
_refire.theme_type_variation = ThemeKeys.PARCHMENT_BUTTON
_refire.text = "▸ The Master continues…"
_refire.pressed.connect(_on_refire_pressed)
col.add_child(_refire)
_status = Label.new()
_status.theme_type_variation = ThemeKeys.MONO
col.add_child(_status)
_indicator = ConsideringIndicator.new()
add_child(_indicator)
func setup(service: DmService, log: CanonLog, state: ShellState) -> void:
_service = service
_log = log
_state = state
_header.text = "%s · %s" % [state.round_label, state.location_label]
func narrate() -> void:
if _service == null or _log == null:
return
_refire.disabled = true
_indicator.start(_status)
var result: NarrateResult = await _service.narrate(_log)
_indicator.stop()
show_prose(result)
for f in result.facts:
_log.add_fact(f) # §11: code applies the state write, explicitly
_refire.disabled = false
func show_prose(result: NarrateResult) -> void:
_prose.text = result.display_text
last_degraded = result.degraded
_degraded_tag.visible = result.degraded
func _on_refire_pressed() -> void:
narrate()