From 2227d703f60e683124e8b860c185a676f04fc66d Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Mon, 13 Jul 2026 15:03:24 -0500 Subject: [PATCH] feat(creation): the character creation screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Variant C of the mock: seven nameplate calling cards in one row, with the chosen calling's blurb AND mechanics in a detail panel below — the one card you committed to is the only one that talks. Editor-first (ADR 0001): the tree is in the .tscn, the script binds. No rules here — CreationDraft holds them, CreationCopy derives the display strings, the prose comes from ContentDB. Emits creation_confirmed(Dictionary); M4-c constructs. Five ability cards. Never six (§7). --- client/scenes/creation/CharacterCreation.tscn | 1207 +++++++++++++++++ .../scripts/ui/creation/character_creation.gd | 305 +++++ .../ui/creation/character_creation.gd.uid | 1 + .../unit/test_character_creation_screen.gd | 105 ++ .../test_character_creation_screen.gd.uid | 1 + 5 files changed, 1619 insertions(+) create mode 100644 client/scenes/creation/CharacterCreation.tscn create mode 100644 client/scripts/ui/creation/character_creation.gd create mode 100644 client/scripts/ui/creation/character_creation.gd.uid create mode 100644 client/tests/unit/test_character_creation_screen.gd create mode 100644 client/tests/unit/test_character_creation_screen.gd.uid diff --git a/client/scenes/creation/CharacterCreation.tscn b/client/scenes/creation/CharacterCreation.tscn new file mode 100644 index 0000000..0636818 --- /dev/null +++ b/client/scenes/creation/CharacterCreation.tscn @@ -0,0 +1,1207 @@ +[gd_scene load_steps=5 format=3] + +[ext_resource type="Script" path="res://scripts/ui/creation/character_creation.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/theme/surfaces/ParchmentPanel.tscn" id="4"] + +[node name="CharacterCreation" 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="Bay" type="Control" parent="Split"] +custom_minimum_size = Vector2(660, 1080) +layout_mode = 2 + +[node name="Backdrop" parent="Split/Bay" instance=ExtResource("3")] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Col" type="VBoxContainer" parent="Split/Bay"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 34.0 +offset_top = 30.0 +offset_right = -34.0 +offset_bottom = -34.0 +theme_override_constants/separation = 16 + +[node name="Kicker" type="Label" parent="Split/Bay/Col"] +layout_mode = 2 +theme_type_variation = &"Mono" +text = "WHO WILL YOU BE?" + +[node name="ModelSlot" type="PanelContainer" parent="Split/Bay/Col"] +custom_minimum_size = Vector2(0, 540) +layout_mode = 2 +size_flags_vertical = 3 +theme_type_variation = &"ItemTileEmpty" + +[node name="Label" type="Label" parent="Split/Bay/Col/ModelSlot"] +layout_mode = 2 +horizontal_alignment = 1 +vertical_alignment = 1 +theme_type_variation = &"Mono" +text = "CHARACTER +MODEL" + +[node name="Nameplate" type="VBoxContainer" parent="Split/Bay/Col"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="Name" type="Label" parent="Split/Bay/Col/Nameplate"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"Accent" +theme_override_font_sizes/font_size = 34 +text = "Nameless" + +[node name="Sub" type="Label" parent="Split/Bay/Col/Nameplate"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"TitleKicker" +text = "Human · Sellsword" + +[node name="OriginCard" type="PanelContainer" parent="Split/Bay/Col"] +layout_mode = 2 +theme_type_variation = &"ParchmentCard" + +[node name="Box" type="VBoxContainer" parent="Split/Bay/Col/OriginCard"] +layout_mode = 2 +theme_override_constants/separation = 7 + +[node name="Kicker" type="Label" parent="Split/Bay/Col/OriginCard/Box"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "THE DM SIZES YOU UP" + +[node name="Text" type="RichTextLabel" parent="Split/Bay/Col/OriginCard/Box"] +custom_minimum_size = Vector2(0, 96) +layout_mode = 2 +bbcode_enabled = true +fit_content = true +text = "[i]The DM has not yet decided what you are.[/i]" + +[node name="Sheet" type="Control" parent="Split"] +custom_minimum_size = Vector2(1260, 1080) +layout_mode = 2 + +[node name="Backdrop" parent="Split/Sheet" instance=ExtResource("4")] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="Body" type="VBoxContainer" parent="Split/Sheet"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 44.0 +offset_top = 30.0 +offset_right = -44.0 +offset_bottom = -30.0 +theme_override_constants/separation = 16 + +[node name="Header" type="VBoxContainer" parent="Split/Sheet/Body"] +layout_mode = 2 + +[node name="Title" type="Label" parent="Split/Sheet/Body/Header"] +layout_mode = 2 +theme_type_variation = &"Heading" +text = "Create Your Character" + +[node name="Kicker" type="Label" parent="Split/Sheet/Body/Header"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "THE WORLD HAS REAL PROBLEMS AND DOES NOT CARE ABOUT YOU" + +[node name="RaceSection" type="VBoxContainer" parent="Split/Sheet/Body"] +layout_mode = 2 +theme_override_constants/separation = 9 + +[node name="Label" type="Label" parent="Split/Sheet/Body/RaceSection"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "RACE" + +[node name="Cards" type="HBoxContainer" parent="Split/Sheet/Body/RaceSection"] +layout_mode = 2 +theme_override_constants/separation = 14 + +[node name="Race0" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"] +custom_minimum_size = Vector2(0, 108) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/RaceSection/Cards/Race0"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 15.0 +offset_top = 13.0 +offset_right = -15.0 +offset_bottom = -13.0 +mouse_filter = 2 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race0/Box"] +layout_mode = 2 +mouse_filter = 2 +text = "Human" + +[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race0/Box"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +autowrap_mode = 3 +theme_override_font_sizes/font_size = 14 +text = "…" + +[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race0/Box"] +layout_mode = 2 +mouse_filter = 2 +autowrap_mode = 3 +theme_type_variation = &"SectionLabel" +text = "…" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race0"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -78.0 +offset_top = -11.0 +offset_right = -12.0 +offset_bottom = 9.0 +grow_horizontal = 0 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Race1" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"] +custom_minimum_size = Vector2(0, 108) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/RaceSection/Cards/Race1"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 15.0 +offset_top = 13.0 +offset_right = -15.0 +offset_bottom = -13.0 +mouse_filter = 2 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race1/Box"] +layout_mode = 2 +mouse_filter = 2 +text = "Elf" + +[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race1/Box"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +autowrap_mode = 3 +theme_override_font_sizes/font_size = 14 +text = "…" + +[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race1/Box"] +layout_mode = 2 +mouse_filter = 2 +autowrap_mode = 3 +theme_type_variation = &"SectionLabel" +text = "…" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race1"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -78.0 +offset_top = -11.0 +offset_right = -12.0 +offset_bottom = 9.0 +grow_horizontal = 0 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Race2" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"] +custom_minimum_size = Vector2(0, 108) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/RaceSection/Cards/Race2"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 15.0 +offset_top = 13.0 +offset_right = -15.0 +offset_bottom = -13.0 +mouse_filter = 2 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race2/Box"] +layout_mode = 2 +mouse_filter = 2 +text = "Dwarf" + +[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race2/Box"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +autowrap_mode = 3 +theme_override_font_sizes/font_size = 14 +text = "…" + +[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race2/Box"] +layout_mode = 2 +mouse_filter = 2 +autowrap_mode = 3 +theme_type_variation = &"SectionLabel" +text = "…" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race2"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -78.0 +offset_top = -11.0 +offset_right = -12.0 +offset_bottom = 9.0 +grow_horizontal = 0 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Race3" type="Button" parent="Split/Sheet/Body/RaceSection/Cards"] +custom_minimum_size = Vector2(0, 108) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/RaceSection/Cards/Race3"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 15.0 +offset_top = 13.0 +offset_right = -15.0 +offset_bottom = -13.0 +mouse_filter = 2 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race3/Box"] +layout_mode = 2 +mouse_filter = 2 +text = "Beastfolk" + +[node name="Blurb" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race3/Box"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +autowrap_mode = 3 +theme_override_font_sizes/font_size = 14 +text = "…" + +[node name="Trait" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race3/Box"] +layout_mode = 2 +mouse_filter = 2 +autowrap_mode = 3 +theme_type_variation = &"SectionLabel" +text = "…" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/RaceSection/Cards/Race3"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -78.0 +offset_top = -11.0 +offset_right = -12.0 +offset_bottom = 9.0 +grow_horizontal = 0 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="CallingSection" type="VBoxContainer" parent="Split/Sheet/Body"] +layout_mode = 2 +theme_override_constants/separation = 9 + +[node name="Label" type="Label" parent="Split/Sheet/Body/CallingSection"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "CALLING" + +[node name="Cards" type="HBoxContainer" parent="Split/Sheet/Body/CallingSection"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Calling0" type="Button" parent="Split/Sheet/Body/CallingSection/Cards"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Cards/Calling0"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling0/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 16 +text = "Sellsword" + +[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling0/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "STR" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling0"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -33.0 +offset_top = -11.0 +offset_right = 33.0 +offset_bottom = 9.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Calling1" type="Button" parent="Split/Sheet/Body/CallingSection/Cards"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Cards/Calling1"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling1/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 16 +text = "Reaver" + +[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling1/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "STR" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling1"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -33.0 +offset_top = -11.0 +offset_right = 33.0 +offset_bottom = 9.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Calling2" type="Button" parent="Split/Sheet/Body/CallingSection/Cards"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Cards/Calling2"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling2/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 16 +text = "Cutpurse" + +[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling2/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "DEX" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling2"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -33.0 +offset_top = -11.0 +offset_right = 33.0 +offset_bottom = 9.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Calling3" type="Button" parent="Split/Sheet/Body/CallingSection/Cards"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Cards/Calling3"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling3/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 16 +text = "Trapper" + +[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling3/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "DEX" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling3"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -33.0 +offset_top = -11.0 +offset_right = 33.0 +offset_bottom = 9.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Calling4" type="Button" parent="Split/Sheet/Body/CallingSection/Cards"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Cards/Calling4"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling4/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 16 +text = "Hedge-Mage" + +[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling4/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "MAG" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling4"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -33.0 +offset_top = -11.0 +offset_right = 33.0 +offset_bottom = 9.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Calling5" type="Button" parent="Split/Sheet/Body/CallingSection/Cards"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Cards/Calling5"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling5/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 16 +text = "Bonesetter" + +[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling5/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "FTH" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling5"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -33.0 +offset_top = -11.0 +offset_right = 33.0 +offset_bottom = 9.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Calling6" type="Button" parent="Split/Sheet/Body/CallingSection/Cards"] +custom_minimum_size = Vector2(0, 64) +layout_mode = 2 +size_flags_horizontal = 3 +toggle_mode = true +theme_type_variation = &"SelectCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Cards/Calling6"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Name" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling6/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_override_font_sizes/font_size = 16 +text = "Bloodsworn" + +[node name="Chip" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling6/Box"] +layout_mode = 2 +mouse_filter = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "FTH" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/CallingSection/Cards/Calling6"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -33.0 +offset_top = -11.0 +offset_right = 33.0 +offset_bottom = 9.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"ChosenTag" +text = "CHOSEN" + +[node name="Detail" type="PanelContainer" parent="Split/Sheet/Body/CallingSection"] +layout_mode = 2 +theme_type_variation = &"ParchmentCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/CallingSection/Detail"] +layout_mode = 2 +theme_override_constants/separation = 7 + +[node name="Blurb" type="RichTextLabel" parent="Split/Sheet/Body/CallingSection/Detail/Box"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +bbcode_enabled = true +fit_content = true +text = "[i]…[/i]" + +[node name="Mechanics" type="Label" parent="Split/Sheet/Body/CallingSection/Detail/Box"] +layout_mode = 2 +autowrap_mode = 3 +theme_type_variation = &"SectionLabel" +text = "…" + +[node name="SkillSection" type="VBoxContainer" parent="Split/Sheet/Body"] +layout_mode = 2 +theme_override_constants/separation = 9 + +[node name="Label" type="Label" parent="Split/Sheet/Body/SkillSection"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "PROFICIENCIES" + +[node name="PoolRow" type="HBoxContainer" parent="Split/Sheet/Body/SkillSection"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Count" type="Label" parent="Split/Sheet/Body/SkillSection/PoolRow"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "0 of 2 chosen" + +[node name="Pool0" type="Button" parent="Split/Sheet/Body/SkillSection/PoolRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "stealth" + +[node name="Pool1" type="Button" parent="Split/Sheet/Body/SkillSection/PoolRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "sleight of hand" + +[node name="Pool2" type="Button" parent="Split/Sheet/Body/SkillSection/PoolRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "acrobatics" + +[node name="Pool3" type="Button" parent="Split/Sheet/Body/SkillSection/PoolRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "athletics" + +[node name="Pool4" type="Button" parent="Split/Sheet/Body/SkillSection/PoolRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "intimidation" + +[node name="Pool5" type="Button" parent="Split/Sheet/Body/SkillSection/PoolRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "perception" + +[node name="BonusRow" type="HBoxContainer" parent="Split/Sheet/Body/SkillSection"] +layout_mode = 2 +theme_override_constants/separation = 10 + +[node name="Label" type="Label" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "HUMAN — ONE MORE, ANY SKILL" + +[node name="Bonus0" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "athletics" + +[node name="Bonus1" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "intimidation" + +[node name="Bonus2" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "stealth" + +[node name="Bonus3" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "sleight of hand" + +[node name="Bonus4" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "acrobatics" + +[node name="Bonus5" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "endurance" + +[node name="Bonus6" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "perception" + +[node name="Bonus7" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "faith lore" + +[node name="Bonus8" type="Button" parent="Split/Sheet/Body/SkillSection/BonusRow"] +layout_mode = 2 +toggle_mode = true +theme_type_variation = &"SkillChip" +text = "sorcery" + +[node name="AbilitySection" type="VBoxContainer" parent="Split/Sheet/Body"] +layout_mode = 2 +theme_override_constants/separation = 9 + +[node name="Head" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection"] +layout_mode = 2 +theme_override_constants/separation = 14 + +[node name="Label" type="Label" parent="Split/Sheet/Body/AbilitySection/Head"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "ABILITY SCORES" + +[node name="Points" type="Label" parent="Split/Sheet/Body/AbilitySection/Head"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "points left: 3" + +[node name="Reroll" type="Button" parent="Split/Sheet/Body/AbilitySection/Head"] +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "⟳ re-roll" + +[node name="Hint" type="Label" parent="Split/Sheet/Body/AbilitySection/Head"] +layout_mode = 2 +theme_type_variation = &"Accent" +theme_override_font_sizes/font_size = 15 +text = "rolled 3d6 — that roll is your floor, spend up only" + +[node name="Cards" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="Ab0" type="PanelContainer" parent="Split/Sheet/Body/AbilitySection/Cards"] +custom_minimum_size = Vector2(0, 132) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ParchmentCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Key" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "STR" + +[node name="Value" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"Accent" +theme_override_font_sizes/font_size = 36 +text = "10" + +[node name="Rolled" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "rolled 10" + +[node name="Buttons" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0/Box"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 6 + +[node name="Minus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "−" + +[node name="Plus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "+" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab0"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -32.0 +offset_top = -10.0 +offset_right = 32.0 +offset_bottom = 8.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"PrimaryTag" +text = "PRIMARY" + +[node name="Ab1" type="PanelContainer" parent="Split/Sheet/Body/AbilitySection/Cards"] +custom_minimum_size = Vector2(0, 132) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ParchmentCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Key" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "DEX" + +[node name="Value" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"Accent" +theme_override_font_sizes/font_size = 36 +text = "10" + +[node name="Rolled" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "rolled 10" + +[node name="Buttons" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1/Box"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 6 + +[node name="Minus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "−" + +[node name="Plus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "+" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab1"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -32.0 +offset_top = -10.0 +offset_right = 32.0 +offset_bottom = 8.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"PrimaryTag" +text = "PRIMARY" + +[node name="Ab2" type="PanelContainer" parent="Split/Sheet/Body/AbilitySection/Cards"] +custom_minimum_size = Vector2(0, 132) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ParchmentCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Key" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "CON" + +[node name="Value" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"Accent" +theme_override_font_sizes/font_size = 36 +text = "10" + +[node name="Rolled" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "rolled 10" + +[node name="Buttons" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2/Box"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 6 + +[node name="Minus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "−" + +[node name="Plus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "+" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab2"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -32.0 +offset_top = -10.0 +offset_right = 32.0 +offset_bottom = 8.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"PrimaryTag" +text = "PRIMARY" + +[node name="Ab3" type="PanelContainer" parent="Split/Sheet/Body/AbilitySection/Cards"] +custom_minimum_size = Vector2(0, 132) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ParchmentCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Key" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "FTH" + +[node name="Value" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"Accent" +theme_override_font_sizes/font_size = 36 +text = "10" + +[node name="Rolled" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "rolled 10" + +[node name="Buttons" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3/Box"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 6 + +[node name="Minus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "−" + +[node name="Plus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "+" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab3"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -32.0 +offset_top = -10.0 +offset_right = 32.0 +offset_bottom = 8.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"PrimaryTag" +text = "PRIMARY" + +[node name="Ab4" type="PanelContainer" parent="Split/Sheet/Body/AbilitySection/Cards"] +custom_minimum_size = Vector2(0, 132) +layout_mode = 2 +size_flags_horizontal = 3 +theme_type_variation = &"ParchmentCard" + +[node name="Box" type="VBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 5 + +[node name="Key" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "MAG" + +[node name="Value" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"Accent" +theme_override_font_sizes/font_size = 36 +text = "10" + +[node name="Rolled" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4/Box"] +layout_mode = 2 +horizontal_alignment = 1 +theme_type_variation = &"SectionLabel" +text = "rolled 10" + +[node name="Buttons" type="HBoxContainer" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4/Box"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 6 + +[node name="Minus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "−" + +[node name="Plus" type="Button" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4/Box/Buttons"] +custom_minimum_size = Vector2(36, 28) +layout_mode = 2 +theme_type_variation = &"ParchmentButton" +text = "+" + +[node name="Tag" type="Label" parent="Split/Sheet/Body/AbilitySection/Cards/Ab4"] +visible = false +layout_mode = 1 +anchors_preset = 1 +anchor_left = 0.5 +anchor_right = 0.5 +offset_left = -32.0 +offset_top = -10.0 +offset_right = 32.0 +offset_bottom = 8.0 +grow_horizontal = 2 +mouse_filter = 2 +theme_type_variation = &"PrimaryTag" +text = "PRIMARY" + +[node name="Footer" type="HBoxContainer" parent="Split/Sheet/Body"] +layout_mode = 2 +size_flags_vertical = 10 +theme_override_constants/separation = 20 + +[node name="NameBox" type="VBoxContainer" parent="Split/Sheet/Body/Footer"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 9 + +[node name="Label" type="Label" parent="Split/Sheet/Body/Footer/NameBox"] +layout_mode = 2 +theme_type_variation = &"SectionLabel" +text = "NAME" + +[node name="NameEdit" type="LineEdit" parent="Split/Sheet/Body/Footer/NameBox"] +layout_mode = 2 +placeholder_text = "What do they call you?" + +[node name="CtaBox" type="VBoxContainer" parent="Split/Sheet/Body/Footer"] +layout_mode = 2 +theme_override_constants/separation = 6 + +[node name="Enter" type="Button" parent="Split/Sheet/Body/Footer/CtaBox"] +custom_minimum_size = Vector2(280, 56) +layout_mode = 2 +disabled = true +theme_type_variation = &"PrimaryCTA" +text = "ENTER THE WORLD" + +[node name="Error" type="Label" parent="Split/Sheet/Body/Footer/CtaBox"] +layout_mode = 2 +horizontal_alignment = 1 +autowrap_mode = 3 +theme_type_variation = &"SectionLabel" +text = "no second chances out there" diff --git a/client/scripts/ui/creation/character_creation.gd b/client/scripts/ui/creation/character_creation.gd new file mode 100644 index 0000000..7ba6308 --- /dev/null +++ b/client/scripts/ui/creation/character_creation.gd @@ -0,0 +1,305 @@ +class_name CharacterCreation +extends Control +## The creation screen (mock: Character Creation). Layout is authored in +## CharacterCreation.tscn — open it in the editor to arrange it (ADR 0001). This +## script does on-load work only: @onready refs, signal wiring, and binding the +## draft into AUTHORED nodes. It creates no nodes and it owns no rules. +## +## Every rule lives in CreationDraft; every display string derived from a rules +## table lives in CreationCopy; the prose lives in ContentDB. This file is glue. +## +## §2: the screen never hands an attribute value to anything. It emits a seed and +## four choices, and NewGame.construct re-rolls from that seed itself. +## §7: LCK appears nowhere here. Five ability cards. Never six. + +signal creation_confirmed(creation: Dictionary) + +const ContentDB = preload("res://scripts/content/content_db.gd") + +## Shown under the CTA when the draft is legal. When it is NOT legal, the first +## validation error takes this slot instead — the player is told what is wrong, in +## the same place, in the same dry voice. §13: even the nag is content. +const CTA_HINT := "no second chances out there" + +## Injection seams — set BETWEEN instantiate() and add_child(), as the shell does +## for DmService. _ready builds the real ones when nothing was supplied. +var world: ContentDB = null +var origin: Dictionary = {} + +var draft: CreationDraft + +var _race_cards: Array = [] +var _calling_cards: Array = [] +var _pool_chips: Array = [] +var _bonus_chips: Array = [] +var _ability_cards: Array = [] + +@onready var _plate_name: Label = $Split/Bay/Col/Nameplate/Name +@onready var _plate_sub: Label = $Split/Bay/Col/Nameplate/Sub +@onready var _origin_text: RichTextLabel = $Split/Bay/Col/OriginCard/Box/Text +@onready var _race_row: HBoxContainer = $Split/Sheet/Body/RaceSection/Cards +@onready var _calling_row: HBoxContainer = $Split/Sheet/Body/CallingSection/Cards +@onready var _detail_blurb: RichTextLabel = $Split/Sheet/Body/CallingSection/Detail/Box/Blurb +@onready var _detail_mech: Label = $Split/Sheet/Body/CallingSection/Detail/Box/Mechanics +@onready var _pool_row: HBoxContainer = $Split/Sheet/Body/SkillSection/PoolRow +@onready var _pool_count: Label = $Split/Sheet/Body/SkillSection/PoolRow/Count +@onready var _bonus_row: HBoxContainer = $Split/Sheet/Body/SkillSection/BonusRow +@onready var _ability_row: HBoxContainer = $Split/Sheet/Body/AbilitySection/Cards +@onready var _points: Label = $Split/Sheet/Body/AbilitySection/Head/Points +@onready var _reroll: Button = $Split/Sheet/Body/AbilitySection/Head/Reroll +@onready var _name_edit: LineEdit = $Split/Sheet/Body/Footer/NameBox/NameEdit +@onready var _enter: Button = $Split/Sheet/Body/Footer/CtaBox/Enter +@onready var _error: Label = $Split/Sheet/Body/Footer/CtaBox/Error + + +func _ready() -> void: + # The .tscn root has a fixed size and NO full-rect anchors — a root Control run + # via F6 is not sized by full-rect anchors and collapses to (0,0) (the M3-a bug). + _fit_to_viewport() + get_viewport().size_changed.connect(_fit_to_viewport) + + if world == null: + world = ContentDB.new() + world.load_from(ContentDB.default_content_root()) + if origin.is_empty(): + origin = ContentDB.load_json(ContentDB.origin_path("deserter")) + + _collect_authored_nodes() + _wire() + + draft = CreationDraft.fresh() + draft.set_race(Races.IDS[0]) + draft.set_calling(_allowed_callings()[0]) + _bind() + + +func _fit_to_viewport() -> void: + size = get_viewport_rect().size + + +func _allowed_callings() -> Array: + ## The ORIGIN gates the callings. The screen does not hardcode seven — a later + ## origin may allow fewer, and construct would reject a calling it disallows. + var allowed: Array = origin.get("build_constraints", {}).get("allowed_callings", []) + var out: Array = [] + for id in Callings.IDS: # Callings.IDS fixes the ORDER; the origin fixes the SET + if id in allowed: + out.append(id) + return out + + +func _collect_authored_nodes() -> void: + # Fixed authored nodes, gathered — never created (ADR 0001). Spares hide. + for i in range(4): + _race_cards.append(_race_row.get_node("Race%d" % i)) + for i in range(7): + _calling_cards.append(_calling_row.get_node("Calling%d" % i)) + for i in range(6): + _pool_chips.append(_pool_row.get_node("Pool%d" % i)) + for i in range(9): + _bonus_chips.append(_bonus_row.get_node("Bonus%d" % i)) + for i in range(5): + _ability_cards.append(_ability_row.get_node("Ab%d" % i)) + + +func _wire() -> void: + for i in range(_race_cards.size()): + _race_cards[i].pressed.connect(_on_race_pressed.bind(i)) + for i in range(_calling_cards.size()): + _calling_cards[i].pressed.connect(_on_calling_pressed.bind(i)) + for i in range(_pool_chips.size()): + _pool_chips[i].pressed.connect(_on_pool_pressed.bind(i)) + for i in range(_bonus_chips.size()): + _bonus_chips[i].pressed.connect(_on_bonus_pressed.bind(i)) + for i in range(_ability_cards.size()): + var card: PanelContainer = _ability_cards[i] + card.get_node("Box/Buttons/Minus").pressed.connect(_on_minus_pressed.bind(i)) + card.get_node("Box/Buttons/Plus").pressed.connect(_on_plus_pressed.bind(i)) + _reroll.pressed.connect(_on_reroll_pressed) + _name_edit.text_changed.connect(_on_name_changed) + _enter.pressed.connect(_on_enter_pressed) + + +# ---------------------------------------------------------------- input -> draft + +func _on_race_pressed(i: int) -> void: + draft.set_race(Races.IDS[i]) + _bind() + + +func _on_calling_pressed(i: int) -> void: + var allowed := _allowed_callings() + if i >= allowed.size(): + return + draft.set_calling(allowed[i]) + _bind() + + +func _on_pool_pressed(i: int) -> void: + var pool := draft.skill_pool() + if i < pool.size(): + draft.toggle_skill(pool[i]) + _bind() + + +func _on_bonus_pressed(i: int) -> void: + draft.set_bonus_skill(Skills.IDS[i]) + _bind() + + +func _on_plus_pressed(i: int) -> void: + draft.increment(Attributes.IDS[i]) + _bind() + + +func _on_minus_pressed(i: int) -> void: + draft.decrement(Attributes.IDS[i]) + _bind() + + +func _on_reroll_pressed() -> void: + draft.reroll() + _bind() + + +func _on_name_changed(text: String) -> void: + draft.name = text + _bind() + + +func _on_enter_pressed() -> void: + if not draft.is_complete(origin, world): + return + creation_confirmed.emit(draft.to_creation()) + + +# ---------------------------------------------------------------- draft -> nodes + +func _bind() -> void: + _bind_races() + _bind_callings() + _bind_skills() + _bind_abilities() + _bind_bay() + _bind_cta() + + +func _bind_races() -> void: + for i in range(_race_cards.size()): + var id: String = Races.IDS[i] + var card: Button = _race_cards[i] + card.get_node("Box/Name").text = str(world.race(id).get("name", id.capitalize())) + card.get_node("Box/Blurb").text = str(world.race(id).get("blurb", "")) + card.get_node("Box/Trait").text = CreationCopy.race_trait(id) + card.button_pressed = (id == draft.race_id) + card.get_node("Tag").visible = card.button_pressed + + +func _bind_callings() -> void: + var allowed := _allowed_callings() + for i in range(_calling_cards.size()): + var card: Button = _calling_cards[i] + if i >= allowed.size(): + card.visible = false # a spare authored slot: hide it (ADR 0001) + continue + var id: String = allowed[i] + card.visible = true + card.get_node("Box/Name").text = str(world.calling(id).get("name", id.capitalize())) + card.get_node("Box/Chip").text = Callings.primary(id).to_upper() + card.button_pressed = (id == draft.calling_id) + card.get_node("Tag").visible = card.button_pressed + + _detail_blurb.text = "[i]%s[/i]" % str(world.calling(draft.calling_id).get("blurb", "")) + _detail_mech.text = CreationCopy.calling_detail(draft.calling_id) + + +func _bind_skills() -> void: + var pool := draft.skill_pool() + _pool_count.text = "%d of %d chosen" % [draft.skills.size(), draft.picks_needed()] + for i in range(_pool_chips.size()): + var chip: Button = _pool_chips[i] + if i >= pool.size(): + chip.visible = false + continue + var skill: String = pool[i] + chip.visible = true + chip.text = CreationCopy.skill_label(skill) + chip.button_pressed = draft.is_picked(skill) + # Inert when the race already granted it, or when the picks are spent. + chip.disabled = draft.is_granted(skill) or (not draft.is_picked(skill) and not draft.can_pick(skill)) + + _bonus_row.visible = draft.wants_bonus_skill() + for i in range(_bonus_chips.size()): + var skill: String = Skills.IDS[i] + var chip: Button = _bonus_chips[i] + chip.text = CreationCopy.skill_label(skill) + chip.button_pressed = (draft.bonus_skill == skill) + chip.disabled = not draft.can_take_bonus(skill) and draft.bonus_skill != skill + + +func _bind_abilities() -> void: + var rolled := draft.rolled() + var final := draft.final() + var primary := Callings.primary(draft.calling_id) + _points.text = "points left: %d" % draft.points_left() + + for i in range(_ability_cards.size()): + var stat: String = Attributes.IDS[i] + var card: PanelContainer = _ability_cards[i] + var spent := int(draft.spend.get(stat, 0)) + card.get_node("Box/Key").text = stat.to_upper() + card.get_node("Box/Value").text = str(int(final[stat])) + card.get_node("Box/Rolled").text = "rolled %d%s" % [ + int(rolled[stat]), (" +%d" % spent) if spent > 0 else ""] + card.get_node("Box/Buttons/Plus").disabled = not draft.can_increment(stat) + card.get_node("Box/Buttons/Minus").disabled = not draft.can_decrement(stat) + card.get_node("Tag").visible = (stat == primary) + card.theme_type_variation = ThemeKeys.PRIMARY_CARD if stat == primary else ThemeKeys.PARCHMENT_CARD + + +func _bind_bay() -> void: + var shown_name := draft.name.strip_edges() + _plate_name.text = shown_name if shown_name != "" else "Nameless" + _plate_sub.text = "%s · %s" % [ + str(world.race(draft.race_id).get("name", "")), + str(world.calling(draft.calling_id).get("name", "")), + ] + _origin_text.text = "[i]%s[/i]" % _origin_prose() + + +func _origin_prose() -> String: + ## The DM sizes you up — authored, not an AI call. The DM's real entrance is the + ## shell's opening narration, one screen later, at the first moment where speaking + ## means something (§13: authored text is CONTENT, not error handling). + var race: Dictionary = world.race(draft.race_id) + var calling: Dictionary = world.calling(draft.calling_id) + # A missing fragment degrades to the blurb rather than crashing. The content + # parity test stops that from ever shipping; this stops it from ever exploding. + var race_frag := str(race.get("fragment", race.get("blurb", ""))) + var calling_frag := str(calling.get("fragment", calling.get("blurb", ""))) + return "%s Now you carry a %s's work — %s" % [ + race_frag, str(calling.get("name", "")).to_lower(), calling_frag] + + +func _bind_cta() -> void: + var errors := draft.errors(origin, world) + _enter.disabled = not errors.is_empty() + _error.text = str(errors[0]) if not errors.is_empty() else CTA_HINT + + +# ---------------------------------------------------------------- test seam + +func _complete_a_valid_draft_for_test() -> void: + ## Drives the draft to a legal state the way a player would. Test-only, and it + ## touches nothing the player's own clicks do not. + draft.name = "Aldric" + draft.set_race("human") + draft.set_calling("sellsword") + for skill in draft.skill_pool(): + if draft.can_pick(skill): + draft.toggle_skill(skill) + for skill in Skills.IDS: + if draft.can_take_bonus(skill): + draft.set_bonus_skill(skill) + break + _bind() diff --git a/client/scripts/ui/creation/character_creation.gd.uid b/client/scripts/ui/creation/character_creation.gd.uid new file mode 100644 index 0000000..4c2ff05 --- /dev/null +++ b/client/scripts/ui/creation/character_creation.gd.uid @@ -0,0 +1 @@ +uid://3cxsq82gjlfk diff --git a/client/tests/unit/test_character_creation_screen.gd b/client/tests/unit/test_character_creation_screen.gd new file mode 100644 index 0000000..2304b3d --- /dev/null +++ b/client/tests/unit/test_character_creation_screen.gd @@ -0,0 +1,105 @@ +extends "res://addons/gut/test.gd" + +const SCENE := "res://scenes/creation/CharacterCreation.tscn" +const ContentDB = preload("res://scripts/content/content_db.gd") + + +func _world() -> ContentDB: + var w := ContentDB.new() + w.load_from(ContentDB.default_content_root()) + return w + + +func _screen() -> CharacterCreation: + # Inject origin + world BEFORE _ready (add_child) — instantiate() does not run + # _ready(), add_child does. Same seam the shell uses for DmService. + var node = load(SCENE).instantiate() + node.world = _world() + node.origin = ContentDB.load_json(ContentDB.origin_path("deserter")) + add_child_autofree(node) + return node + + +func test_screen_applies_theme_and_fits_viewport(): + var packed = load(SCENE) + assert_true(packed is PackedScene) + var s := _screen() + assert_true(s is Control) + assert_true(s.theme is Theme, "the screen pulls the shared theme") + assert_gt(s.size.x, 0.0, "viewport-fit sized the root (the M3-a collapse guard)") + + +func test_a_draft_exists_with_a_seed_on_load(): + var s := _screen() + assert_not_null(s.draft) + assert_ne(s.draft.character_seed, 0) + + +func test_five_ability_cards_never_six(): + # §7 — LCK has no card, no row, no tooltip. The player must never be able to + # CALCULATE that he is cursed. + var s := _screen() + assert_eq(s._ability_cards.size(), 5) + for card in s._ability_cards: + assert_false(card.name.to_lower().contains("lck")) + + +func test_no_node_on_the_screen_says_luck(): + var s := _screen() + for node in s.find_children("*", "Label", true, false): + assert_false(node.text.to_lower().contains("luck"), "%s leaks Luck (§7)" % node.name) + assert_false(node.text.to_lower().contains("lck"), "%s leaks Luck (§7)" % node.name) + + +func test_only_the_origin_s_allowed_callings_are_shown(): + var s := _screen() + var shown := 0 + for card in s._calling_cards: + if card.visible: + shown += 1 + assert_eq(shown, s.origin["build_constraints"]["allowed_callings"].size()) + + +func test_cta_is_disabled_until_the_draft_is_legal(): + var s := _screen() + assert_true(s._enter.disabled, "an empty draft cannot enter the world") + assert_ne(s._error.text, CharacterCreation.CTA_HINT, "the screen says WHY, dryly") + assert_ne(s._error.text.strip_edges(), "") + + s._complete_a_valid_draft_for_test() + assert_false(s._enter.disabled) + assert_eq(s._error.text, CharacterCreation.CTA_HINT, + "a legal draft gets the mock's line back, not a blank") + + +func test_pressing_enter_emits_exactly_the_draft_s_creation_dict(): + var s := _screen() + s._complete_a_valid_draft_for_test() + watch_signals(s) + s._on_enter_pressed() + assert_signal_emitted_with_parameters(s, "creation_confirmed", [s.draft.to_creation()]) + + +func test_the_screen_never_constructs_the_character(): + # The flow is M4-c's job, and a saga later synthesizes the same Dictionary and + # skips this scene entirely. The screen PRODUCES the dict; it does not spend it. + var s := _screen() + assert_false(s.has_method("construct")) + + +func test_the_displayed_scores_are_the_draft_s_scores(): + var s := _screen() + s._complete_a_valid_draft_for_test() + for i in range(Attributes.IDS.size()): + var stat: String = Attributes.IDS[i] + var value_label: Label = s._ability_cards[i].get_node("Box/Value") + assert_eq(value_label.text, str(int(s.draft.final()[stat])), + "%s shows what the draft holds" % stat) + + +func test_reroll_changes_what_is_on_screen(): + var s := _screen() + var before := s.draft.character_seed + s._on_reroll_pressed() + assert_ne(s.draft.character_seed, before) + assert_eq(s._points.text, "points left: 3", "a re-roll returns the pool") diff --git a/client/tests/unit/test_character_creation_screen.gd.uid b/client/tests/unit/test_character_creation_screen.gd.uid new file mode 100644 index 0000000..f63d415 --- /dev/null +++ b/client/tests/unit/test_character_creation_screen.gd.uid @@ -0,0 +1 @@ +uid://dmliumgn42tv8