diff --git a/README.md b/README.md index 5ec3b67..a2dd0b9 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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. -- The opening battle now uses a dedicated high-resolution Yingchuan battlefield backdrop, lighter terrain overlays, no text class boxes on units, quieter objective progress text, and a pulled-back enemy line so the player can lure and form up before the first clash. +- The opening battle now uses a dedicated high-resolution Yingchuan battlefield backdrop, lighter terrain overlays, no text class boxes on units, quieter objective progress text, a tactical lure-line marker, and a pulled-back enemy line so the player can lure and form up before the first clash. - The opening victory dialogue now bridges Xiahou Yuan and Cao Ren's join into the Sishui Gate march against Hua Xiong. - Sishui Gate now opens with camp dialogue that turns the coalition briefing into a spoken march order. - 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. @@ -124,11 +124,11 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Campaign completion screen. - New campaign save reset. - Manual campaign checkpoint save/load. -- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop and Cao Cao portrait art, offers start/load/settings, leads new campaigns through a four-beat Cao Cao decision and Xiahou Dun opening prologue, exposes the same audio/edge-scroll settings from the top in-game toolbar, and persists BGM/SFX volume, sound toggles, and edge-scroll preference in `user://heros_settings.json`. +- Cinematic title screen uses a dedicated ancient-war-camp dawn backdrop and Cao Cao portrait art, offers start/load/settings, leads new campaigns through a generated four-panel Cao Cao decision and Xiahou Dun opening prologue, exposes the same audio/edge-scroll settings from the top in-game toolbar, and persists BGM/SFX volume, sound toggles, and edge-scroll preference in `user://heros_settings.json`. - Victory and defeat result overlay. - Dialogue portrait slot stays fixed across speaker and narration lines, with officer default image paths, optional per-line overrides, and speaker-initial or record fallback. - Initial AI-generated photorealistic officer portraits for Cao Cao, Xiahou Dun, Xiahou Yuan, Cao Ren, Dian Wei, Guo Jia, and Zhang He. -- AI-generated scenario battlefield backgrounds, transparent class units, transparent generic-enemy units, and transparent item icon assets live under `art/backgrounds`, `art/units`, and `art/items`. +- AI-generated scenario battlefield backgrounds, opening story panels, transparent class units, transparent generic-enemy units, and transparent item icon assets live under `art/backgrounds`, `art/story`, `art/units`, and `art/items`. - Battle HUD portrait thumbnails show the selected unit, or a hovered unit when nothing is selected. - Selected and hovered unit focus text shows combat role, movement type, current terrain move cost, terrain bonuses, core stats, and status effects. - Dialogue lines can mark speaking side for text alignment inside a fixed visual-novel-style dialogue panel with stable portrait, progress, and previous-line controls. diff --git a/art/story/opening/cao_cao_resolves.png b/art/story/opening/cao_cao_resolves.png new file mode 100644 index 0000000..4e5aec1 Binary files /dev/null and b/art/story/opening/cao_cao_resolves.png differ diff --git a/art/story/opening/xiahou_dun_joins.png b/art/story/opening/xiahou_dun_joins.png new file mode 100644 index 0000000..2aed59e Binary files /dev/null and b/art/story/opening/xiahou_dun_joins.png differ diff --git a/art/story/opening/yellow_turban_rebellion.png b/art/story/opening/yellow_turban_rebellion.png new file mode 100644 index 0000000..aa2c59c Binary files /dev/null and b/art/story/opening/yellow_turban_rebellion.png differ diff --git a/art/story/opening/yingchuan_first_contact.png b/art/story/opening/yingchuan_first_contact.png new file mode 100644 index 0000000..dfc8cda Binary files /dev/null and b/art/story/opening/yingchuan_first_contact.png differ diff --git a/data/scenarios/001_yellow_turbans.json b/data/scenarios/001_yellow_turbans.json index 55c9b62..516dc67 100644 --- a/data/scenarios/001_yellow_turbans.json +++ b/data/scenarios/001_yellow_turbans.json @@ -652,6 +652,62 @@ } ] }, + { + "id": "first_lure_line", + "once": true, + "when": { + "type": "unit_reaches_tile", + "team": "player", + "label": "유인선", + "cells": [ + [ + 4, + 5 + ], + [ + 4, + 6 + ], + [ + 4, + 7 + ], + [ + 5, + 6 + ], + [ + 5, + 7 + ] + ], + "show_marker": true, + "marker_kind": "tactic" + }, + "actions": [ + { + "type": "log", + "text": "조조군이 마을 앞 유인선에 닿았다." + }, + { + "type": "dialogue", + "lines": [ + { + "speaker": "Xiahou Dun", + "display_speaker": "하후돈", + "side": "right", + "text": "이쯤이면 적 앞줄만 끌어낼 수 있겠습니다. 더 깊이 들어가면 궁수와 마을 수비가 함께 움직입니다." + }, + { + "speaker": "Cao Cao", + "display_speaker": "조조", + "side": "left", + "text": "좋다. 하후돈이 앞을 붙들고 나는 측면을 본다. 마을 회복을 등지고 한 부대씩 끊어라." + } + ] + } + ] + }, { "id": "northern_woods_cache", "once": true, diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index e85ae03..bf00af2 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -621,7 +621,7 @@ Movement-triggered pickups can use `grant_item` for one item or `grant_items` fo } ``` -Rewarding `unit_reaches_tile` events are also exposed as optional side-event markers on the battlefield when they are not already objective cells. Use `when.label` to keep the map marker and tile-info text readable in Korean; otherwise the marker falls back to a short supply or gold label derived from its reward action. +Rewarding `unit_reaches_tile` events are also exposed as optional side-event markers on the battlefield when they are not already objective cells. Use `when.label` to keep the map marker and tile-info text readable in Korean; otherwise the marker falls back to a short supply or gold label derived from its reward action. Non-reward events can opt into the same battlefield marker system with `show_marker: true` and a `marker_kind`, such as `tactic`, so scenarios can show lure lines, trap warnings, or rally points without attaching an item reward. Movement-triggered gold caches can use `grant_gold` with a positive `amount`. The gold is shown immediately through log and floating feedback, but it stays battle-only until victory commits the battle result. diff --git a/scripts/core/battle_state.gd b/scripts/core/battle_state.gd index 8ae7ef6..4d29b0e 100644 --- a/scripts/core/battle_state.gd +++ b/scripts/core/battle_state.gd @@ -1761,7 +1761,7 @@ func get_event_markers() -> Array[Dictionary]: continue if not _event_gate_open(when) or not _campaign_flags_match(when.get("campaign_flags", {})): continue - if not _event_has_reward_action(event): + if not _event_should_show_marker(event): continue var cells: Array[Vector2i] = [] for cell in _condition_cells(when): @@ -4331,12 +4331,31 @@ func _event_has_reward_action(event: Dictionary) -> bool: return false +func _event_should_show_marker(event: Dictionary) -> bool: + if _event_declares_marker(event): + return true + return _event_has_reward_action(event) + + +func _event_declares_marker(event: Dictionary) -> bool: + if bool(event.get("show_marker", false)): + return true + if not str(event.get("marker_kind", "")).strip_edges().is_empty(): + return true + var when: Dictionary = event.get("when", {}) + if bool(when.get("show_marker", false)): + return true + return not str(when.get("marker_kind", "")).strip_edges().is_empty() + + func _event_marker_label(event: Dictionary) -> String: var when: Dictionary = event.get("when", {}) var label := _condition_destination_label(when) if label != "전장 표식": return label var kind := _event_marker_kind(event) + if kind == "tactic": + return "전술선" if kind == "gold": return "군자금" if kind == "supply": @@ -4345,6 +4364,12 @@ func _event_marker_label(event: Dictionary) -> String: func _event_marker_kind(event: Dictionary) -> String: + var explicit_kind := str(event.get("marker_kind", "")).strip_edges().to_lower() + if explicit_kind.is_empty(): + var when: Dictionary = event.get("when", {}) + explicit_kind = str(when.get("marker_kind", "")).strip_edges().to_lower() + if not explicit_kind.is_empty(): + return explicit_kind var has_item := false var has_gold := false for action in event.get("actions", []): diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index af73971..98ca60e 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -28,24 +28,32 @@ const BGM_MENU_PATH := "res://audio/bgm/menu_theme_placeholder.wav" const BGM_BATTLE_PATH := "res://audio/bgm/battle_loop_placeholder.wav" const TITLE_BACKGROUND_PATH := "res://art/backgrounds/title_war_camp_dawn.png" const TITLE_PORTRAIT_PATH := "res://art/portraits/cao_cao.png" +const OPENING_PROLOGUE_REBELLION_PATH := "res://art/story/opening/yellow_turban_rebellion.png" +const OPENING_PROLOGUE_RESOLVE_PATH := "res://art/story/opening/cao_cao_resolves.png" +const OPENING_PROLOGUE_XIAHOU_DUN_PATH := "res://art/story/opening/xiahou_dun_joins.png" +const OPENING_PROLOGUE_YINGCHUAN_PATH := "res://art/story/opening/yingchuan_first_contact.png" const SETTINGS_PATH := "user://heros_settings.json" const SETTINGS_SAVE_VERSION := 1 const OPENING_PROLOGUE_PAGES := [ { "title": "낙양의 그늘", - "body": "젊은 조조는 재주와 결단으로 이름을 얻었지만, 조정 안팎의 권세와 부패를 가까이 보며 법만으로는 세상을 붙들기 어렵다는 것을 알았다." + "body": "젊은 조조는 재주와 결단으로 이름을 얻었지만, 조정 안팎의 권세와 부패를 가까이 보며 법만으로는 세상을 붙들기 어렵다는 것을 알았다.", + "image": OPENING_PROLOGUE_REBELLION_PATH }, { "title": "맹덕의 결심", - "body": "황건의 봉기가 고을을 삼키자 조조는 더는 조정의 명만 기다리지 않았다. 그는 작은 군세라도 직접 세워 백성이 빠져나갈 길을 열기로 했다." + "body": "황건의 봉기가 고을을 삼키자 조조는 더는 조정의 명만 기다리지 않았다. 그는 작은 군세라도 직접 세워 백성이 빠져나갈 길을 열기로 했다.", + "image": OPENING_PROLOGUE_RESOLVE_PATH }, { "title": "하후돈의 합류", - "body": "가문의 친척 하후돈은 먼저 창을 들고 조조의 길에 섰다. 두 사람은 큰 명성보다 무너지는 고을을 구하는 첫 군령을 함께 택했다." + "body": "가문의 친척 하후돈은 먼저 창을 들고 조조의 길에 섰다. 두 사람은 큰 명성보다 무너지는 고을을 구하는 첫 군령을 함께 택했다.", + "image": OPENING_PROLOGUE_XIAHOU_DUN_PATH }, { "title": "영천으로", - "body": "황건의 깃발이 영천 들판에 섰다. 마을과 성채를 기대어 버티는 잔당을 꺾고, 백성이 빠져나갈 길을 열어야 한다." + "body": "황건의 깃발이 영천 들판에 섰다. 마을과 성채를 기대어 버티는 잔당을 꺾고, 백성이 빠져나갈 길을 열어야 한다.", + "image": OPENING_PROLOGUE_YINGCHUAN_PATH } ] const SFX_BOW_RELEASE_PATH := "res://audio/sfx/bow_release_01.wav" @@ -507,6 +515,8 @@ var settings_sfx_volume_value_label: Label var restart_button: Button var new_campaign_button: Button var opening_prologue_root: Control +var opening_prologue_background_texture_rect: TextureRect +var opening_prologue_scene_texture_rect: TextureRect var opening_prologue_title_label: Label var opening_prologue_body_label: Label var opening_prologue_step_label: Label @@ -2749,15 +2759,15 @@ func _create_hud() -> void: opening_prologue_root.set_anchors_preset(Control.PRESET_FULL_RECT) root.add_child(opening_prologue_root) - var opening_background := TextureRect.new() - opening_background.name = "OpeningBattlefieldBackdrop" - opening_background.mouse_filter = Control.MOUSE_FILTER_IGNORE - opening_background.set_anchors_preset(Control.PRESET_FULL_RECT) - opening_background.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - opening_background.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED - opening_background.texture = _load_art_texture(TITLE_BACKGROUND_PATH) - opening_background.modulate = Color(0.88, 0.88, 0.82, 0.98) - opening_prologue_root.add_child(opening_background) + opening_prologue_background_texture_rect = TextureRect.new() + opening_prologue_background_texture_rect.name = "OpeningStoryBackdrop" + opening_prologue_background_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + opening_prologue_background_texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT) + opening_prologue_background_texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + opening_prologue_background_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED + opening_prologue_background_texture_rect.texture = _load_art_texture(OPENING_PROLOGUE_REBELLION_PATH) + opening_prologue_background_texture_rect.modulate = Color(0.78, 0.78, 0.74, 0.98) + opening_prologue_root.add_child(opening_prologue_background_texture_rect) var opening_dim := ColorRect.new() opening_dim.name = "OpeningInkWash" @@ -2780,34 +2790,34 @@ func _create_hud() -> void: opening_panel.add_child(opening_shell) opening_shell.add_child(_make_bamboo_gutter(22, 548)) - var opening_portrait := TextureRect.new() - opening_portrait.name = "OpeningCaoCaoPortrait" - opening_portrait.custom_minimum_size = Vector2(356, 548) - opening_portrait.expand_mode = TextureRect.EXPAND_IGNORE_SIZE - opening_portrait.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED - opening_portrait.texture = _load_art_texture(TITLE_PORTRAIT_PATH) - opening_portrait.modulate = Color(0.98, 0.96, 0.91, 0.96) - opening_portrait.mouse_filter = Control.MOUSE_FILTER_IGNORE - opening_shell.add_child(opening_portrait) + opening_prologue_scene_texture_rect = TextureRect.new() + opening_prologue_scene_texture_rect.name = "OpeningStoryImage" + opening_prologue_scene_texture_rect.custom_minimum_size = Vector2(520, 548) + opening_prologue_scene_texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + opening_prologue_scene_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED + opening_prologue_scene_texture_rect.texture = _load_art_texture(OPENING_PROLOGUE_REBELLION_PATH) + opening_prologue_scene_texture_rect.modulate = Color(0.98, 0.96, 0.91, 0.98) + opening_prologue_scene_texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + opening_shell.add_child(opening_prologue_scene_texture_rect) var opening_story_column := VBoxContainer.new() - opening_story_column.custom_minimum_size = Vector2(586, 548) + opening_story_column.custom_minimum_size = Vector2(422, 548) opening_story_column.add_theme_constant_override("separation", 14) opening_shell.add_child(opening_story_column) opening_shell.add_child(_make_bamboo_gutter(22, 548)) - opening_story_column.add_child(_make_scroll_rod(586, 6)) - opening_story_column.add_child(_make_seal_ribbon(586, 18)) + opening_story_column.add_child(_make_scroll_rod(422, 6)) + opening_story_column.add_child(_make_seal_ribbon(422, 18)) opening_prologue_title_label = Label.new() - opening_prologue_title_label.custom_minimum_size = Vector2(586, 52) + opening_prologue_title_label.custom_minimum_size = Vector2(422, 52) opening_prologue_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER opening_prologue_title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _apply_label_style(opening_prologue_title_label, UI_PARCHMENT_TEXT, 26) opening_story_column.add_child(opening_prologue_title_label) var opening_body_panel := PanelContainer.new() - opening_body_panel.custom_minimum_size = Vector2(586, 250) + opening_body_panel.custom_minimum_size = Vector2(422, 250) _apply_panel_style(opening_body_panel, "edict") opening_story_column.add_child(opening_body_panel) @@ -2817,7 +2827,7 @@ func _create_hud() -> void: opening_body_row.add_child(_make_edict_marker_stack(["", "", ""], 206)) opening_prologue_body_label = Label.new() - opening_prologue_body_label.custom_minimum_size = Vector2(486, 206) + opening_prologue_body_label.custom_minimum_size = Vector2(322, 206) opening_prologue_body_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART opening_prologue_body_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _apply_label_style(opening_prologue_body_label, UI_AGED_INK, 20) @@ -2825,32 +2835,32 @@ func _create_hud() -> void: opening_body_row.add_child(_make_edict_marker_stack(["", "", ""], 206)) opening_prologue_step_label = Label.new() - opening_prologue_step_label.custom_minimum_size = Vector2(586, 26) + opening_prologue_step_label.custom_minimum_size = Vector2(422, 26) opening_prologue_step_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER opening_prologue_step_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _apply_label_style(opening_prologue_step_label, UI_OLD_BRONZE, 14) opening_story_column.add_child(opening_prologue_step_label) var opening_button_row := HBoxContainer.new() - opening_button_row.custom_minimum_size = Vector2(586, 44) + opening_button_row.custom_minimum_size = Vector2(422, 44) opening_button_row.add_theme_constant_override("separation", 10) opening_story_column.add_child(opening_button_row) opening_prologue_skip_button = Button.new() opening_prologue_skip_button.text = "건너뛰기" - opening_prologue_skip_button.custom_minimum_size = Vector2(188, 42) + opening_prologue_skip_button.custom_minimum_size = Vector2(132, 42) opening_prologue_skip_button.tooltip_text = "오프닝을 닫고 군막으로 이동합니다." opening_prologue_skip_button.pressed.connect(_on_opening_prologue_skip_pressed) opening_button_row.add_child(opening_prologue_skip_button) opening_prologue_next_button = Button.new() opening_prologue_next_button.text = "다음" - opening_prologue_next_button.custom_minimum_size = Vector2(388, 42) + opening_prologue_next_button.custom_minimum_size = Vector2(280, 42) opening_prologue_next_button.tooltip_text = "다음 이야기로 넘깁니다." opening_prologue_next_button.pressed.connect(_on_opening_prologue_next_pressed) opening_button_row.add_child(opening_prologue_next_button) - opening_story_column.add_child(_make_scroll_rod(586, 5)) + opening_story_column.add_child(_make_scroll_rod(422, 5)) briefing_panel = PanelContainer.new() briefing_panel.visible = false @@ -5039,10 +5049,14 @@ func _event_marker_color(kind: String) -> Color: return Color(0.92, 0.62, 0.18, 1.0) if kind == "supply": return Color(0.38, 0.62, 0.34, 1.0) + if kind == "tactic": + return Color(0.16, 0.48, 0.44, 1.0) return Color(UI_MUTED_JADE.r, UI_MUTED_JADE.g, UI_MUTED_JADE.b, 1.0) func _event_marker_abbrev(label: String, kind: String) -> String: + if kind == "tactic": + return "유" if label.contains("유인") else "전" if kind == "gold": return "금" var text := label.strip_edges() @@ -11262,6 +11276,14 @@ func _update_opening_prologue_page() -> void: return opening_prologue_index = mini(maxi(opening_prologue_index, 0), page_count - 1) var page: Dictionary = OPENING_PROLOGUE_PAGES[opening_prologue_index] + var image_path := str(page.get("image", TITLE_BACKGROUND_PATH)) + var image_texture := _load_art_texture(image_path) + if image_texture == null: + image_texture = _load_art_texture(TITLE_BACKGROUND_PATH) + if opening_prologue_background_texture_rect != null: + opening_prologue_background_texture_rect.texture = image_texture + if opening_prologue_scene_texture_rect != null: + opening_prologue_scene_texture_rect.texture = image_texture if opening_prologue_title_label != null: opening_prologue_title_label.text = str(page.get("title", "")) if opening_prologue_body_label != null: diff --git a/tools/smoke_objective_progress.gd b/tools/smoke_objective_progress.gd index 9598e45..01f1992 100644 --- a/tools/smoke_objective_progress.gd +++ b/tools/smoke_objective_progress.gd @@ -335,16 +335,23 @@ func _check_opening_battle_side_event_markers(failures: Array[String]) -> void: return var cache_cell := Vector2i(4, 2) + var lure_cell := Vector2i(4, 6) var village_cell := Vector2i(6, 5) var initial_cells := state.get_event_marker_cells() if not initial_cells.has(cache_cell): failures.append("opening battle should mark the northern woods supply cache: %s" % str(initial_cells)) + if not initial_cells.has(lure_cell): + failures.append("opening battle should mark the first tactical lure line: %s" % str(initial_cells)) if not initial_cells.has(village_cell): failures.append("opening battle should mark village supply side event cells: %s" % str(initial_cells)) if state.get_event_marker_label(cache_cell) != "북숲 보급고": failures.append("northern woods cache marker should expose Korean label: %s" % state.get_event_marker_label(cache_cell)) if state.get_event_marker_kind(cache_cell) != "supply": failures.append("northern woods cache marker should be a supply marker") + if state.get_event_marker_label(lure_cell) != "유인선": + failures.append("lure line marker should expose Korean label: %s" % state.get_event_marker_label(lure_cell)) + if state.get_event_marker_kind(lure_cell) != "tactic": + failures.append("lure line marker should be a tactic marker") if state.get_event_marker_label(village_cell) != "마을 보급": failures.append("village supply marker should expose Korean label: %s" % state.get_event_marker_label(village_cell)) if state.get_event_marker_kind(village_cell) != "supply": diff --git a/tools/smoke_title_menu.gd b/tools/smoke_title_menu.gd index 70d800d..63ecaf9 100644 --- a/tools/smoke_title_menu.gd +++ b/tools/smoke_title_menu.gd @@ -122,7 +122,14 @@ func _check_title_menu_start_load_and_settings(failures: Array[String]) -> void: failures.append("opening prologue should first explain Cao Cao before the Yellow Turban battle") if scene.opening_prologue_step_label == null or not scene.opening_prologue_step_label.text.ends_with("/4"): failures.append("opening prologue should give Cao Cao, decision, Xiahou Dun, and Yingchuan their own beats") + if scene.opening_prologue_scene_texture_rect == null or scene.opening_prologue_scene_texture_rect.texture == null: + failures.append("opening prologue should show generated story artwork") + var first_story_texture: Texture2D = null + if scene.opening_prologue_scene_texture_rect != null: + first_story_texture = scene.opening_prologue_scene_texture_rect.texture scene._on_opening_prologue_next_pressed() + if scene.opening_prologue_scene_texture_rect == null or scene.opening_prologue_scene_texture_rect.texture == first_story_texture: + failures.append("opening prologue should change story artwork per page") if scene.opening_prologue_title_label == null or scene.opening_prologue_title_label.text != "맹덕의 결심": failures.append("opening prologue should move from Cao Cao background to his decision") elif not scene.opening_prologue_body_label.text.contains("황건") or not scene.opening_prologue_body_label.text.contains("작은 군세"): diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 5dbccbb..eb6d555 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -143,6 +143,18 @@ func _check_battle_visual_data(failures: Array[String]) -> void: _check_image_path(failures, state.get_map_background_path(), "001 map background") if state.get_map_background_path() != "res://art/backgrounds/yingchuan_opening_battlefield.png": failures.append("001 should use a dedicated Yingchuan battlefield background") + var opening_story_images: Array[String] = [] + for page in BattleSceneScript.OPENING_PROLOGUE_PAGES: + var image_path := str((page as Dictionary).get("image", "")) + if image_path.is_empty(): + failures.append("opening prologue pages should reference generated story artwork") + continue + if opening_story_images.has(image_path): + failures.append("opening prologue pages should use distinct story artwork: %s" % image_path) + opening_story_images.append(image_path) + _check_image_path(failures, image_path, "opening prologue story art") + if opening_story_images.size() != 4: + failures.append("opening prologue should expose four generated story panels") _check_scenario_backgrounds_data(failures) for unit_id in ["cao_cao", "xiahou_dun", "yellow_turban_1", "yellow_turban_2", "yellow_turban_3"]: var unit := state.get_unit(unit_id) @@ -4072,6 +4084,10 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: var event_hover_badge := scene._hover_info_badge() if str(event_hover_badge.get("kind", "")) != "event" or not str(event_hover_badge.get("lines", [])).contains("표식 북숲 보급고"): failures.append("event hover info badge should expose marker label: %s" % str(event_hover_badge)) + scene.hover_cell = Vector2i(4, 6) + var tactic_hover_badge := scene._hover_info_badge() + if str(tactic_hover_badge.get("kind", "")) != "event" or not str(tactic_hover_badge.get("lines", [])).contains("표식 유인선"): + failures.append("tactic hover info badge should expose lure-line marker label: %s" % str(tactic_hover_badge)) scene.hover_cell = Vector2i(20, 1) var objective_hover_badge := scene._hover_info_badge() var objective_hover_text := str(objective_hover_badge.get("lines", [])) @@ -4175,6 +4191,12 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: scene._update_cell_info() if scene.cell_info_label == null or not scene.cell_info_label.text.contains("표식 북숲 보급고"): failures.append("side event marker cell info should expose the supply label: %s" % scene.cell_info_label.text) + scene.hover_cell = Vector2i(4, 6) + scene._update_cell_info() + if scene.cell_info_label == null or not scene.cell_info_label.text.contains("표식 유인선"): + failures.append("tactical lure marker cell info should expose the marker label: %s" % scene.cell_info_label.text) + if scene.state.get_event_marker_kind(Vector2i(4, 6)) != "tactic": + failures.append("tactical lure marker should expose tactic kind") scene.hover_cell = Vector2i(-1, -1) scene._update_cell_info() if scene.cell_info_panel != null and scene.cell_info_panel.visible: @@ -4201,6 +4223,10 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: failures.append("Supply marker fallback should stay concise") if scene._event_marker_abbrev("군자금", "gold") != "금": failures.append("Gold marker should use a compact gold glyph") + if scene._event_marker_abbrev("유인선", "tactic") != "유": + failures.append("Tactic lure marker should use a compact lure glyph") + if scene._event_marker_color("tactic") == scene._event_marker_color("supply"): + failures.append("Tactic marker should be visually distinct from supply markers") var background_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", true) var fallback_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", false) if background_fill.a >= 0.20: