diff --git a/README.md b/README.md index 8eda980..ac21bc4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Unit selection. - Move, attack, wait, end turn. - Automatic end-turn prompt appears after all controllable allies finish acting, using a styled command-complete confirmation. -- Ally, enemy, objective phase changes, and battle-start tactical reminders raise compact command notices with generated visual icon chips, short marker summaries, and hover details in the active battle HUD. +- Ally, enemy, objective phase changes, and battle-start tactical reminders raise compact command notices with generated visual icon chips, short marker summaries, hover details, and clickable marker chips that focus the large battlefield map on objectives, lure lines, and supply points. - Enemy AI with movement, damage-aware physical attacks, multiple MP tactic choices, scenario target priorities, and sentry-awake state that events can retune mid-battle. - Hover tile and unit info with class, movement type, AGI, movement, range, and equipped gear. - Active battle unit list opens from the top toolbar with ally/enemy tabs, portrait or sprite rows, compact HP/status/zone badges, and hover combat details. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 0225a9c..56e22c8 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -3319,7 +3319,7 @@ func _create_hud() -> void: objective_notice_panel.visible = false objective_notice_panel.position = Vector2(338, 88) objective_notice_panel.size = Vector2(604, 170) - objective_notice_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + objective_notice_panel.mouse_filter = Control.MOUSE_FILTER_PASS _apply_panel_style(objective_notice_panel, "notice_edict") root.add_child(objective_notice_panel) @@ -11488,13 +11488,13 @@ func _command_notice_icon_entries(title: String, body: String) -> Array[Dictiona _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("깃발"): - _append_command_notice_icon_entry(entries, seen, "badge", "objective", "목표") + _append_command_notice_icon_entry(entries, seen, "badge", "objective", "목표", _command_notice_focus_cell_for_key("objective")) if combined_text.contains("유인"): - _append_command_notice_icon_entry(entries, seen, "badge", "tactic", "유인") + _append_command_notice_icon_entry(entries, seen, "badge", "tactic", "유인", _command_notice_focus_cell_for_key("tactic")) if combined_text.contains("보급") or combined_text.contains("마을"): - _append_command_notice_icon_entry(entries, seen, "badge", "supply", "보급") + _append_command_notice_icon_entry(entries, seen, "badge", "supply", "보급", _command_notice_focus_cell_for_key("supply")) if combined_text.contains("회복"): - _append_command_notice_icon_entry(entries, seen, "badge", "recover", "회복") + _append_command_notice_icon_entry(entries, seen, "badge", "recover", "회복", _command_notice_focus_cell_for_key("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("반격"): @@ -11513,18 +11513,21 @@ func _command_notice_icon_entries(title: String, body: String) -> Array[Dictiona return entries -func _append_command_notice_icon_entry(entries: Array[Dictionary], seen: Dictionary, source: String, key: String, label: String) -> void: +func _append_command_notice_icon_entry(entries: Array[Dictionary], seen: Dictionary, source: String, key: String, label: String, focus_cell := Vector2i(-1, -1)) -> void: if entries.size() >= COMMAND_NOTICE_MAX_ICONS: return var entry_key := "%s/%s" % [source, key] if seen.has(entry_key): return seen[entry_key] = true - entries.append({ + var entry := { "source": source, "key": key, "label": label - }) + } + if state.is_inside(focus_cell): + entry["focus_cell"] = focus_cell + entries.append(entry) func _make_command_notice_icon_chip(entry: Dictionary) -> PanelContainer: @@ -11534,7 +11537,13 @@ func _make_command_notice_icon_chip(entry: Dictionary) -> PanelContainer: chip.name = "CommandNoticeIconChip" chip.custom_minimum_size = COMMAND_NOTICE_ICON_SIZE chip.tooltip_text = tooltip_text - chip.mouse_filter = Control.MOUSE_FILTER_PASS + chip.mouse_filter = Control.MOUSE_FILTER_STOP + chip.set_meta("notice_icon_key", str(entry.get("key", ""))) + var focus_cell: Vector2i = entry.get("focus_cell", Vector2i(-1, -1)) + if state.is_inside(focus_cell): + chip.set_meta("focus_cell", focus_cell) + chip.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND + chip.gui_input.connect(_on_command_notice_icon_gui_input.bind(chip)) _apply_panel_style(chip, "caption") var chip_style := chip.get_theme_stylebox("panel") if chip_style != null: @@ -11548,6 +11557,30 @@ func _make_command_notice_icon_chip(entry: Dictionary) -> PanelContainer: return chip +func _on_command_notice_icon_gui_input(event: InputEvent, chip: Control) -> void: + if not (event is InputEventMouseButton): + return + var mouse_event := event as InputEventMouseButton + if mouse_event.button_index != MOUSE_BUTTON_LEFT or not mouse_event.pressed: + return + if _focus_board_on_command_notice_chip(chip): + _play_ui_click() + get_viewport().set_input_as_handled() + + +func _focus_board_on_command_notice_chip(chip: Control) -> bool: + if chip == null or not battle_started: + return false + var focus_cell: Vector2i = chip.get_meta("focus_cell", Vector2i(-1, -1)) + if not state.is_inside(focus_cell): + return false + hover_cell = focus_cell + _focus_board_on_cell(focus_cell, true) + _update_hud() + queue_redraw() + return true + + func _make_command_notice_icon_art(entry: Dictionary, tooltip_text: String) -> Control: var stack := Control.new() stack.name = "CommandNoticeIconArt" @@ -11635,7 +11668,51 @@ func _command_notice_icon_entry_tooltip(entry: Dictionary, title: String, body: detail = body.strip_edges() if detail.is_empty(): detail = title.strip_edges() - return "%s\n%s" % [label_text, _short_preview_text(detail, 110)] + var tooltip := "%s\n%s" % [label_text, _short_preview_text(detail, 110)] + var focus_cell: Vector2i = entry.get("focus_cell", Vector2i(-1, -1)) + if state.is_inside(focus_cell): + tooltip += "\n클릭하면 지도에서 해당 지점을 봅니다." + return tooltip + + +func _command_notice_focus_cell_for_key(key: String) -> Vector2i: + if key == "objective": + return _representative_focus_cell(state.get_objective_cells()) + for marker in state.get_event_markers(): + var label := str(marker.get("label", "")).strip_edges() + var kind := str(marker.get("kind", "event")).strip_edges().to_lower() + var hint := str(marker.get("hint", "")).strip_edges() + if label.is_empty(): + continue + if not _battle_start_marker_matches_notice_key(label, kind, hint, key): + continue + var cell := _representative_focus_cell(marker.get("cells", [])) + if state.is_inside(cell): + return cell + return Vector2i(-1, -1) + + +func _representative_focus_cell(cells) -> Vector2i: + if typeof(cells) != TYPE_ARRAY: + return Vector2i(-1, -1) + var valid_cells: Array[Vector2i] = [] + for cell in cells: + if typeof(cell) == TYPE_VECTOR2I and state.is_inside(cell): + valid_cells.append(cell) + if valid_cells.is_empty(): + return Vector2i(-1, -1) + var sum := Vector2.ZERO + for cell in valid_cells: + sum += Vector2(cell.x, cell.y) + var average := sum / float(valid_cells.size()) + var best_cell := valid_cells[0] + var best_distance := INF + for cell in valid_cells: + var distance := Vector2(cell.x, cell.y).distance_squared_to(average) + if distance < best_distance: + best_distance = distance + best_cell = cell + return best_cell func _battle_start_marker_tooltip_detail_for_key(key: String) -> String: diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 6beea30..647bcec 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -592,6 +592,32 @@ func _check_battle_start_notice_sequence(failures: Array[String]) -> void: failures.append("battle start notice icon hover should preserve marker hint `%s`: %s" % [expected_hint, 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") + if scene.objective_notice_panel != null and scene.objective_notice_panel.mouse_filter == Control.MOUSE_FILTER_IGNORE: + failures.append("battle start notice panel should allow marker icon hover and click guidance") + for expected_key in ["objective", "tactic", "supply", "recover"]: + var chip := _find_command_notice_chip_by_key(scene.objective_notice_icon_strip, expected_key) + if chip == null: + failures.append("battle start notice should expose a clickable `%s` guide chip" % expected_key) + continue + var focus_cell: Vector2i = chip.get_meta("focus_cell", Vector2i(-1, -1)) + if not scene.state.is_inside(focus_cell): + failures.append("battle start `%s` chip should carry a map focus cell: %s" % [expected_key, str(focus_cell)]) + if not chip.tooltip_text.contains("클릭하면"): + failures.append("battle start `%s` chip tooltip should mention map focus: %s" % [expected_key, chip.tooltip_text]) + if chip.mouse_filter == Control.MOUSE_FILTER_IGNORE: + failures.append("battle start `%s` chip should receive pointer input" % expected_key) + var objective_chip := _find_command_notice_chip_by_key(scene.objective_notice_icon_strip, "objective") + if objective_chip != null: + var objective_focus_cell: Vector2i = objective_chip.get_meta("focus_cell", Vector2i(-1, -1)) + var previous_offset: Vector2 = scene.board_scroll_offset + if not scene._focus_board_on_command_notice_chip(objective_chip): + failures.append("clicking the objective notice chip should focus the battlefield objective") + elif not scene._cell_is_in_visible_bounds(objective_focus_cell, 0): + failures.append("objective notice chip focus should bring the castle cells into view") + elif scene.board_scroll_offset.distance_squared_to(previous_offset) <= 0.01: + failures.append("objective notice chip should scroll the large opening map toward the eastern castle") + elif scene.hover_cell != objective_focus_cell: + failures.append("objective notice chip should update the hover focus cell") scene.free() @@ -2050,6 +2076,18 @@ func _collect_tooltips(root: Node) -> String: return "\n".join(parts) +func _find_command_notice_chip_by_key(root: Node, key: String) -> Control: + if root == null: + return null + if root is Control and str(root.get_meta("notice_icon_key", "")) == key: + return root as Control + for child in root.get_children(): + var found := _find_command_notice_chip_by_key(child, key) + if found != null: + return found + return null + + func _collect_tooltips_into(root: Node, parts: Array[String]) -> void: if root == null: return