diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index bde90e8..c5e1733 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -58,6 +58,7 @@ const DIALOGUE_PANEL_SIZE := Vector2(1040, 210) const DIALOGUE_PORTRAIT_SIZE := Vector2(164, 186) const DIALOGUE_COLUMN_SIZE := Vector2(820, 186) const DIALOGUE_TEXT_SIZE := Vector2(800, 104) +const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(138, 72) const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58) const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50) const SHOP_ITEM_ICON_SIZE := Vector2(48, 48) @@ -110,6 +111,10 @@ var briefing_panel: PanelContainer var briefing_title_label: Label var briefing_location_label: Label var briefing_objective_label: Label +var briefing_camp_overview_row: HBoxContainer +var briefing_camp_overview_texture: TextureRect +var briefing_camp_overview_fallback_label: Label +var briefing_camp_overview_label: Label var briefing_label: Label var chapter_button: Button var chapter_menu: VBoxContainer @@ -632,8 +637,40 @@ func _create_hud() -> void: briefing_objective_label.custom_minimum_size = Vector2(640, 48) briefing_column.add_child(briefing_objective_label) + briefing_camp_overview_row = HBoxContainer.new() + briefing_camp_overview_row.custom_minimum_size = Vector2(640, 76) + briefing_camp_overview_row.add_theme_constant_override("separation", 8) + briefing_column.add_child(briefing_camp_overview_row) + + var camp_thumbnail_panel := PanelContainer.new() + camp_thumbnail_panel.custom_minimum_size = BRIEFING_CAMP_THUMBNAIL_SIZE + briefing_camp_overview_row.add_child(camp_thumbnail_panel) + + var camp_thumbnail_stack := Control.new() + camp_thumbnail_stack.custom_minimum_size = BRIEFING_CAMP_THUMBNAIL_SIZE + camp_thumbnail_panel.add_child(camp_thumbnail_stack) + + briefing_camp_overview_texture = TextureRect.new() + briefing_camp_overview_texture.set_anchors_preset(Control.PRESET_FULL_RECT) + briefing_camp_overview_texture.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + briefing_camp_overview_texture.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + camp_thumbnail_stack.add_child(briefing_camp_overview_texture) + + briefing_camp_overview_fallback_label = Label.new() + briefing_camp_overview_fallback_label.set_anchors_preset(Control.PRESET_FULL_RECT) + briefing_camp_overview_fallback_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + briefing_camp_overview_fallback_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + briefing_camp_overview_fallback_label.add_theme_font_size_override("font_size", 18) + camp_thumbnail_stack.add_child(briefing_camp_overview_fallback_label) + + briefing_camp_overview_label = Label.new() + briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + briefing_camp_overview_label.custom_minimum_size = Vector2(490, 72) + briefing_camp_overview_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + briefing_camp_overview_row.add_child(briefing_camp_overview_label) + var briefing_scroll := ScrollContainer.new() - briefing_scroll.custom_minimum_size = Vector2(640, 96) + briefing_scroll.custom_minimum_size = Vector2(640, 72) briefing_column.add_child(briefing_scroll) briefing_label = Label.new() @@ -1296,6 +1333,8 @@ func _handle_key(event: InputEventKey) -> void: _hide_chapter_menu() elif shop_menu != null and shop_menu.visible: _hide_shop_menu() + elif talk_menu != null and talk_menu.visible: + _hide_talk_menu() elif armory_menu != null and armory_menu.visible: _hide_armory_menu() elif roster_menu != null and roster_menu.visible: @@ -3992,6 +4031,111 @@ func _format_briefing_rewards() -> String: return _join_strings(parts, ", ") +func _update_briefing_camp_overview(briefing: Dictionary, prep_locked: bool) -> void: + if briefing_camp_overview_row == null: + return + var overview_text := _format_briefing_camp_overview_text(briefing, prep_locked) + briefing_camp_overview_row.visible = not overview_text.is_empty() + if briefing_camp_overview_label != null: + briefing_camp_overview_label.text = overview_text + var texture := _current_battle_background_texture() + var has_texture := texture != null + if briefing_camp_overview_texture != null: + briefing_camp_overview_texture.texture = texture + briefing_camp_overview_texture.visible = has_texture + if briefing_camp_overview_fallback_label != null: + briefing_camp_overview_fallback_label.text = "Camp" + briefing_camp_overview_fallback_label.visible = not has_texture + + +func _format_briefing_camp_overview_text(briefing: Dictionary, prep_locked: bool) -> String: + var parts := [] + var location := str(briefing.get("location", "")).strip_edges() + if not location.is_empty(): + parts.append(location) + parts.append("Gold %dG" % campaign_state.gold) + var talk_text := _format_briefing_talk_overview_text() + if not talk_text.is_empty(): + parts.append(talk_text) + var shop_text := _format_briefing_shop_overview_text() + if not shop_text.is_empty(): + parts.append(shop_text) + var roster_text := _format_briefing_roster_overview_text() + if not roster_text.is_empty(): + parts.append(roster_text) + var formation_text := _format_briefing_formation_overview_text() + if not formation_text.is_empty(): + parts.append(formation_text) + if not state.get_controllable_player_units().is_empty(): + parts.append("Armory ready") + if prep_locked: + parts.append("Replay prep locked") + return _join_strings(parts, " | ") + + +func _format_briefing_talk_overview_text() -> String: + var conversations := _camp_conversation_entries() + if conversations.is_empty(): + return "" + var supply_counts := _camp_conversation_supply_counts(conversations) + var ready := int(supply_counts.get("ready", 0)) + var claimed := int(supply_counts.get("claimed", 0)) + var parts := ["Talk %d" % conversations.size()] + if ready > 0: + parts.append("%d supply" % ready) + if claimed > 0: + parts.append("%d claimed" % claimed) + return _join_strings(parts, ", ") + + +func _format_briefing_shop_overview_text() -> String: + var buy_count := state.get_shop_item_ids().size() + var sell_count := _get_sellable_item_ids().size() + if buy_count <= 0 and sell_count <= 0: + return "" + if sell_count > 0: + return "Shop %d buy, %d sell" % [buy_count, sell_count] + return "Shop %d buy" % buy_count + + +func _format_briefing_roster_overview_text() -> String: + if not state.has_deployment_roster(): + return "" + var deployed_count := state.get_deployed_player_count() + var max_units := state.get_deployment_max_units() + if max_units > 0: + return "Deploy %d/%d" % [deployed_count, max_units] + return "Deploy %d" % deployed_count + + +func _format_briefing_formation_overview_text() -> String: + var formation_count := state.get_formation_cells().size() + if formation_count <= 0: + return "" + return "Formation %d tiles" % formation_count + + +func _camp_conversation_supply_counts(conversations: Array) -> Dictionary: + var counts := {"ready": 0, "claimed": 0} + for conversation in conversations: + if typeof(conversation) != TYPE_DICTIONARY: + continue + var effects: Array = conversation.get("effects", []) + var has_supply := false + for effect in effects: + if typeof(effect) == TYPE_DICTIONARY and str(effect.get("type", "")) == "grant_item": + has_supply = true + break + if not has_supply: + continue + var conversation_id := str(conversation.get("id", "")) + if campaign_state.has_claimed_camp_conversation_effects(active_scenario_id, conversation_id): + counts["claimed"] = int(counts.get("claimed", 0)) + 1 + else: + counts["ready"] = int(counts.get("ready", 0)) + 1 + return counts + + func _show_briefing() -> void: _play_bgm("menu") var briefing := state.get_briefing() @@ -4013,6 +4157,8 @@ func _show_briefing() -> void: briefing_label.text = body briefing_panel.visible = true battle_started = false + var prep_locked := _is_prebattle_prep_locked() + _update_briefing_camp_overview(briefing, prep_locked) _hide_chapter_menu() _hide_shop_menu() _hide_talk_menu() @@ -4021,7 +4167,6 @@ func _show_briefing() -> void: _hide_formation_menu() _hide_save_menu() _clear_pending_move_state() - var prep_locked := _is_prebattle_prep_locked() if chapter_button != null: chapter_button.disabled = campaign_state.get_chapter_progress_entries().is_empty() if shop_button != null: @@ -4718,6 +4863,7 @@ func _reload_from_campaign_state() -> void: _hide_equip_menu() _hide_chapter_menu() _hide_shop_menu() + _hide_talk_menu() _hide_armory_menu() _hide_roster_menu() _hide_formation_menu() diff --git a/tools/smoke_camp_conversation_rewards.gd b/tools/smoke_camp_conversation_rewards.gd index 27f440d..3fa7453 100644 --- a/tools/smoke_camp_conversation_rewards.gd +++ b/tools/smoke_camp_conversation_rewards.gd @@ -25,6 +25,7 @@ func _init() -> void: _check_completed_replay_cannot_claim(failures) _check_conditional_camp_conversations(failures) _check_wuchao_branch_camp_conversations(failures) + _check_talk_menu_closes_on_cancel_and_reload(failures) if not _restore_user_file(SAVE_PATH, save_backup): failures.append("could not restore automatic campaign save") @@ -280,6 +281,25 @@ func _check_wuchao_branch_camp_conversations(failures: Array[String]) -> void: defense_scene.free() +func _check_talk_menu_closes_on_cancel_and_reload(failures: Array[String]) -> void: + var scene = _new_prebattle_scene(failures, "talk menu close guards") + if scene == null: + return + scene._create_hud() + scene.briefing_panel.visible = true + scene.talk_menu.visible = true + var cancel_event := InputEventKey.new() + cancel_event.keycode = KEY_ESCAPE + scene._handle_key(cancel_event) + if scene.talk_menu.visible: + failures.append("Escape should close the pre-battle talk menu") + scene.talk_menu.visible = true + scene._reload_from_campaign_state() + if scene.talk_menu.visible: + failures.append("campaign reload should hide the pre-battle talk menu") + scene.free() + + func _new_prebattle_scene(failures: Array[String], label: String): return _new_prebattle_scene_for(failures, label, SCENARIO_ID, SCENARIO_PATH) diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index e2c3ece..935cc51 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -195,6 +195,10 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: var merchant_notice := scene._format_shop_merchant_notice_text(scene.state.get_shop_merchant()) if not merchant_notice.contains("Camp Merchant") or not merchant_notice.contains("Roadside stores"): failures.append("shop merchant notice should include name and flavor lines") + var camp_overview := scene._format_briefing_camp_overview_text(scene.state.get_briefing(), false) + for expected in ["Yingchuan, 184 CE", "Gold 0G", "Talk 3", "1 supply", "Shop 2 buy", "Deploy 2/2", "Formation 4 tiles", "Armory ready"]: + if not camp_overview.contains(expected): + failures.append("camp overview should include %s: %s" % [expected, camp_overview]) var bean: Dictionary = scene.state.get_item_def("bean") var bean_buy_detail := scene._format_shop_buy_item_detail_text("bean", bean, int(bean.get("price", 0)), 0) if not bean_buy_detail.contains("Heal 20") or not bean_buy_detail.contains("Owned x0"):