From 6ba977cc0b7bfc74e35c42ef89ed9c907622e63d Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 01:21:42 +0900 Subject: [PATCH] Add opening camp route report --- README.md | 1 + data/scenarios/001_yellow_turbans.json | 26 +++++++++++++++++ tools/smoke_chapter_one_polish.gd | 39 ++++++++++++++++++++++++++ tools/smoke_visual_assets.gd | 17 +++++++---- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7f12854..88d0e0e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Scenario-specific pre-battle shop purchases, optional finite shop stock, and 50% sell-back save immediately and sync into the upcoming battle inventory. - Pre-battle shop rows emphasize item artwork, compact price/status badges, and hover details instead of dense inline text. - Pre-battle Talk can list scenario camp conversations by officer or topic with portrait rows, compact topic/supply badges, hover dialogue previews, saved-branch filtering, one-time pre-battle supplies, and merchant flavor lines before a battle. +- The opening battle's camp now includes a villager route report that frames the castle, village recovery point, and enemy approach before combat starts. - Pre-battle Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock, with portrait and item-icon rows plus hover change details. - Pre-battle Roster can move optional officers between sortie and reserve within scenario limits, using portrait rows, compact status badges, and hover details. - Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, coordinate badges, and hover placement instructions. diff --git a/data/scenarios/001_yellow_turbans.json b/data/scenarios/001_yellow_turbans.json index 2ba6fc6..acbb583 100644 --- a/data/scenarios/001_yellow_turbans.json +++ b/data/scenarios/001_yellow_turbans.json @@ -147,6 +147,32 @@ } ] }, + { + "id": "yingchuan_villager_report", + "group": "topic", + "label": "영천 백성 - 길목", + "summary": "피난민에게 성채와 마을 길목의 형세를 묻는다.", + "lines": [ + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "마을을 버리고 나온 백성이 있다면 불러오라. 적의 수보다 백성이 어디로 밀려나는지가 먼저다." + }, + { + "speaker": "Yingchuan Villager", + "display_speaker": "영천 백성", + "side": "right", + "text": "장군, 동쪽 성채에는 황건 수괴가 머물고 북쪽 언덕에는 궁수가 숨어 있습니다. 마을 우물가에서 숨을 고르면 다친 병사도 조금 버틸 수 있습니다." + }, + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "그렇다면 길을 곧게 밀기보다 마을을 거점 삼아 끊어 가겠습니다. 선봉이 먼저 적의 눈을 붙들겠습니다." + } + ] + }, { "id": "northern_woods_cache", "group": "topic", diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 052598e..b0ecceb 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -26,6 +26,7 @@ func _init() -> void: _check_readability_contract(failures) _check_dialogue_localization_and_side(failures) _check_event_dialogue_side_passthrough(failures) + _check_opening_battle_camp_context(failures) _check_opening_battle_event_dialogue_structure(failures) _check_sishui_gate_event_dialogue_structure(failures) _check_xingyang_ambush_event_dialogue_structure(failures) @@ -640,6 +641,26 @@ func _check_event_dialogue_side_passthrough(failures: Array[String]) -> void: scene.free() +func _check_opening_battle_camp_context(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): + failures.append("could not load opening battle for camp context") + return + var conversations: Array = state.get_briefing().get("camp_conversations", []) + if conversations.size() < 4: + failures.append("opening battle should offer enough camp conversations before the first fight") + var villager_report := _camp_conversation_by_id(conversations, "yingchuan_villager_report") + if villager_report.is_empty(): + failures.append("opening battle should include a villager route report before combat") + return + if str(villager_report.get("group", "")) != "topic": + failures.append("villager route report should be a topic conversation") + var report_text := _camp_conversation_text(villager_report) + for expected in ["영천 백성", "성채", "마을", "다친 병사"]: + if not report_text.contains(expected): + failures.append("villager route report should mention %s: %s" % [expected, report_text]) + + func _check_opening_battle_event_dialogue_structure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): @@ -1567,6 +1588,24 @@ func _event_dialogue_text(event: Dictionary) -> String: return "\n".join(parts) +func _camp_conversation_by_id(conversations: Array, conversation_id: String) -> Dictionary: + for conversation in conversations: + if typeof(conversation) == TYPE_DICTIONARY and str(conversation.get("id", "")) == conversation_id: + return conversation + return {} + + +func _camp_conversation_text(conversation: Dictionary) -> String: + var parts: Array[String] = [] + var lines: Array = conversation.get("lines", []) + for line in lines: + if typeof(line) != TYPE_DICTIONARY: + continue + parts.append(str((line as Dictionary).get("display_speaker", (line as Dictionary).get("speaker", "")))) + parts.append(str((line as Dictionary).get("text", ""))) + return "\n".join(parts) + + func _event_has_set_objective_action(event: Dictionary) -> bool: var actions: Array = event.get("actions", []) for action in actions: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index cad7c39..a7b1259 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -124,13 +124,18 @@ func _check_battle_visual_data(failures: Array[String]) -> void: if (briefing.get("camp_dialogue", []) as Array).is_empty(): failures.append("001 briefing should expose camp dialogue") var conversations: Array = briefing.get("camp_conversations", []) - if conversations.size() < 3: - failures.append("001 briefing should expose at least three camp conversations") + if conversations.size() < 4: + failures.append("001 briefing should expose at least four camp conversations") else: _check_camp_conversation(failures, conversations[0], "cao_cao_strategy", "officer", "cao_cao") _check_camp_conversation(failures, conversations[1], "xiahou_dun_vanguard", "officer", "xiahou_dun") - _check_camp_conversation(failures, conversations[2], "northern_woods_cache", "topic", "") - _check_camp_conversation_effect(failures, conversations[2], "bean", 1) + var villager_report := _find_camp_conversation(conversations, "yingchuan_villager_report") + _check_camp_conversation(failures, villager_report, "yingchuan_villager_report", "topic", "") + if not str(villager_report).contains("영천 백성") or not str(villager_report).contains("마을 우물가"): + failures.append("001 villager report should explain the route and recovery point") + var cache := _find_camp_conversation(conversations, "northern_woods_cache") + _check_camp_conversation(failures, cache, "northern_woods_cache", "topic", "") + _check_camp_conversation_effect(failures, cache, "bean", 1) var merchant := state.get_shop_merchant() if merchant.is_empty() or (merchant.get("lines", []) as Array).is_empty(): failures.append("001 shop should expose merchant lines") @@ -2968,7 +2973,7 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: if not merchant_notice.contains("군막 상인") or not merchant_notice.contains("길가의 물자"): 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 ["위치: 영천, 중평 원년", "군막:", "준비:", "군자금 0냥", "군막 회의 · 3건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 6칸", "비축 물자"]: + for expected in ["위치: 영천, 중평 원년", "군막:", "준비:", "군자금 0냥", "군막 회의 · 4건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 6칸", "비축 물자"]: 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") @@ -3015,7 +3020,7 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: var talk_tooltips := _collect_child_tooltips(scene.talk_list) if not talk_visible_text.contains("조조 - 군략") or not talk_visible_text.contains("장수") or not talk_visible_text.contains("보급 대기"): failures.append("talk rows should show compact labels and status badges: %s" % talk_visible_text) - if talk_visible_text.contains("영천") or talk_visible_text.contains("첫 행군") or talk_visible_text.contains("보급 콩") or talk_visible_text.contains("수령 가능"): + if talk_visible_text.contains("첫 행군") or talk_visible_text.contains("보급 콩") or talk_visible_text.contains("수령 가능"): failures.append("talk rows should keep preview and supply details in hover text: %s" % talk_visible_text) if not talk_tooltips.contains("영천") or not talk_tooltips.contains("보급 콩") or not talk_tooltips.contains("수령 가능"): failures.append("talk row tooltip should retain dialogue preview and supply detail: %s" % talk_tooltips)