diff --git a/README.md b/README.md index 1342342..fb48b60 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, readable zone badges, and hover placement instructions. - Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, first-clear reward preview, generated battlefield thumbnail, allied portrait/enemy unit force markers, and image-first tactical marker cards for lure lines and supply points. - Pre-battle prep commands use compact icon buttons with active selection state and Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records. +- Post-move tactic and item pickers use generated command/item icons, compact two-line option rows, and hover details instead of dense single-line text. - Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons` and generated lacquer/jade/cinnabar button surfaces under `art/ui/buttons`, with procedural icon drawing and flat button styles kept only as fallbacks. - Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop, with generated transparent feature overlays for road connections, water, villages, castles, and wasteland accents. - Core UI panels now prefer generated high-resolution lacquer, scroll, jade, and command-seal frame textures under `art/ui/panels`, with the calmer ink/wood/jade flat styles kept as fallbacks instead of the earlier yellow, hard-edged frame treatment. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index f5d9cea..8c368c8 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -183,6 +183,8 @@ const POST_MOVE_TITLE_SIZE := Vector2(196, 22) const POST_MOVE_PICKER_PANEL_SIZE := Vector2(336, 236) const POST_MOVE_PICKER_TITLE_SIZE := Vector2(296, 22) const POST_MOVE_PICKER_DETAIL_SIZE := Vector2(296, 32) +const POST_MOVE_PICKER_OPTION_SIZE := Vector2(296, 46) +const POST_MOVE_PICKER_OPTION_ICON_MAX_WIDTH := 34 const TARGETING_HINT_PANEL_SIZE := Vector2(254, 118) const TARGETING_HINT_TITLE_SIZE := Vector2(220, 22) const TARGETING_HINT_DETAIL_SIZE := Vector2(220, 28) @@ -14910,12 +14912,8 @@ func _rebuild_post_move_tactic_picker(selected: Dictionary) -> void: for skill_id_value in skill_ids: var skill_id := str(skill_id_value) var skill := state.get_skill_def(skill_id) - var skill_button := Button.new() - skill_button.text = _format_post_move_tactic_button_text(skill_id, skill, selected) var disabled_reason := _post_move_tactic_disabled_reason(selected, skill_id, skill) - skill_button.disabled = not disabled_reason.is_empty() - _suppress_local_command_tooltip(skill_button) - _apply_button_style(skill_button) + var skill_button := _make_post_move_tactic_option_button(skill_id, skill, disabled_reason) skill_button.pressed.connect(_on_post_move_tactic_option_pressed.bind(skill_id)) post_move_picker_list.add_child(skill_button) @@ -14933,17 +14931,115 @@ func _rebuild_post_move_item_picker(selected: Dictionary) -> void: var item_id := str(item_id_value) var item := state.get_item_def(item_id) var count := int(inventory.get(item_id, 0)) - var item_button := Button.new() - item_button.text = _format_post_move_item_button_text(item_id, item, count) - _apply_item_button_icon(item_button, item) var disabled_reason := _post_move_item_disabled_reason(selected, item_id, count) - item_button.disabled = not disabled_reason.is_empty() - _suppress_local_command_tooltip(item_button) - _apply_button_style(item_button) + var item_button := _make_post_move_item_option_button(item_id, item, count, disabled_reason) item_button.pressed.connect(_on_post_move_item_option_pressed.bind(item_id)) post_move_picker_list.add_child(item_button) +func _make_post_move_tactic_option_button(skill_id: String, skill: Dictionary, disabled_reason: String) -> Button: + var skill_name := str(skill.get("name", skill_id)) + var mp_cost := int(skill.get("mp_cost", 0)) + var kind_text := _skill_kind_text(str(skill.get("kind", "skill"))) + var range_text := _format_tactic_range_text(skill) + var effect_text := _format_tactic_effect_text(skill) + var detail_text := "%s · 거리 %s" % [kind_text, range_text] + if not effect_text.is_empty(): + detail_text = "%s · %s" % [detail_text, effect_text] + if not disabled_reason.is_empty(): + detail_text = "%s · %s" % [detail_text, _post_move_disabled_summary(disabled_reason)] + + var button := Button.new() + button.text = "%s 기력 %d\n%s" % [skill_name, mp_cost, detail_text] + button.tooltip_text = _format_post_move_tactic_option_tooltip(skill_name, mp_cost, kind_text, range_text, effect_text, disabled_reason) + button.disabled = not disabled_reason.is_empty() + button.custom_minimum_size = POST_MOVE_PICKER_OPTION_SIZE + button.icon = _post_move_tactic_icon_texture(skill) + button.expand_icon = false + button.add_theme_constant_override("icon_max_width", POST_MOVE_PICKER_OPTION_ICON_MAX_WIDTH) + button.set_meta("picker_icon", _post_move_tactic_icon_kind(skill)) + button.set_meta("picker_label", skill_name) + _apply_button_style(button) + return button + + +func _make_post_move_item_option_button(item_id: String, item: Dictionary, count: int, disabled_reason: String) -> Button: + var item_name := _item_display_name(item_id) + var effect_text := _format_post_move_item_effect_text(item) + var detail_text := "잔량 %d · %s" % [count, effect_text] + if not disabled_reason.is_empty(): + detail_text = "%s · %s" % [detail_text, _post_move_disabled_summary(disabled_reason)] + + var button := Button.new() + button.text = "%s\n%s" % [item_name, detail_text] + button.tooltip_text = _format_post_move_item_option_tooltip(item_name, count, effect_text, disabled_reason) + button.disabled = not disabled_reason.is_empty() + button.custom_minimum_size = POST_MOVE_PICKER_OPTION_SIZE + button.icon = _post_move_item_icon_texture(item) + button.expand_icon = false + button.add_theme_constant_override("icon_max_width", POST_MOVE_PICKER_OPTION_ICON_MAX_WIDTH) + button.set_meta("picker_icon", "item") + button.set_meta("picker_label", item_name) + _apply_button_style(button) + return button + + +func _post_move_tactic_icon_kind(skill: Dictionary) -> String: + var kind := str(skill.get("kind", "skill")).strip_edges().to_lower() + if kind == "damage": + return "attack" + if kind == "heal": + return "item" + if kind == "support": + return "status" + return "tactic" + + +func _post_move_tactic_icon_texture(skill: Dictionary) -> Texture2D: + return _make_toolbar_icon(_post_move_tactic_icon_kind(skill)) + + +func _post_move_item_icon_texture(item: Dictionary) -> Texture2D: + var icon := _load_art_texture(str(item.get("icon", ""))) + if icon != null: + return icon + return _make_toolbar_icon("item") + + +func _format_post_move_tactic_option_tooltip(skill_name: String, mp_cost: int, kind_text: String, range_text: String, effect_text: String, disabled_reason: String) -> String: + var lines: Array[String] = [ + skill_name, + "기력 %d · %s · 사거리 %s" % [mp_cost, kind_text, range_text] + ] + if not effect_text.is_empty(): + lines.append(effect_text) + if not disabled_reason.is_empty(): + lines.append(disabled_reason) + return _join_strings(lines, "\n") + + +func _format_post_move_item_option_tooltip(item_name: String, count: int, effect_text: String, disabled_reason: String) -> String: + var lines: Array[String] = [ + item_name, + "잔량 %d · %s" % [count, effect_text] + ] + if not disabled_reason.is_empty(): + lines.append(disabled_reason) + return _join_strings(lines, "\n") + + +func _post_move_disabled_summary(disabled_reason: String) -> String: + if disabled_reason.contains("기력"): + return "기력 부족" + if disabled_reason.contains("봉인"): + return "봉인" + if disabled_reason.contains("대상"): + return "표식 없음" + if disabled_reason.contains("보급") or disabled_reason.contains("없습니다"): + return "잔량 없음" + return "불가" + + func _format_post_move_tactic_button_text(skill_id: String, skill: Dictionary, selected: Dictionary) -> String: var marker := "> " if selected_skill_id == skill_id else "" var skill_name := str(skill.get("name", skill_id)) diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index 1544669..ee5a9b5 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -832,6 +832,7 @@ func _check_scene_post_move_tactic_picker_flow(failures: Array[String]) -> void: if scene.tactic_menu != null and scene.tactic_menu.visible: failures.append("post-move tactic shortcut should not use the side tactic menu") _check_post_move_picker_option_tooltips_present(failures, scene, "post-move tactic picker") + _check_post_move_picker_option_visuals(failures, scene, "post-move tactic picker") var escape := InputEventKey.new() escape.keycode = KEY_ESCAPE @@ -941,6 +942,7 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void: if scene.item_menu != null and scene.item_menu.visible: failures.append("post-move item shortcut should not use the side item menu") _check_post_move_picker_option_tooltips_present(failures, scene, "post-move item picker") + _check_post_move_picker_option_visuals(failures, scene, "post-move item picker") scene._on_post_move_item_option_pressed("bean") if scene.selected_item_id != "bean": @@ -1027,6 +1029,28 @@ func _check_post_move_picker_option_tooltips_present(failures: Array[String], sc failures.append("%s option should explain its function through hover tooltip" % label) +func _check_post_move_picker_option_visuals(failures: Array[String], scene, label: String) -> void: + if scene.post_move_picker_list == null: + failures.append("%s should expose a visual picker option list" % label) + return + var saw_button := false + for child in scene.post_move_picker_list.get_children(): + if not child is Button: + continue + saw_button = true + var button := child as Button + if button.icon == null or str(button.get_meta("picker_icon", "")).strip_edges().is_empty(): + failures.append("%s option should carry an immediate generated icon" % label) + if button.custom_minimum_size.x < BattleSceneScript.POST_MOVE_PICKER_OPTION_SIZE.x or button.custom_minimum_size.y < BattleSceneScript.POST_MOVE_PICKER_OPTION_SIZE.y: + failures.append("%s option should reserve stable visual row space: %s" % [label, str(button.custom_minimum_size)]) + if not button.text.contains("\n"): + failures.append("%s option should use compact two-line visual text: %s" % [label, button.text]) + if button.get_theme_constant("icon_max_width") < BattleSceneScript.POST_MOVE_PICKER_OPTION_ICON_MAX_WIDTH: + failures.append("%s option should reserve readable icon width" % label) + if not saw_button: + failures.append("%s should create at least one visual option button" % label) + + func _check_fitted_label_font(failures: Array[String], label: Label, base_size: int, min_size: int, label_name: String) -> void: var font_size := label.get_theme_font_size("font_size") if font_size < min_size or font_size > base_size: