Add prebattle camp overview

This commit is contained in:
2026-06-18 23:03:09 +09:00
parent 2183580d81
commit 7e3100c2bd
3 changed files with 172 additions and 2 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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"):