feat(shell): SystemDock slide-in with inert screen-routing seam

This commit is contained in:
2026-07-11 08:51:14 -05:00
parent c4aa74da5a
commit 9a02c9b038
5 changed files with 108 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/ui/shell/system_dock.gd" id="1"]
[node name="SystemDock" type="Control"]
script = ExtResource("1")

View File

@@ -0,0 +1,68 @@
class_name SystemDock
extends Control
## The slide-in system dock (mock 2a): a gold handle toggles a column of screen
## 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.
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
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.DARK_PANEL
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
_handle.pressed.connect(_on_handle_pressed)
add_child(_handle)
func setup(state: ShellState) -> void:
_state = state
_apply_open(_state.dock_open)
func _on_handle_pressed() -> void:
if _state == null:
return
_apply_open(_state.toggle_dock())
func _apply_open(open: bool) -> void:
_handle.text = "" if open else ""
var target := Vector2.ZERO if open else Vector2(HIDDEN_SHIFT, 0.0)
var alpha := 1.0 if open else 0.0
if not is_inside_tree():
_menu.position = target
_menu.modulate.a = alpha
return
var tween := create_tween().set_parallel(true)
tween.tween_property(_menu, "position", target, SLIDE_SECONDS)
tween.tween_property(_menu, "modulate:a", alpha, SLIDE_SECONDS)

View File

@@ -0,0 +1 @@
uid://dvbg24rvye3qw

View File

@@ -0,0 +1,32 @@
extends "res://addons/gut/test.gd"
const SCENE := "res://scenes/shell/SystemDock.tscn"
func _dock() -> SystemDock:
var d = load(SCENE).instantiate()
add_child_autofree(d) # _ready() builds the buttons
return d
func test_builds_all_six_screen_buttons():
var d := _dock()
assert_eq(d._screen_buttons.size(), 6)
assert_true(d._screen_buttons.has(&"spellbook"), "spellbook kept for mock fidelity")
assert_true(d._screen_buttons.has(&"party"))
func test_pressing_a_button_emits_its_id():
var d := _dock()
watch_signals(d)
d._screen_buttons[&"inventory"].emit_signal("pressed")
assert_signal_emitted_with_parameters(d, "screen_requested", [&"inventory"])
func test_handle_toggles_state():
var d := _dock()
var state := ShellState.seed()
d.setup(state)
assert_true(state.dock_open)
d._on_handle_pressed()
assert_false(state.dock_open, "handle flips the shared state")

View File

@@ -0,0 +1 @@
uid://dllvm2kmia00h