diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 410c384..838b323 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -61,6 +61,6 @@ Inventory and campaign flags are copied from `CampaignState` into `BattleState` The battle scene owns presentation feedback: briefing and result states use menu BGM, briefing headers combine `CampaignState` chapter ranges, campaign order, and title data with `BattleState` briefing, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, can place the portrait on the left or right side of that panel, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures or class sprites for the selected unit or hovered unit and formats focus text with role, movement type, facing, current terrain move cost, defense/avoid bonuses, core stats, status effects, guard-zone AI hints, and gear, board maps can draw a scenario background texture under translucent terrain overlays, connected road segments, terrain detail patterns, shoreline/castle/forest/hill edge blends, compact recovery markers for healing terrain, compact side-event markers for visible item/gold pickups, and move-attack target badges for enemies that can be struck after movement, board units render class sprite tokens or generic-enemy transparent class cutouts, team rings, class badges, facing markers, generic-enemy marks, officer pins, low-HP warning rings, HP bar color states, active support/debuff/poison/seal/snare/disarm status pips, and a scene-clocked idle stance whose intensity reacts to selected, acted, and low-HP unit state, item buttons can show optional cached item icons, hover previews are rendered as target badges from existing `BattleState` forecast APIs and include compact side/rear attack bonuses when facing matters, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item/gold pickup effects use distinct feedback, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, `BattleState.unit_action_motion_requested` keeps its rules-facing signal shape while the scene maps attacker class ids to infantry, archer, cavalry, command, and heavy attack presentation profiles, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules. -Large tactical maps are presented through a fixed visible map window rather than by shrinking the whole board, including the opening chapter-one battle. `BattleScene` keeps a clamped `board_scroll_offset`, scrolls the board while the mouse is near or slightly outside the visible map edge, brightens the scrollable frame edge under the cursor, ignores board clicks outside the visible map window, and draws a small minimap plate only when the board actually exceeds the visible map view; clicking or dragging the minimap recenters the tactical viewport, while compact maps keep the lower-right tiles unobstructed. Board-attached command panels are repositioned after map scrolling so post-move choices stay beside the moved unit. Battle start focuses the board on the deployed player group, pre-battle Formation can focus the board on the selected unit even before the battle opens, and action feedback can focus the board on off-screen cells before motion or combat effects play. +Large tactical maps are presented through a fixed visible map window rather than by shrinking the whole board, including the opening chapter-one battle. `BattleScene` keeps a clamped `board_scroll_offset`, scrolls the board while the mouse is near or slightly outside the visible map edge, brightens the scrollable frame edge under the cursor, ignores board clicks outside the visible map window, and draws a small minimap plate only when the board actually exceeds the visible map view; the minimap shows terrain, unit dots, objective seals, and side-event markers, and clicking or dragging it recenters the tactical viewport, while compact maps keep the lower-right tiles unobstructed. Board-attached command panels are repositioned after map scrolling so post-move choices stay beside the moved unit. Battle start focuses the board on the deployed player group, pre-battle Formation can focus the board on the selected unit even before the battle opens, and action feedback can focus the board on off-screen cells before motion or combat effects play. The `New Campaign` button clears `user://campaign_save.json`, resets campaign state to the first scenario, and reloads the opening briefing. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 7289420..4bb06b1 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -2926,6 +2926,9 @@ func _draw_minimap() -> void: terrain_color.a = 0.76 draw_rect(_minimap_cell_rect(cell), terrain_color) + _draw_minimap_event_markers() + _draw_minimap_objective_markers() + for unit in state.get_living_units(): var cell: Vector2i = unit.get("pos", Vector2i(-1, -1)) if not state.is_inside(cell): @@ -2943,6 +2946,61 @@ 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_objective_markers() -> void: + for cell in state.get_objective_cells(): + if not state.is_inside(cell): + continue + var marker_rect := _minimap_marker_rect(cell, 1.36, 5.8, 9.0) + var center := marker_rect.get_center() + var diamond := PackedVector2Array([ + Vector2(center.x, marker_rect.position.y), + Vector2(marker_rect.end.x, center.y), + Vector2(center.x, marker_rect.end.y), + Vector2(marker_rect.position.x, center.y) + ]) + var outline := PackedVector2Array([diamond[0], diamond[1], diamond[2], diamond[3], diamond[0]]) + draw_colored_polygon(diamond, _minimap_objective_marker_fill_color()) + draw_polyline(outline, _minimap_marker_outline_color(), 1.1) + + +func _draw_minimap_event_markers() -> void: + for marker in state.get_event_markers(): + var cells = marker.get("cells", []) + if typeof(cells) != TYPE_ARRAY: + continue + var kind := str(marker.get("kind", "event")) + for cell in cells: + if typeof(cell) != TYPE_VECTOR2I or not state.is_inside(cell): + continue + var marker_rect := _minimap_marker_rect(cell, 1.05, 4.8, 7.0) + draw_rect(marker_rect, Color(0.020, 0.014, 0.010, 0.84)) + draw_rect(marker_rect.grow(-1.0), _minimap_event_marker_fill_color(kind)) + draw_rect(marker_rect, _minimap_marker_outline_color(), false, 1.0) + + +func _minimap_marker_rect(cell: Vector2i, scale: float = 1.0, min_size: float = 4.0, max_size: float = 8.0) -> Rect2: + var cell_rect := _minimap_cell_rect(cell) + var size := clampf(minf(cell_rect.size.x, cell_rect.size.y) * scale, min_size, max_size) + var map_rect := _minimap_map_rect() + var position := cell_rect.get_center() - Vector2(size * 0.5, size * 0.5) + position.x = clampf(position.x, map_rect.position.x, maxf(map_rect.position.x, map_rect.end.x - size)) + position.y = clampf(position.y, map_rect.position.y, maxf(map_rect.position.y, map_rect.end.y - size)) + return Rect2(position, Vector2(size, size)) + + +func _minimap_objective_marker_fill_color() -> Color: + return Color(UI_SEAL_RED.r, UI_SEAL_RED.g, UI_SEAL_RED.b, 0.92) + + +func _minimap_event_marker_fill_color(kind: String) -> Color: + var color := _event_marker_color(kind) + return Color(color.r, color.g, color.b, 0.90) + + +func _minimap_marker_outline_color() -> Color: + return Color(UI_PARCHMENT_TEXT.r, UI_PARCHMENT_TEXT.g, UI_PARCHMENT_TEXT.b, 0.88) + + func _current_battle_background_texture() -> Texture2D: var next_path := state.get_map_background_path() if next_path.is_empty(): diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 0449ff4..885a499 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -227,7 +227,12 @@ func _check_minimap_navigation_contract(failures: Array[String]) -> void: "_handle_minimap_mouse_button", "_handle_minimap_mouse_motion", "_is_minimap_visible", - "_map_has_scrollable_area" + "_map_has_scrollable_area", + "_minimap_marker_rect", + "_minimap_objective_marker_fill_color", + "_minimap_event_marker_fill_color", + "_draw_minimap_objective_markers", + "_draw_minimap_event_markers" ]: if not scene.has_method(method_name): failures.append("missing minimap helper: %s" % method_name) @@ -249,6 +254,30 @@ 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 objective_cells: Array[Vector2i] = scene.state.get_objective_cells() + if objective_cells.is_empty(): + failures.append("opening map should expose minimap objective markers") + else: + for objective_cell in objective_cells: + var objective_marker_rect: Rect2 = scene._minimap_marker_rect(objective_cell, 1.36, 5.8, 9.0) + if not map_rect.encloses(objective_marker_rect): + 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 has_event_marker := false + for marker in scene.state.get_event_markers(): + var cells = marker.get("cells", []) + if typeof(cells) != TYPE_ARRAY or cells.is_empty(): + continue + has_event_marker = true + for event_cell in cells: + var event_marker_rect: Rect2 = scene._minimap_marker_rect(event_cell, 1.05, 4.8, 7.0) + if not map_rect.encloses(event_marker_rect): + failures.append("minimap event marker should stay inside map body: %s within %s" % [str(event_marker_rect), str(map_rect)]) + if scene._minimap_event_marker_fill_color(str(marker.get("kind", "event"))).a < 0.85: + failures.append("minimap event marker should be visible") + if not has_event_marker: + failures.append("opening map should expose minimap side-event markers") var min_offset: Vector2 = scene._clamped_board_scroll_offset(Vector2(-9999.0, -9999.0)) var top_left_offset: Vector2 = scene._board_scroll_offset_for_minimap_position(map_rect.position)