diff --git a/.gitignore b/.gitignore index 202b738b6..cf0972478 100644 --- a/.gitignore +++ b/.gitignore @@ -9,10 +9,12 @@ tooling/line-previewer/target/ tooling/test-client/target/ content-ron/ -# Godot client (further ignores managed by client team) +# Godot client client/.godot/ client/export/ client/reports/ +client/**/*.import +client/**/*.uid # Renderer (separate Godot project for sprite pipeline) renderer/.godot/ diff --git a/client/data/ui-strings.yaml b/client/data/ui-strings.yaml index 917f1336b..a4dc980b1 100644 --- a/client/data/ui-strings.yaml +++ b/client/data/ui-strings.yaml @@ -221,7 +221,7 @@ character_select: character_creation: btn_back: "[ESC] Back" - btn_randomise: "[R] Randomise" + btn_randomize: "[R] Randomize" btn_start: "[ENTER] Start" btn_rotate_left: "[Q] ◄" btn_rotate_right: "► [E]" diff --git a/client/scenes/character_creation.tscn b/client/scenes/character_creation.tscn index f81c570ff..e63362a50 100644 --- a/client/scenes/character_creation.tscn +++ b/client/scenes/character_creation.tscn @@ -4,7 +4,7 @@ ; #705: Character creation screen — 3D preview + 5-tab customisation panel. ; SubViewport renders CharacterVisual live. Tab panel: Body/Head/Hair/Clothing/Accessories. -; Keyboard: Q/E rotate, Tab/Shift+Tab cycle tabs, R randomise, Enter start, Esc back. +; Keyboard: Q/E rotate, Tab/Shift+Tab cycle tabs, R randomize, Enter start, Esc back. ; D-146 (tile-scale preview), D-158 (frontal -5° camera default), D-155 (cardinal rotation). [sub_resource type="Environment" id="Environment_1"] @@ -198,10 +198,10 @@ theme_override_colors/font_color = Color(0.53, 0.56, 0.63, 1.0) layout_mode = 2 size_flags_horizontal = 3 -[node name="RandomiseBtn" type="Button" parent="Footer"] +[node name="RandomizeBtn" type="Button" parent="Footer"] layout_mode = 2 custom_minimum_size = Vector2(140, 36) -text = "[R] Randomise" +text = "[R] Randomize" theme_override_font_sizes/font_size = 13 theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 1.0) diff --git a/client/scenes/character_select.tscn b/client/scenes/character_select.tscn deleted file mode 100644 index 23360aab7..000000000 --- a/client/scenes/character_select.tscn +++ /dev/null @@ -1,189 +0,0 @@ -[gd_scene load_steps=2 format=3 uid="uid://char_select_scene_sr"] - -[ext_resource type="Script" path="res://ui/character_select.gd" id="1_charselect"] - -; #588: Character archetype select — two-card overlay between New Game and main.tscn. -; Keyboard: left/right to pick, Enter to confirm, ESC to cancel (no save dir created). - -[node name="CharacterSelect" type="Control"] -layout_mode = 3 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -script = ExtResource("1_charselect") - -[node name="Background" type="ColorRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.04, 0.04, 0.07, 0.97) -mouse_filter = 2 - -[node name="TitleLabel" type="Label" parent="."] -layout_mode = 1 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -200.0 -offset_top = 100.0 -offset_right = 200.0 -offset_bottom = 126.0 -grow_horizontal = 2 -text = "Choose your perspective." -horizontal_alignment = 1 -theme_override_font_sizes/font_size = 16 -theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 1.0) - -[node name="Cards" type="HBoxContainer" parent="."] -layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -316.0 -offset_top = -110.0 -offset_right = 316.0 -offset_bottom = 140.0 -grow_horizontal = 2 -grow_vertical = 2 -theme_override_constants/separation = 24 -alignment = 1 - -; --- Smuggler card --- - -[node name="CardSmugglerWrapper" type="Control" parent="Cards"] -layout_mode = 2 -custom_minimum_size = Vector2(280, 240) -mouse_filter = 0 - -[node name="CardBorder" type="ColorRect" parent="Cards/CardSmugglerWrapper"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.18, 0.22, 0.28, 1.0) -mouse_filter = 2 - -[node name="CardInner" type="ColorRect" parent="Cards/CardSmugglerWrapper"] -layout_mode = 1 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 2.0 -offset_top = 2.0 -offset_right = -2.0 -offset_bottom = -2.0 -color = Color(0.07, 0.07, 0.10, 1.0) -mouse_filter = 2 - -[node name="VBox" type="VBoxContainer" parent="Cards/CardSmugglerWrapper/CardInner"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 20.0 -offset_top = 20.0 -offset_right = -20.0 -offset_bottom = -20.0 -theme_override_constants/separation = 10 - -[node name="NameLabel" type="Label" parent="Cards/CardSmugglerWrapper/CardInner/VBox"] -layout_mode = 2 -text = "Smuggler" -theme_override_font_sizes/font_size = 26 -theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0) - -[node name="RoleLabel" type="Label" parent="Cards/CardSmugglerWrapper/CardInner/VBox"] -layout_mode = 2 -text = "Freight logistics worker — Sova Transit" -autowrap_mode = 2 -theme_override_font_sizes/font_size = 13 -theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0) - -[node name="Divider" type="Control" parent="Cards/CardSmugglerWrapper/CardInner/VBox"] -layout_mode = 2 -custom_minimum_size = Vector2(0, 12) - -[node name="ToneLabel" type="Label" parent="Cards/CardSmugglerWrapper/CardInner/VBox"] -layout_mode = 2 -text = "Insider access. Social camouflage. The ring is your daily life." -autowrap_mode = 2 -theme_override_font_sizes/font_size = 12 -theme_override_colors/font_color = Color(0.416, 0.447, 0.510, 1.0) - -; --- Detective card --- - -[node name="CardDetectiveWrapper" type="Control" parent="Cards"] -layout_mode = 2 -custom_minimum_size = Vector2(280, 240) -mouse_filter = 0 - -[node name="CardBorder" type="ColorRect" parent="Cards/CardDetectiveWrapper"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -color = Color(0.18, 0.22, 0.28, 1.0) -mouse_filter = 2 - -[node name="CardInner" type="ColorRect" parent="Cards/CardDetectiveWrapper"] -layout_mode = 1 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 2.0 -offset_top = 2.0 -offset_right = -2.0 -offset_bottom = -2.0 -color = Color(0.07, 0.07, 0.10, 1.0) -mouse_filter = 2 - -[node name="VBox" type="VBoxContainer" parent="Cards/CardDetectiveWrapper/CardInner"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -offset_left = 20.0 -offset_top = 20.0 -offset_right = -20.0 -offset_bottom = -20.0 -theme_override_constants/separation = 10 - -[node name="NameLabel" type="Label" parent="Cards/CardDetectiveWrapper/CardInner/VBox"] -layout_mode = 2 -text = "Detective" -theme_override_font_sizes/font_size = 26 -theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0) - -[node name="RoleLabel" type="Label" parent="Cards/CardDetectiveWrapper/CardInner/VBox"] -layout_mode = 2 -text = "Commission investigator — External assignment" -autowrap_mode = 2 -theme_override_font_sizes/font_size = 13 -theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0) - -[node name="Divider" type="Control" parent="Cards/CardDetectiveWrapper/CardInner/VBox"] -layout_mode = 2 -custom_minimum_size = Vector2(0, 12) - -[node name="ToneLabel" type="Label" parent="Cards/CardDetectiveWrapper/CardInner/VBox"] -layout_mode = 2 -text = "Institutional authority. Analytical lattice. You were sent here." -autowrap_mode = 2 -theme_override_font_sizes/font_size = 12 -theme_override_colors/font_color = Color(0.416, 0.447, 0.510, 1.0) - -[node name="ConfirmBtn" type="Button" parent="."] -layout_mode = 1 -anchor_left = 0.5 -anchor_top = 1.0 -anchor_right = 0.5 -anchor_bottom = 1.0 -offset_left = -60.0 -offset_top = -80.0 -offset_right = 60.0 -offset_bottom = -50.0 -grow_horizontal = 2 -grow_vertical = 0 -text = "Begin" -theme_override_font_sizes/font_size = 15 -theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0) diff --git a/client/tests/test_character_creation_sprint28.gd b/client/tests/test_character_creation_sprint28.gd index 9cbf9bb61..acf1af0ba 100644 --- a/client/tests/test_character_creation_sprint28.gd +++ b/client/tests/test_character_creation_sprint28.gd @@ -2,7 +2,7 @@ ## ## Validates the CharacterCreation UI: scene instantiation, tab structure, ## signal emission (creation_confirmed / creation_cancelled), keyboard nav -## callbacks, randomise, color derivation helpers, and game flow wiring. +## callbacks, randomize, color derivation helpers, and game flow wiring. ## ## These are UI-only tests (no compositor/server required). ## CharacterVisual asset paths fall back gracefully when GLBs are absent. @@ -115,10 +115,10 @@ func test_footer_buttons_exist() -> void: if _scene == null: return var back: Button = _scene.get_node_or_null("Footer/BackBtn") - var rand_btn: Button = _scene.get_node_or_null("Footer/RandomiseBtn") + var rand_btn: Button = _scene.get_node_or_null("Footer/RandomizeBtn") var start: Button = _scene.get_node_or_null("Footer/StartBtn") assert_bool(back != null).override_failure_message("BackBtn must exist in Footer").is_true() - assert_bool(rand_btn != null).override_failure_message("RandomiseBtn must exist in Footer").is_true() + assert_bool(rand_btn != null).override_failure_message("RandomizeBtn must exist in Footer").is_true() assert_bool(start != null).override_failure_message("StartBtn must exist in Footer").is_true() @@ -367,33 +367,33 @@ func test_derive_accent_shifts_hue() -> void: # ============================================================================= -# Randomise (Task #8) +# Randomize (Task #8) # ============================================================================= -func test_randomise_changes_body_type() -> void: +func test_randomize_changes_body_type() -> void: if _scene == null: return - # Run randomise multiple times to ensure it doesn't always pick the default + # Run randomize multiple times to ensure it doesn't always pick the default var changed := false var original := _scene._descriptor.body_type for _i in 20: - _scene._on_randomise() + _scene._on_randomize() if _scene._descriptor.body_type != original: changed = true break assert_bool(changed).override_failure_message( - "Randomise must be able to change body type from default" + "Randomize must be able to change body type from default" ).is_true() -func test_randomise_produces_valid_skin_tone() -> void: +func test_randomize_produces_valid_skin_tone() -> void: if _scene == null: return for _i in 10: - _scene._on_randomise() + _scene._on_randomize() var tone := _scene._descriptor.skin_tone assert_bool(tone >= 0 and tone <= 8).override_failure_message( - "Randomise must produce skin_tone in range 0–8" + "Randomize must produce skin_tone in range 0–8" ).is_true() @@ -471,7 +471,7 @@ func test_q_key_rotates_left() -> void: ).is_not_equal(initial) -func test_r_key_triggers_randomise() -> void: +func test_r_key_triggers_randomize() -> void: if _scene == null: return # Seed to a known state so change is detectable @@ -490,7 +490,7 @@ func test_r_key_triggers_randomise() -> void: changed = true break assert_bool(changed).override_failure_message( - "R key must trigger randomise and change body_type or skin_tone" + "R key must trigger randomize and change body_type or skin_tone" ).is_true() diff --git a/client/ui/character_creation.gd b/client/ui/character_creation.gd index 2252f5ecc..cb3655522 100644 --- a/client/ui/character_creation.gd +++ b/client/ui/character_creation.gd @@ -114,7 +114,7 @@ const RECENT_SLOTS := 9 @onready var _cam_angle_btn: Button = $Layout/PreviewPanel/PreviewOverlay/CamAngleBtn @onready var _tab_container: TabContainer = $Layout/TabPanel/TabContainer @onready var _footer_back: Button = $Footer/BackBtn -@onready var _footer_randomise: Button = $Footer/RandomiseBtn +@onready var _footer_randomize: Button = $Footer/RandomizeBtn @onready var _footer_start: Button = $Footer/StartBtn @onready var _modal_root: Control = $ColorPickerModal @@ -179,7 +179,7 @@ var _modal_recent_btns: Array[Button] = [] var _clothing_dock_container: Control = null var _accessory_dock_container: Control = null -# --- Cached asset scan results (populated once during tab build, reused by randomise) --- +# --- Cached asset scan results (populated once during tab build, reused by randomize) --- var _cached_hair_ids: Array = [] var _cached_head_ids: Array = [] @@ -212,11 +212,11 @@ func _ready() -> void: _rotate_right_btn.pressed.connect(_on_rotate_right) _cam_angle_btn.pressed.connect(_on_cam_angle_toggle) _footer_back.pressed.connect(_on_back) - _footer_randomise.pressed.connect(_on_randomise) + _footer_randomize.pressed.connect(_on_randomize) _footer_start.pressed.connect(_on_start) _footer_back.text = UIStrings.get_text("character_creation.btn_back") - _footer_randomise.text = UIStrings.get_text("character_creation.btn_randomise") + _footer_randomize.text = UIStrings.get_text("character_creation.btn_randomize") _footer_start.text = UIStrings.get_text("character_creation.btn_start") _rotate_left_btn.text = UIStrings.get_text("character_creation.btn_rotate_left") _rotate_right_btn.text = UIStrings.get_text("character_creation.btn_rotate_right") @@ -1117,7 +1117,7 @@ func _input(event: InputEvent) -> void: _on_rotate_right() get_viewport().set_input_as_handled() KEY_R: - _on_randomise() + _on_randomize() get_viewport().set_input_as_handled() KEY_ENTER, KEY_KP_ENTER: _on_start() @@ -1147,10 +1147,10 @@ func _on_start() -> void: # ============================================================================= -# Randomise +# Randomize # ============================================================================= -func _on_randomise() -> void: +func _on_randomize() -> void: var all_types := CharacterVisualDescriptor.BodyType.values() _descriptor.body_type = all_types[randi() % all_types.size()] _descriptor.skin_tone = randi() % CharacterVisual.SKIN_TONES.size() diff --git a/client/ui/character_select.gd b/client/ui/character_select.gd deleted file mode 100644 index f3130df55..000000000 --- a/client/ui/character_select.gd +++ /dev/null @@ -1,89 +0,0 @@ -extends Control -## #588: Character archetype select panel — shown after "New Game", before loading main.tscn. -## Two cards (Smuggler / Detective). Keyboard (left/right/enter/esc) and mouse. -## Emits archetype_confirmed(archetype: String) or archetype_cancelled on ESC. -## -## ESC cancels without creating a save directory — new_game() fires AFTER confirmation. - -signal archetype_confirmed(archetype: String) -signal archetype_cancelled - -const CARD_BG_NORMAL := Color(0.07, 0.07, 0.10, 1.0) -const CARD_BG_SELECTED := Color(0.10, 0.12, 0.18, 1.0) -const CARD_BORDER_NORMAL := Color(0.18, 0.22, 0.28, 1.0) -const CARD_BORDER_SELECTED := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HOVER - -# Archetypes in display order — index 0=smuggler (left card), 1=detective (right card) -const ARCHETYPES := ["smuggler", "detective"] - -@onready var _smuggler_wrapper: Control = $Cards/CardSmugglerWrapper -@onready var _detective_wrapper: Control = $Cards/CardDetectiveWrapper -@onready var _confirm_btn: Button = $ConfirmBtn -@onready var _title_label: Label = $TitleLabel - -var _selected_index: int = 0 # 0=smuggler, 1=detective - - -func _ready() -> void: - _title_label.text = UIStrings.get_text("character_select.title") - _confirm_btn.text = UIStrings.get_text("character_select.confirm") - - # Smuggler card labels - $Cards/CardSmugglerWrapper/CardInner/VBox/NameLabel.text = UIStrings.get_text("character_select.smuggler_card_name") - $Cards/CardSmugglerWrapper/CardInner/VBox/RoleLabel.text = UIStrings.get_text("character_select.smuggler_card_role") - $Cards/CardSmugglerWrapper/CardInner/VBox/ToneLabel.text = UIStrings.get_text("character_select.smuggler_card_tone") - - # Detective card labels - $Cards/CardDetectiveWrapper/CardInner/VBox/NameLabel.text = UIStrings.get_text("character_select.detective_card_name") - $Cards/CardDetectiveWrapper/CardInner/VBox/RoleLabel.text = UIStrings.get_text("character_select.detective_card_role") - $Cards/CardDetectiveWrapper/CardInner/VBox/ToneLabel.text = UIStrings.get_text("character_select.detective_card_tone") - - _confirm_btn.pressed.connect(_on_confirm) - _smuggler_wrapper.gui_input.connect(_on_card_input.bind(0)) - _detective_wrapper.gui_input.connect(_on_card_input.bind(1)) - - _update_card_visuals() - - -func _input(event: InputEvent) -> void: - if not visible: - return - if event is InputEventKey and event.pressed and not event.is_echo(): - match event.keycode: - KEY_LEFT: - _selected_index = 0 - _update_card_visuals() - get_viewport().set_input_as_handled() - KEY_RIGHT: - _selected_index = 1 - _update_card_visuals() - get_viewport().set_input_as_handled() - KEY_ENTER, KEY_KP_ENTER: - _on_confirm() - get_viewport().set_input_as_handled() - KEY_ESCAPE: - archetype_cancelled.emit() - get_viewport().set_input_as_handled() - - -func _on_card_input(event: InputEvent, card_index: int) -> void: - if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: - _selected_index = card_index - _update_card_visuals() - - -func _on_confirm() -> void: - archetype_confirmed.emit(ARCHETYPES[_selected_index]) - - -func _update_card_visuals() -> void: - _set_card_selected(_smuggler_wrapper, _selected_index == 0) - _set_card_selected(_detective_wrapper, _selected_index == 1) - _confirm_btn.grab_focus() - - -func _set_card_selected(wrapper: Control, selected: bool) -> void: - var border: ColorRect = wrapper.get_node("CardBorder") - var inner: ColorRect = wrapper.get_node("CardInner") - border.color = CARD_BORDER_SELECTED if selected else CARD_BORDER_NORMAL - inner.color = CARD_BG_SELECTED if selected else CARD_BG_NORMAL diff --git a/client/ui/main_menu.gd b/client/ui/main_menu.gd index 7646c7616..992aea439 100644 --- a/client/ui/main_menu.gd +++ b/client/ui/main_menu.gd @@ -1,12 +1,10 @@ extends Control ## #258: Main menu — New Game / Continue / Load Game / Quit. -## New Game: shows character select panel (D-085 save dir created after archetype chosen). +## New Game: opens character creation screen, then starts game. ## Continue: loads most recent save directory. ## Load Game: shows sorted save list for manual selection (#257). -## #588: Character archetype selection — panel shown between New Game click and game load. const GAME_SCENE := "res://scenes/main.tscn" -const CHARACTER_SELECT_SCENE := "res://scenes/character_select.tscn" const CHARACTER_CREATION_SCENE := "res://scenes/character_creation.tscn" const BG_COLOR := Color(0.05, 0.05, 0.08, 1.0) @@ -26,8 +24,7 @@ const FONT_SIZE_BTN := 15 @onready var _saves_list: VBoxContainer = $LoadGamePanel/VBox/SavesScroll/SavesList @onready var _load_back_btn: Button = $LoadGamePanel/VBox/BackBtn -var _char_select: Control = null # Instantiated on demand -var _char_creation: Control = null # Instantiated after archetype confirmed +var _char_creation: Control = null func _ready() -> void: @@ -47,40 +44,10 @@ func _refresh_continue_state() -> void: func _on_new_game() -> void: - # #588: Show character select before creating the save directory. - # ESC on character select cancels with no directory created. GameState.pending_load_path = "" - _show_character_select() - - -func _show_character_select() -> void: - if _char_select != null and is_instance_valid(_char_select): - _char_select.queue_free() - var scene := load(CHARACTER_SELECT_SCENE) as PackedScene - if scene == null: - push_error("MainMenu: failed to load character_select.tscn") - return - _char_select = scene.instantiate() - add_child(_char_select) - _char_select.archetype_confirmed.connect(_on_archetype_confirmed) - _char_select.archetype_cancelled.connect(_on_archetype_cancelled) - - -func _on_archetype_confirmed(archetype: String) -> void: - if _char_select != null and is_instance_valid(_char_select): - _char_select.queue_free() - _char_select = null - # Set archetype first; save dir created after character creation confirms. - GameState.character_archetype = archetype _show_character_creation() -func _on_archetype_cancelled() -> void: - if _char_select != null and is_instance_valid(_char_select): - _char_select.queue_free() - _char_select = null - - func _show_character_creation() -> void: if _char_creation != null and is_instance_valid(_char_creation): _char_creation.queue_free() @@ -107,8 +74,6 @@ func _on_creation_cancelled() -> void: if _char_creation != null and is_instance_valid(_char_creation): _char_creation.queue_free() _char_creation = null - # Return to archetype select - _show_character_select() func _start_game_with_defaults() -> void: @@ -116,12 +81,11 @@ func _start_game_with_defaults() -> void: if game_id.is_empty(): push_error("MainMenu: new_game() failed to create save directory — cannot start") return - SessionManager.save_character_archetype(game_id, GameState.character_archetype) get_tree().change_scene_to_file(GAME_SCENE) func _on_continue() -> void: - GameState.pending_load_path = "" # clear stale load path from previous Load selection + GameState.pending_load_path = "" var saves := SessionManager.list_game_dirs() if saves.is_empty(): return @@ -129,7 +93,7 @@ func _on_continue() -> void: get_tree().change_scene_to_file(GAME_SCENE) -var _list_built: bool = false # guard against queue_free() race on rapid reopen +var _list_built: bool = false func _on_load_game_browse() -> void: @@ -141,7 +105,7 @@ func _on_load_game_browse() -> void: func _on_load_back() -> void: _load_panel.visible = false - _list_built = false # allow rebuild on next open + _list_built = false func _on_quit() -> void: