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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = "›"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user