From dee3d9813b663c91cb5898beda19395fd474e22b Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 01:14:32 +0900 Subject: [PATCH] Add minimap hover details --- README.md | 1 + scripts/scenes/battle_scene.gd | 116 +++++++++++++++++++++++++++++- tools/smoke_chapter_one_polish.gd | 25 +++++++ 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 93fc758..7f12854 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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/position badges, and hover combat details. - Enemy threat range overlay with tile-level threat source, physical damage hints, and hostile tactic hints. +- Scrollable battle maps show a clickable minimap with terrain, unit, objective, and event hover badges. - Damage forecast for selected-unit attacks. - Terrain defense and avoid affect physical attack damage and hit chance. - Min/max attack ranges and counterattack forecasts. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index a6021f8..5593f30 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -659,6 +659,7 @@ func _draw() -> void: _draw_floating_texts() _draw_map_view_chrome() _draw_minimap() + _draw_minimap_hover_badge() func _make_panel_style(fill: Color, border: Color, border_width: int, radius: int, margin: int, shadow_size: int) -> StyleBoxFlat: @@ -4539,6 +4540,41 @@ func _draw_minimap() -> void: draw_rect(map_rect, Color(UI_BRUSH_RULE.r, UI_BRUSH_RULE.g, UI_BRUSH_RULE.b, 0.90), false, 1.5) +func _draw_minimap_hover_badge() -> void: + if not _is_minimap_visible(): + return + var viewport := get_viewport() + if viewport == null: + return + var badge := _minimap_hover_badge(viewport.get_mouse_position()) + if badge.is_empty(): + return + var lines: Array[String] = badge.get("lines", []) + if lines.is_empty(): + return + var badge_rect: Rect2 = badge.get("rect", Rect2()) + if badge_rect.size.x <= 0.0 or badge_rect.size.y <= 0.0: + return + var color := _hover_info_badge_color(str(badge.get("kind", "terrain"))) + var background := Color(0.035, 0.022, 0.014, 0.93) + var font := _draw_ui_font(true) + + draw_rect(badge_rect, background) + draw_rect(badge_rect.grow(-2.0), Color(color.r, color.g, color.b, 0.12)) + draw_rect(badge_rect, color, false, 2.0) + for index in range(lines.size()): + var line_color := color if index == 0 else UI_PARCHMENT_TEXT + _draw_ink_text( + font, + badge_rect.position + Vector2(8.0, 16.0 + float(index) * 15.0), + lines[index], + HORIZONTAL_ALIGNMENT_LEFT, + badge_rect.size.x - 16.0, + 12, + line_color + ) + + func _draw_minimap_objective_markers() -> void: for cell in state.get_objective_cells(): if not state.is_inside(cell): @@ -7090,6 +7126,81 @@ func _minimap_view_rect() -> Rect2: ) +func _minimap_cell_for_screen_position(screen_position: Vector2) -> Vector2i: + if not _is_minimap_visible() or state.map_size.x <= 0 or state.map_size.y <= 0: + return Vector2i(-1, -1) + var map_rect := _minimap_map_rect() + if not map_rect.has_point(screen_position): + return Vector2i(-1, -1) + var local := screen_position - map_rect.position + var x := clampi(int(floor((local.x / map_rect.size.x) * float(state.map_size.x))), 0, state.map_size.x - 1) + var y := clampi(int(floor((local.y / map_rect.size.y) * float(state.map_size.y))), 0, state.map_size.y - 1) + return Vector2i(x, y) + + +func _minimap_hover_badge(screen_position: Vector2) -> Dictionary: + var cell := _minimap_cell_for_screen_position(screen_position) + if not state.is_inside(cell): + return {} + var lines := _minimap_hover_badge_lines(cell) + if lines.is_empty(): + return {} + return { + "cell": cell, + "kind": _hover_info_badge_kind(cell), + "lines": lines, + "rect": _minimap_hover_badge_rect(screen_position, lines) + } + + +func _minimap_hover_badge_lines(cell: Vector2i) -> Array[String]: + if not state.is_inside(cell): + return [] + var summary := state.get_cell_summary(cell) + var terrain_name := _terrain_display_name(str(summary.get("terrain", ""))) + var lines: Array[String] = [] + var unit: Dictionary = summary.get("unit", {}) + if not unit.is_empty(): + lines.append("%s · %s" % [_short_name(str(unit.get("name", "부대"))), _unit_team_text(str(unit.get("team", "")))]) + lines.append("%s · %s" % [_unit_class_display_name(unit), terrain_name]) + else: + lines.append("전장도 %s · %s" % [_format_cell_label(cell), terrain_name]) + var markers: Array[String] = [] + var objective_label := state.get_objective_cell_label(cell) + if not objective_label.is_empty(): + markers.append("목표 %s" % objective_label) + var event_label := state.get_event_marker_label(cell) + if not event_label.is_empty(): + markers.append("표식 %s" % event_label) + if show_threat_overlay: + var threat_names := state.get_threatening_unit_names(cell, BattleState.TEAM_ENEMY) + if not threat_names.is_empty(): + markers.append("감시 %s" % _compact_name_list(threat_names, 2)) + if not markers.is_empty(): + lines.append(_join_strings(markers, " · ")) + lines.append("클릭/드래그로 전장 이동") + var capped_lines: Array[String] = [] + for index in range(mini(lines.size(), 4)): + capped_lines.append(lines[index]) + return capped_lines + + +func _minimap_hover_badge_rect(screen_position: Vector2, lines: Array[String]) -> Rect2: + var size := _hover_info_badge_size(lines) + var position := screen_position + Vector2(12.0, 12.0) + var view_rect := _map_view_rect() + var minimap_rect := _minimap_rect() + var min_x := view_rect.position.x + 2.0 + var min_y := view_rect.position.y + 2.0 + var max_x := maxf(min_x, view_rect.end.x - size.x - 2.0) + var max_y := maxf(min_y, minimap_rect.position.y - size.y - 8.0) + if max_y < min_y: + max_y = maxf(min_y, view_rect.end.y - size.y - 2.0) + position.x = clampf(position.x, min_x, max_x) + position.y = clampf(position.y, min_y, max_y) + return Rect2(position, size) + + func _board_scroll_offset_for_minimap_position(screen_position: Vector2) -> Vector2: var map_rect := _minimap_map_rect() var board_size := _board_size() @@ -7312,7 +7423,10 @@ func _handle_minimap_mouse_motion(event: InputEventMouseMotion) -> bool: if minimap_dragging: _scroll_board_to_minimap_position(event.position) return true - return _minimap_rect().has_point(event.position) + if _minimap_rect().has_point(event.position): + queue_redraw() + return true + return false func _edge_scroll_velocity_for_position(mouse_position: Vector2, view_rect: Rect2) -> Vector2: diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index cf71b18..052598e 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -228,6 +228,11 @@ func _check_minimap_navigation_contract(failures: Array[String]) -> void: "_handle_minimap_mouse_motion", "_is_minimap_visible", "_map_has_scrollable_area", + "_minimap_cell_for_screen_position", + "_minimap_hover_badge", + "_minimap_hover_badge_rect", + "_minimap_hover_badge_lines", + "_draw_minimap_hover_badge", "_minimap_marker_rect", "_minimap_objective_marker_fill_color", "_minimap_event_marker_fill_color", @@ -254,6 +259,22 @@ func _check_minimap_navigation_contract(failures: Array[String]) -> void: var first_cell_rect: Rect2 = scene._minimap_cell_rect(Vector2i(0, 0)) if first_cell_rect.size.x <= 0.0 or first_cell_rect.size.y <= 0.0: failures.append("minimap cells should have visible area: %s" % str(first_cell_rect)) + var first_hover_position := first_cell_rect.get_center() + if scene._minimap_cell_for_screen_position(first_hover_position) != Vector2i(0, 0): + failures.append("minimap hover position should resolve to the matching map cell") + var first_hover_badge: Dictionary = scene._minimap_hover_badge(first_hover_position) + if first_hover_badge.is_empty(): + failures.append("minimap hover should expose a compact battlefield badge") + else: + var first_hover_lines: Array = first_hover_badge.get("lines", []) + var first_hover_text := "\n".join(first_hover_lines) + if not first_hover_text.contains("클릭/드래그"): + failures.append("minimap hover should explain click-drag navigation: %s" % first_hover_text) + var first_hover_rect: Rect2 = first_hover_badge.get("rect", Rect2()) + if first_hover_rect.size.x <= 0.0 or first_hover_rect.size.y <= 0.0 or not view_rect.encloses(first_hover_rect): + failures.append("minimap hover badge should stay inside the visible map view: %s within %s" % [str(first_hover_rect), str(view_rect)]) + if not scene._minimap_hover_badge(map_rect.position - Vector2(4.0, 4.0)).is_empty(): + failures.append("minimap hover should stay hidden outside the map body") var objective_cells: Array[Vector2i] = scene.state.get_objective_cells() if objective_cells.is_empty(): failures.append("opening map should expose minimap objective markers") @@ -264,6 +285,10 @@ func _check_minimap_navigation_contract(failures: Array[String]) -> void: failures.append("minimap objective marker should stay inside map body: %s within %s" % [str(objective_marker_rect), str(map_rect)]) if scene._minimap_objective_marker_fill_color().a < 0.85: failures.append("minimap objective marker should be prominent") + var objective_hover: Dictionary = scene._minimap_hover_badge(scene._minimap_cell_rect(objective_cells[0]).get_center()) + var objective_hover_text := "\n".join(Array(objective_hover.get("lines", []))) + if objective_hover.is_empty() or not objective_hover_text.contains("목표"): + failures.append("minimap objective hover should identify objective cells: %s" % objective_hover_text) var has_event_marker := false for marker in scene.state.get_event_markers(): var cells = marker.get("cells", [])