refactor(shell): author shell scenes as editor-previewable node trees

Move the three shell scenes (SystemDock, NarrationBook, MainWindowShell)
from build-everything-in-_ready() to authored .tscn node trees with the
theme + type-variations set in the editor, so the layout previews and can
be arranged in the Godot editor. Scripts keep only on-load work: @onready
node refs, DM-loop wiring, the dock slide, and binding seed ShellState into
the fixed authored turn-order tokens + consumable slots.

Fixes the dock offset + over-long buttons (now fixed-width DockButtons in a
right-anchored menu). Headless probe: root 1920x1080, world/book split
1180/740, dock at the world's right edge — no collapse. Suite 138/138.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 09:38:22 -05:00
parent 578b3e86c9
commit 4698732285
6 changed files with 525 additions and 218 deletions

View File

@@ -1,6 +1,284 @@
[gd_scene load_steps=2 format=3]
[gd_scene load_steps=6 format=3]
[ext_resource type="Script" path="res://scripts/ui/shell/main_window_shell.gd" id="1"]
[ext_resource type="Theme" path="res://assets/theme/game_theme.tres" id="2"]
[ext_resource type="PackedScene" path="res://scenes/theme/surfaces/DarkBay.tscn" id="3"]
[ext_resource type="PackedScene" path="res://scenes/shell/SystemDock.tscn" id="4"]
[ext_resource type="PackedScene" path="res://scenes/shell/NarrationBook.tscn" id="5"]
[node name="MainWindowShell" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 1920.0
offset_bottom = 1080.0
theme = ExtResource("2")
script = ExtResource("1")
[node name="Split" type="HBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_constants/separation = 0
[node name="World" type="Control" parent="Split"]
layout_mode = 2
size_flags_horizontal = 3
clip_contents = true
[node name="Bay" parent="Split/World" instance=ExtResource("3")]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Caption" type="Label" parent="Split/World"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
grow_horizontal = 2
grow_vertical = 2
theme_type_variation = &"Mono"
text = "ISOMETRIC WORLD VIEW"
[node name="TurnRail" type="PanelContainer" parent="Split/World"]
layout_mode = 1
anchors_preset = 0
offset_left = 28.0
offset_top = 28.0
offset_right = 300.0
offset_bottom = 128.0
theme_type_variation = &"DarkPanel"
[node name="Col" type="VBoxContainer" parent="Split/World/TurnRail"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="Title" type="Label" parent="Split/World/TurnRail/Col"]
layout_mode = 2
theme_type_variation = &"Mono"
text = "TURN ORDER"
[node name="Tokens" type="HBoxContainer" parent="Split/World/TurnRail/Col"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="T0" type="Label" parent="Split/World/TurnRail/Col/Tokens"]
layout_mode = 2
theme_type_variation = &"Accent"
text = "EL"
[node name="T1" type="Label" parent="Split/World/TurnRail/Col/Tokens"]
layout_mode = 2
theme_type_variation = &"Accent"
text = "DW"
[node name="T2" type="Label" parent="Split/World/TurnRail/Col/Tokens"]
layout_mode = 2
theme_type_variation = &"Accent"
text = "HU"
[node name="T3" type="Label" parent="Split/World/TurnRail/Col/Tokens"]
layout_mode = 2
theme_type_variation = &"Accent"
text = "✕"
[node name="T4" type="Label" parent="Split/World/TurnRail/Col/Tokens"]
layout_mode = 2
theme_type_variation = &"Accent"
text = "—"
[node name="Minimap" type="PanelContainer" parent="Split/World"]
custom_minimum_size = Vector2(200, 200)
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -228.0
offset_top = 28.0
offset_right = -28.0
offset_bottom = 228.0
grow_horizontal = 0
theme_type_variation = &"DarkPanel"
[node name="Label" type="Label" parent="Split/World/Minimap"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 1
theme_type_variation = &"Mono"
text = "MINIMAP"
[node name="SystemDock" parent="Split/World" instance=ExtResource("4")]
layout_mode = 1
anchors_preset = 6
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -268.0
offset_top = -210.0
offset_right = -28.0
offset_bottom = 210.0
grow_horizontal = 0
grow_vertical = 2
[node name="CommandBar" type="HBoxContainer" parent="Split/World"]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 28.0
offset_top = -150.0
offset_right = -28.0
grow_vertical = 0
theme_override_constants/separation = 24
alignment = 1
[node name="Vitals" type="VBoxContainer" parent="Split/World/CommandBar"]
custom_minimum_size = Vector2(230, 0)
layout_mode = 2
size_flags_vertical = 8
theme_override_constants/separation = 8
[node name="HP" type="Label" parent="Split/World/CommandBar/Vitals"]
layout_mode = 2
theme_type_variation = &"Mono"
text = "HP 42 / 60"
[node name="MP" type="Label" parent="Split/World/CommandBar/Vitals"]
layout_mode = 2
theme_type_variation = &"Mono"
text = "MP 18 / 30"
[node name="Gold" type="Label" parent="Split/World/CommandBar/Vitals"]
layout_mode = 2
theme_type_variation = &"Mono"
text = "◈ 1240 gp"
[node name="Consumables" type="HBoxContainer" parent="Split/World/CommandBar"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 8
theme_override_constants/separation = 9
alignment = 1
[node name="Slot1" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTile"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot1"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
text = "Salve"
[node name="Slot2" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTile"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot2"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
text = "Draught"
[node name="Slot3" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTile"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot3"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
text = "Bomb"
[node name="Slot4" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTile"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot4"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
text = "Antidote"
[node name="Slot5" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTile"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot5"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
text = "Ration"
[node name="Slot6" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTile"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot6"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
text = "Torch"
[node name="Slot7" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTileEmpty"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot7"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
[node name="Slot8" type="PanelContainer" parent="Split/World/CommandBar/Consumables"]
custom_minimum_size = Vector2(62, 62)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"ItemTileEmpty"
[node name="Label" type="Label" parent="Split/World/CommandBar/Consumables/Slot8"]
layout_mode = 2
horizontal_alignment = 1
vertical_alignment = 2
theme_type_variation = &"Mono"
[node name="EndTurn" type="Button" parent="Split/World/CommandBar"]
custom_minimum_size = Vector2(150, 60)
layout_mode = 2
size_flags_vertical = 4
theme_type_variation = &"PrimaryCTA"
text = "END TURN"
[node name="BookPanel" type="PanelContainer" parent="Split"]
custom_minimum_size = Vector2(740, 0)
layout_mode = 2
theme_type_variation = &"ParchmentCard"
[node name="Book" parent="Split/BookPanel" instance=ExtResource("5")]
layout_mode = 2

View File

@@ -1,6 +1,85 @@
[gd_scene load_steps=2 format=3]
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scripts/ui/shell/narration_book.gd" id="1"]
[ext_resource type="Theme" path="res://assets/theme/game_theme.tres" id="2"]
[node name="NarrationBook" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 740.0
offset_bottom = 1080.0
theme = ExtResource("2")
script = ExtResource("1")
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_constants/margin_left = 40
theme_override_constants/margin_top = 34
theme_override_constants/margin_right = 40
theme_override_constants/margin_bottom = 34
[node name="Col" type="VBoxContainer" parent="Margin"]
layout_mode = 2
theme_override_constants/separation = 18
[node name="Header" type="Label" parent="Margin/Col"]
layout_mode = 2
theme_type_variation = &"Accent"
text = "ROUND 4 · THE LOWER WARD"
[node name="Prose" type="RichTextLabel" parent="Margin/Col"]
custom_minimum_size = Vector2(660, 320)
layout_mode = 2
size_flags_vertical = 3
bbcode_enabled = true
text = "The alley stinks of piss and rot. Rain beads on the cobbles and runs black into the gutter."
fit_content = true
[node name="DegradedTag" type="Label" parent="Margin/Col"]
visible = false
layout_mode = 2
theme_type_variation = &"Mono"
text = "THE MASTER'S VOICE COMES THIN"
[node name="Prompt" type="Label" parent="Margin/Col"]
layout_mode = 2
theme_type_variation = &"Accent"
text = "▸ Your turn. What do you do?"
[node name="Choice1" type="Button" parent="Margin/Col"]
layout_mode = 2
disabled = true
theme_type_variation = &"ParchmentButton"
text = "1 · Pay the fence double and keep your hand where he can see it"
alignment = 0
[node name="Choice2" type="Button" parent="Margin/Col"]
layout_mode = 2
disabled = true
theme_type_variation = &"ParchmentButton"
text = "2 · Draw your blade — he's closer than his guards"
alignment = 0
[node name="Choice3" type="Button" parent="Margin/Col"]
layout_mode = 2
disabled = true
theme_type_variation = &"ParchmentButton"
text = "3 · Lie — tell him the rest is coming with your dwarf"
alignment = 0
[node name="FreeText" type="Label" parent="Margin/Col"]
layout_mode = 2
theme_type_variation = &"Mono"
text = "⌨ …or describe your own action"
[node name="Refire" type="Button" parent="Margin/Col"]
layout_mode = 2
theme_type_variation = &"ParchmentButton"
text = "▸ The Master continues…"
[node name="Status" type="Label" parent="Margin/Col"]
layout_mode = 2
theme_type_variation = &"Mono"

View File

@@ -1,6 +1,86 @@
[gd_scene load_steps=2 format=3]
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://scripts/ui/shell/system_dock.gd" id="1"]
[ext_resource type="Theme" path="res://assets/theme/game_theme.tres" id="2"]
[node name="SystemDock" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 240.0
offset_bottom = 420.0
theme = ExtResource("2")
script = ExtResource("1")
[node name="Menu" type="VBoxContainer" parent="."]
layout_mode = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -190.0
offset_right = -10.0
offset_bottom = 316.0
grow_horizontal = 0
theme_override_constants/separation = 10
[node name="Inventory" type="Button" parent="Menu"]
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_type_variation = &"DockButton"
text = "I Inventory"
alignment = 0
metadata/screen_id = &"inventory"
[node name="Character" type="Button" parent="Menu"]
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_type_variation = &"DockButton"
text = "C Character"
alignment = 0
metadata/screen_id = &"character"
[node name="QuestLog" type="Button" parent="Menu"]
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_type_variation = &"DockButton"
text = "J Quest Log"
alignment = 0
metadata/screen_id = &"quest_log"
[node name="Spellbook" type="Button" parent="Menu"]
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_type_variation = &"DockButton"
text = "K Spellbook"
alignment = 0
metadata/screen_id = &"spellbook"
[node name="WorldMap" type="Button" parent="Menu"]
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_type_variation = &"DockButton"
text = "M World Map"
alignment = 0
metadata/screen_id = &"world_map"
[node name="Party" type="Button" parent="Menu"]
custom_minimum_size = Vector2(0, 42)
layout_mode = 2
theme_type_variation = &"DockButton"
text = "P Party"
alignment = 0
metadata/screen_id = &"party"
[node name="Handle" type="Button" parent="."]
custom_minimum_size = Vector2(26, 104)
layout_mode = 1
anchor_left = 1.0
anchor_top = 0.5
anchor_right = 1.0
anchor_bottom = 0.5
offset_left = -222.0
offset_top = -52.0
offset_right = -196.0
offset_bottom = 52.0
grow_horizontal = 0
grow_vertical = 2
theme_type_variation = &"TabActive"
text = ""

View File

@@ -5,13 +5,13 @@ extends Control
## command bar) + the permanent DM narration book. §2: the shell owns HUD state
## (ShellState) and consumes DM text (never owns narrative truth). It reuses the
## proven Theme (M3-a) and DM loop (M2) — nothing here is reinvented.
const GAME_THEME := "res://assets/theme/game_theme.tres"
const DARK_BAY := "res://scenes/theme/surfaces/DarkBay.tscn"
const SYSTEM_DOCK := "res://scenes/shell/SystemDock.tscn"
const NARRATION_BOOK := "res://scenes/shell/NarrationBook.tscn"
const WORLD_WIDTH := 1180.0
const BOOK_WIDTH := 740.0
##
## Layout is authored in MainWindowShell.tscn (and its SystemDock/NarrationBook
## sub-scenes) — open it in the editor to arrange it. This script binds the seed
## ShellState into the authored nodes, wires the DM loop, and keeps the runtime
## viewport-fit. The turn-order tokens and consumable slots are a FIXED set of
## authored nodes bound from state (extra slots hide); combat/inventory become the
## writers later.
# HttpTransport carries no class_name (unlike DmService/FallbackLibrary) — every
# other call site (narrate_harness.gd, test_net_primitives.gd) reaches it via
@@ -20,23 +20,29 @@ const HttpTransport = preload("res://scripts/net/http_transport.gd")
var service: DmService = null # injection seam; real HttpTransport built if null
var book: NarrationBook
var dock: SystemDock
var _state: ShellState
var _log: CanonLog
var _http: HTTPRequest
@onready var book: NarrationBook = $Split/BookPanel/Book
@onready var dock: SystemDock = $Split/World/SystemDock
@onready var _tokens: HBoxContainer = $Split/World/TurnRail/Col/Tokens
@onready var _hp: Label = $Split/World/CommandBar/Vitals/HP
@onready var _mp: Label = $Split/World/CommandBar/Vitals/MP
@onready var _gold: Label = $Split/World/CommandBar/Vitals/Gold
@onready var _consumables: HBoxContainer = $Split/World/CommandBar/Consumables
func _ready() -> void:
theme = load(GAME_THEME)
# Root Control is not sized by full-rect anchors under F6 — size explicitly,
# and no anchors_preset on the .tscn root (M3-a collapse bug).
# The theme is assigned on the scene root in the editor (so it previews). Keep
# the runtime viewport-fit: a root Control run via F6 is not sized by full-rect
# anchors — the .tscn root uses a fixed size + no full-rect preset (M3-a bug).
_fit_to_viewport()
get_viewport().size_changed.connect(_fit_to_viewport)
_state = ShellState.seed()
_log = _build_seed_log()
_build_layout()
_bind_state()
if service == null:
_http = HTTPRequest.new()
@@ -53,88 +59,24 @@ func _fit_to_viewport() -> void:
size = get_viewport_rect().size
func _build_layout() -> void:
var split := HBoxContainer.new()
split.set_anchors_preset(Control.PRESET_FULL_RECT)
split.add_theme_constant_override("separation", 0)
add_child(split)
var world := _build_world()
world.size_flags_horizontal = Control.SIZE_EXPAND_FILL
split.add_child(world)
book = load(NARRATION_BOOK).instantiate()
book.custom_minimum_size = Vector2(BOOK_WIDTH, 0)
var book_panel := PanelContainer.new()
book_panel.theme_type_variation = ThemeKeys.PARCHMENT_CARD
book_panel.custom_minimum_size = Vector2(BOOK_WIDTH, 0)
book_panel.add_child(book)
split.add_child(book_panel)
func _bind_state() -> void:
_bind_turn_rail()
_bind_vitals()
_bind_consumables()
func _build_world() -> Control:
var world := Control.new()
world.custom_minimum_size = Vector2(WORLD_WIDTH, 0)
world.clip_contents = true
var bay = load(DARK_BAY).instantiate()
bay.set_anchors_preset(Control.PRESET_FULL_RECT)
world.add_child(bay)
var caption := Label.new()
caption.theme_type_variation = ThemeKeys.MONO
caption.text = "ISOMETRIC WORLD VIEW"
caption.set_anchors_preset(Control.PRESET_CENTER)
world.add_child(caption)
var rail := _build_turn_rail()
rail.position = Vector2(28, 28)
world.add_child(rail)
var minimap := PanelContainer.new()
minimap.theme_type_variation = ThemeKeys.DARK_PANEL
minimap.custom_minimum_size = Vector2(200, 200)
minimap.set_anchors_preset(Control.PRESET_TOP_RIGHT)
minimap.position = Vector2(-228, 28)
var minimap_label := Label.new()
minimap_label.theme_type_variation = ThemeKeys.MONO
minimap_label.text = "MINIMAP"
minimap.add_child(minimap_label)
world.add_child(minimap)
dock = load(SYSTEM_DOCK).instantiate()
dock.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
dock.position = Vector2(-260, 0)
world.add_child(dock)
var command_bar := _build_command_bar()
command_bar.set_anchors_preset(Control.PRESET_BOTTOM_WIDE)
command_bar.position = Vector2(0, -150)
world.add_child(command_bar)
return world
func _build_turn_rail() -> Control:
var panel := PanelContainer.new()
panel.theme_type_variation = ThemeKeys.DARK_PANEL
var col := VBoxContainer.new()
col.add_theme_constant_override("separation", 8)
panel.add_child(col)
var title := Label.new()
title.theme_type_variation = ThemeKeys.MONO
title.text = "TURN ORDER"
col.add_child(title)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 12)
col.add_child(row)
for e in _state.turn_order:
var token := Label.new()
token.theme_type_variation = ThemeKeys.ACCENT
token.text = "" if e.is_downed else e.initials
token.add_theme_color_override("font_color", _side_color(e))
row.add_child(token)
return panel
func _bind_turn_rail() -> void:
# Fill the authored token labels from the turn order; hide any spare slots.
var tokens := _tokens.get_children()
for i in range(tokens.size()):
var label: Label = tokens[i]
if i < _state.turn_order.size():
var e: TurnEntry = _state.turn_order[i]
label.text = "" if e.is_downed else e.initials
label.add_theme_color_override("font_color", _side_color(e))
label.visible = true
else:
label.visible = false
func _side_color(e: TurnEntry) -> Color:
@@ -148,46 +90,26 @@ func _side_color(e: TurnEntry) -> Color:
_: return Palette.CREAM
func _build_command_bar() -> Control:
var bar := HBoxContainer.new()
bar.add_theme_constant_override("separation", 24)
bar.custom_minimum_size = Vector2(0, 150)
func _bind_vitals() -> void:
_hp.text = "HP %d / %d" % [_state.vitals["hp"], _state.vitals["hp_max"]]
_mp.text = "MP %d / %d" % [_state.vitals["mp"], _state.vitals["mp_max"]]
_gold.text = "%d gp" % _state.vitals["gold"]
_gold.add_theme_color_override("font_color", Palette.GOLD_BRIGHT)
var vitals := VBoxContainer.new()
var hp := Label.new()
hp.theme_type_variation = ThemeKeys.MONO
hp.text = "HP %d / %d" % [_state.vitals["hp"], _state.vitals["hp_max"]]
var mp := Label.new()
mp.theme_type_variation = ThemeKeys.MONO
mp.text = "MP %d / %d" % [_state.vitals["mp"], _state.vitals["mp_max"]]
var gold := Label.new()
gold.theme_type_variation = ThemeKeys.MONO
gold.add_theme_color_override("font_color", Palette.GOLD_BRIGHT)
gold.text = "%d gp" % _state.vitals["gold"]
for l in [hp, mp, gold]:
vitals.add_child(l)
bar.add_child(vitals)
var slots := HBoxContainer.new()
slots.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slots.add_theme_constant_override("separation", 9)
for c in _state.consumables:
var slot := PanelContainer.new()
slot.theme_type_variation = ThemeKeys.ITEM_TILE if c["label"] != "" else ThemeKeys.ITEM_TILE_EMPTY
slot.custom_minimum_size = Vector2(62, 62)
var name_label := Label.new()
name_label.theme_type_variation = ThemeKeys.MONO
name_label.text = c["label"]
slot.add_child(name_label)
slots.add_child(slot)
bar.add_child(slots)
var end_turn := Button.new()
end_turn.theme_type_variation = ThemeKeys.PRIMARY_CTA
end_turn.text = "END TURN"
bar.add_child(end_turn)
return bar
func _bind_consumables() -> void:
# The 8 slots are authored; bind labels + fill/empty variation, hide spares.
var slots := _consumables.get_children()
for i in range(slots.size()):
var slot: PanelContainer = slots[i]
var label: Label = slot.get_node("Label")
if i < _state.consumables.size():
var c: Dictionary = _state.consumables[i]
label.text = c["label"]
slot.theme_type_variation = ThemeKeys.ITEM_TILE if c["label"] != "" else ThemeKeys.ITEM_TILE_EMPTY
slot.visible = true
else:
slot.visible = false
func _on_screen_requested(_id: StringName) -> void:

View File

@@ -5,75 +5,30 @@ extends Control
## 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.
##
## Layout is authored in NarrationBook.tscn (editor-previewable). This script only
## binds the header, drives the narrate loop, and swaps prose text.
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
@onready var _header: Label = $Margin/Col/Header
@onready var _prose: RichTextLabel = $Margin/Col/Prose
@onready var _degraded_tag: Label = $Margin/Col/DegradedTag
@onready var _refire: Button = $Margin/Col/Refire
@onready var _status: Label = $Margin/Col/Status
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)
# The considering indicator is a non-visual Node (a Timer + label driver), so
# it stays code-created rather than authored in the scene.
_indicator = ConsideringIndicator.new()
add_child(_indicator)
_refire.pressed.connect(_on_refire_pressed)
func setup(service: DmService, log: CanonLog, state: ShellState) -> void:

View File

@@ -4,44 +4,37 @@ extends Control
## buttons in/out. §2: state (owns the open toggle via ShellState). The buttons
## are the routing seam — they emit screen_requested; the screens they open land
## in later milestones, so the signal is currently a no-op for the shell.
##
## Layout is authored in SystemDock.tscn (editor-previewable). This script only
## wires the buttons' signals, drives the slide animation, and toggles state. Each
## dock button carries a `screen_id` StringName in its scene metadata; add/reorder
## buttons in the editor and the wiring follows.
signal screen_requested(id: StringName)
const SCREENS := [
[&"inventory", "Inventory", "I"],
[&"character", "Character", "C"],
[&"quest_log", "Quest Log", "J"],
[&"spellbook", "Spellbook", "K"],
[&"world_map", "World Map", "M"],
[&"party", "Party", "P"],
]
const SLIDE_SECONDS := 0.32
const HIDDEN_SHIFT := 230.0
var _screen_buttons: Dictionary = {}
var _state: ShellState
var _menu: VBoxContainer
var _handle: Button
var _menu_open_pos: Vector2
@onready var _menu: VBoxContainer = $Menu
@onready var _handle: Button = $Handle
func _ready() -> void:
_menu = VBoxContainer.new()
_menu.add_theme_constant_override("separation", 10)
add_child(_menu)
for entry in SCREENS:
var id: StringName = entry[0]
var button := Button.new()
button.text = "%s %s" % [entry[2], entry[1]]
button.theme_type_variation = ThemeKeys.DOCK_BUTTON
button.pressed.connect(func(): screen_requested.emit(id))
_menu.add_child(button)
_screen_buttons[id] = button
_handle = Button.new()
_handle.text = ""
_handle.theme_type_variation = ThemeKeys.TAB_ACTIVE
# The buttons live in the scene; harvest each one's authored screen_id so the
# routing seam follows editor edits (add/remove/reorder needs no code change).
for child in _menu.get_children():
if child is Button and child.has_meta("screen_id"):
var id: StringName = child.get_meta("screen_id")
child.pressed.connect(func(): screen_requested.emit(id))
_screen_buttons[id] = child
_handle.pressed.connect(_on_handle_pressed)
add_child(_handle)
# Capture the authored open position so the slide is relative to wherever the
# menu is placed in the editor — moving it in the editor just works.
_menu_open_pos = _menu.position
func setup(state: ShellState) -> void:
@@ -57,7 +50,7 @@ func _on_handle_pressed() -> void:
func _apply_open(open: bool) -> void:
_handle.text = "" if open else ""
var target := Vector2.ZERO if open else Vector2(HIDDEN_SHIFT, 0.0)
var target := _menu_open_pos if open else _menu_open_pos + Vector2(HIDDEN_SHIFT, 0.0)
var alpha := 1.0 if open else 0.0
if not is_inside_tree():
_menu.position = target