diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index eebac9e..ed383d0 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -169,7 +169,7 @@ const TITLE_MENU_ICON_MAX_WIDTH := 42 const OPENING_STORY_PROGRESS_CARD_SIZE := Vector2(50, 24) const OPENING_STORY_PROGRESS_KNOT_SIZE := Vector2(18, 18) const COMMAND_NOTICE_ICON_SIZE := Vector2(32, 32) -const COMMAND_NOTICE_MAX_ICONS := 4 +const COMMAND_NOTICE_MAX_ICONS := 5 const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54) const CHAPTER_SCENARIO_THUMBNAIL_SIZE := Vector2(72, 50) const CHAPTER_INFO_BADGE_SIZE := Vector2(86, 22) @@ -11220,18 +11220,15 @@ func _command_notice_icon_entries(title: String, body: String) -> Array[Dictiona var entries: Array[Dictionary] = [] var seen := {} var combined_text := "%s\n%s" % [title, body] - if combined_text.contains("아군"): + var is_player_turn_notice := title.contains("아군") or body.contains("아군 차례") + var is_enemy_turn_notice := title.contains("적군") or body.contains("적군 차례") + if is_player_turn_notice: _append_command_notice_icon_entry(entries, seen, "toolbar", "move", "행군") _append_command_notice_icon_entry(entries, seen, "toolbar", "attack", "타격") _append_command_notice_icon_entry(entries, seen, "toolbar", "wait", "대기") - elif combined_text.contains("적군"): + elif is_enemy_turn_notice: _append_command_notice_icon_entry(entries, seen, "toolbar", "threat", "적세") _append_command_notice_icon_entry(entries, seen, "toolbar", "forecast", "예측") - if combined_text.contains("증원") or combined_text.contains("잔병") or combined_text.contains("약탈대") or combined_text.contains("잔당") or combined_text.contains("반격"): - _append_command_notice_icon_entry(entries, seen, "toolbar", "threat", "적세") - _append_command_notice_icon_entry(entries, seen, "toolbar", "status", "주의") - if combined_text.contains("경계") or combined_text.contains("표적") or combined_text.contains("노린다") or combined_text.contains("시선"): - _append_command_notice_icon_entry(entries, seen, "toolbar", "threat", "적세") if combined_text.contains("목표") or combined_text.contains("성채") or combined_text.contains("깃발"): _append_command_notice_icon_entry(entries, seen, "badge", "objective", "목표") if combined_text.contains("유인"): @@ -11242,10 +11239,19 @@ func _command_notice_icon_entries(title: String, body: String) -> Array[Dictiona _append_command_notice_icon_entry(entries, seen, "badge", "recover", "회복") if combined_text.contains("군자금"): _append_command_notice_icon_entry(entries, seen, "badge", "gold", "군자금") + if combined_text.contains("증원") or combined_text.contains("잔병") or combined_text.contains("약탈대") or combined_text.contains("잔당") or combined_text.contains("반격"): + _append_command_notice_icon_entry(entries, seen, "toolbar", "threat", "적세") + _append_command_notice_icon_entry(entries, seen, "toolbar", "status", "주의") + if combined_text.contains("경계") or combined_text.contains("표적") or combined_text.contains("노린다") or combined_text.contains("시선"): + _append_command_notice_icon_entry(entries, seen, "toolbar", "threat", "적세") if combined_text.contains("퇴각") or combined_text.contains("주의"): _append_command_notice_icon_entry(entries, seen, "toolbar", "status", "주의") if entries.is_empty(): _append_command_notice_icon_entry(entries, seen, "badge", "event", "군령") + for index in range(entries.size()): + var entry := entries[index] + entry["tooltip"] = _command_notice_icon_entry_tooltip(entry, title, body) + entries[index] = entry return entries @@ -11265,9 +11271,11 @@ func _append_command_notice_icon_entry(entries: Array[Dictionary], seen: Diction func _make_command_notice_icon_chip(entry: Dictionary) -> PanelContainer: var label_text := str(entry.get("label", "군령")) + var tooltip_text := str(entry.get("tooltip", label_text)) var chip := PanelContainer.new() chip.name = "CommandNoticeIconChip" chip.custom_minimum_size = COMMAND_NOTICE_ICON_SIZE + chip.tooltip_text = tooltip_text chip.mouse_filter = Control.MOUSE_FILTER_PASS _apply_panel_style(chip, "caption") var chip_style := chip.get_theme_stylebox("panel") @@ -11277,16 +11285,41 @@ func _make_command_notice_icon_chip(entry: Dictionary) -> PanelContainer: chip_style.content_margin_top = 2 chip_style.content_margin_bottom = 2 + chip.add_child(_make_command_notice_icon_art(entry, tooltip_text)) + _set_control_tree_tooltip(chip, tooltip_text) + return chip + + +func _make_command_notice_icon_art(entry: Dictionary, tooltip_text: String) -> Control: + var stack := Control.new() + stack.name = "CommandNoticeIconArt" + stack.custom_minimum_size = COMMAND_NOTICE_ICON_SIZE + stack.tooltip_text = tooltip_text + stack.mouse_filter = Control.MOUSE_FILTER_PASS + var source := str(entry.get("source", "toolbar")) + if source == "badge": + var marker_texture := _load_tile_marker_texture(_command_notice_tile_marker_key(str(entry.get("key", "event")))) + if marker_texture != null: + var marker_rect := TextureRect.new() + marker_rect.name = "CommandNoticeTileMarkerImage" + marker_rect.set_anchors_preset(Control.PRESET_FULL_RECT) + marker_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + marker_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + marker_rect.modulate = Color(1.0, 1.0, 1.0, 0.42) + marker_rect.mouse_filter = Control.MOUSE_FILTER_PASS + marker_rect.texture = marker_texture + marker_rect.tooltip_text = tooltip_text + stack.add_child(marker_rect) var icon := TextureRect.new() icon.name = "CommandNoticeIconImage" - icon.custom_minimum_size = COMMAND_NOTICE_ICON_SIZE + icon.set_anchors_preset(Control.PRESET_FULL_RECT) icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED icon.mouse_filter = Control.MOUSE_FILTER_PASS icon.texture = _command_notice_icon_texture(entry) - chip.add_child(icon) - _set_control_tree_tooltip(chip, label_text) - return chip + icon.tooltip_text = tooltip_text + stack.add_child(icon) + return stack func _command_notice_icon_texture(entry: Dictionary) -> Texture2D: @@ -11297,6 +11330,61 @@ func _command_notice_icon_texture(entry: Dictionary) -> Texture2D: return _make_toolbar_icon(key) +func _command_notice_tile_marker_key(key: String) -> String: + match key: + "objective": + return "objective" + "tactic": + return "target" + "supply": + return "recover" + "recover": + return "recover" + "gold": + return "select" + _: + return "select" + + +func _command_notice_icon_entry_tooltip(entry: Dictionary, title: String, body: String) -> String: + var label_text := str(entry.get("label", "군령")) + var key := str(entry.get("key", "status")) + var detail := "" + match key: + "objective": + detail = _first_notice_body_line(body, ["목표", "성채", "깃발"]) + "status": + detail = _first_notice_body_line(body, ["주의", "퇴각", "위험", "패"]) + "tactic": + detail = _first_notice_body_line(body, ["유인", "표식"]) + "supply": + detail = _first_notice_body_line(body, ["보급", "마을"]) + "recover": + detail = _first_notice_body_line(body, ["회복", "마을"]) + "gold": + detail = _first_notice_body_line(body, ["군자금"]) + "threat", "forecast": + detail = _first_notice_body_line(body, ["적군", "증원", "잔병", "경계", "표적", "주의"]) + "move", "attack", "wait": + detail = _first_notice_body_line(body, ["아군", "군령", "차례"]) + if detail.is_empty(): + detail = body.strip_edges() + if detail.is_empty(): + detail = title.strip_edges() + return "%s\n%s" % [label_text, _short_preview_text(detail, 110)] + + +func _first_notice_body_line(body: String, keywords: Array[String]) -> String: + for line in body.split("\n", false): + var normalized_line := str(line).strip_edges() + if normalized_line.is_empty(): + continue + for keyword in keywords: + if normalized_line.contains(keyword): + return normalized_line + return "" + + func _on_dialogue_requested(lines: Array) -> void: var normalized_lines := _normalized_dialogue_lines(lines) if normalized_lines.is_empty(): diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index db09a79..0b787e3 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -580,6 +580,12 @@ func _check_battle_start_notice_sequence(failures: Array[String]) -> void: failures.append("battle start notice should not expose raw grid coordinates: %s" % start_notice_text) if scene.objective_notice_icon_strip == null or scene.objective_notice_icon_strip.get_child_count() < 3: failures.append("battle start notice should use generated objective, warning, and marker icons") + else: + var notice_icon_tooltips := _collect_tooltips(scene.objective_notice_icon_strip) + if not notice_icon_tooltips.contains("목표:") or not notice_icon_tooltips.contains("주의:") or not notice_icon_tooltips.contains("유인선") or not notice_icon_tooltips.contains("북숲 보급고") or not notice_icon_tooltips.contains("마을 보급"): + failures.append("battle start notice icon hover should explain objective, risk, and tactical markers: %s" % notice_icon_tooltips) + if _count_descendants_by_name(scene.objective_notice_icon_strip, "CommandNoticeTileMarkerImage") < 3: + failures.append("battle start marker notice chips should layer generated map marker art behind objective/tactic/supply icons") scene.free() @@ -1971,3 +1977,31 @@ func _check_pixel_unit_idle_state(scene, failures: Array[String]) -> void: failures.append("pixel idle clock should not advance while dialogue is visible") scene.dialogue_panel.free() scene.dialogue_panel = null + + +func _collect_tooltips(root: Node) -> String: + var parts: Array[String] = [] + _collect_tooltips_into(root, parts) + return "\n".join(parts) + + +func _collect_tooltips_into(root: Node, parts: Array[String]) -> void: + if root == null: + return + if root is Control: + var tooltip := str((root as Control).tooltip_text).strip_edges() + if not tooltip.is_empty(): + parts.append(tooltip) + for child in root.get_children(): + _collect_tooltips_into(child, parts) + + +func _count_descendants_by_name(root: Node, node_name: String) -> int: + if root == null: + return 0 + var count := 0 + for child in root.get_children(): + if child.name == node_name: + count += 1 + count += _count_descendants_by_name(child, node_name) + return count