diff --git a/client/scenes/shell/MainWindowShell.tscn b/client/scenes/shell/MainWindowShell.tscn index 5ff2349..389b409 100644 --- a/client/scenes/shell/MainWindowShell.tscn +++ b/client/scenes/shell/MainWindowShell.tscn @@ -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 diff --git a/client/scenes/shell/NarrationBook.tscn b/client/scenes/shell/NarrationBook.tscn index 465a65b..d46908d 100644 --- a/client/scenes/shell/NarrationBook.tscn +++ b/client/scenes/shell/NarrationBook.tscn @@ -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" diff --git a/client/scenes/shell/SystemDock.tscn b/client/scenes/shell/SystemDock.tscn index f984bdc..a281f7d 100644 --- a/client/scenes/shell/SystemDock.tscn +++ b/client/scenes/shell/SystemDock.tscn @@ -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 = "›" diff --git a/client/scripts/ui/shell/main_window_shell.gd b/client/scripts/ui/shell/main_window_shell.gd index ebacf2c..fcdce62 100644 --- a/client/scripts/ui/shell/main_window_shell.gd +++ b/client/scripts/ui/shell/main_window_shell.gd @@ -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: diff --git a/client/scripts/ui/shell/narration_book.gd b/client/scripts/ui/shell/narration_book.gd index d1a4626..0dbec7e 100644 --- a/client/scripts/ui/shell/narration_book.gd +++ b/client/scripts/ui/shell/narration_book.gd @@ -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: diff --git a/client/scripts/ui/shell/system_dock.gd b/client/scripts/ui/shell/system_dock.gd index 282bd88..b42d05e 100644 --- a/client/scripts/ui/shell/system_dock.gd +++ b/client/scripts/ui/shell/system_dock.gd @@ -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