diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index dd567d4..86d1695 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -51,6 +51,10 @@ const EDGE_SCROLL_MARGIN := 48.0 const EDGE_SCROLL_OUTER_MARGIN := 28.0 const EDGE_SCROLL_MIN_SPEED := 80.0 const EDGE_SCROLL_MAX_SPEED := 430.0 +const MINIMAP_SIZE := Vector2(178, 134) +const MINIMAP_PADDING := 8.0 +const MINIMAP_TITLE_HEIGHT := 18.0 +const MINIMAP_MARGIN := Vector2(18, 18) const UNIT_MOVE_ANIMATION_DURATION := 0.18 const TARGET_PREVIEW_BADGE_SIZE := Vector2(104, 22) const LOW_HP_WARNING_RATIO := 0.35 @@ -431,6 +435,7 @@ var battle_background_texture: Texture2D var ui_regular_font: Font var ui_bold_font: Font var board_scroll_offset := Vector2.ZERO +var minimap_dragging := false func _ready() -> void: @@ -467,14 +472,22 @@ func _unhandled_input(event: InputEvent) -> void: return if event is InputEventMouseMotion: + if _handle_minimap_mouse_motion(event): + return var next_hover := _cell_from_screen(event.position) if next_hover != hover_cell: hover_cell = next_hover _update_hud() queue_redraw() elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed: + if _handle_minimap_mouse_button(event): + return _handle_board_click(event.position) + elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: + if _handle_minimap_mouse_button(event): + return elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: + minimap_dragging = false _handle_cancel_input() elif event is InputEventKey and event.pressed and not event.echo: _handle_key(event) @@ -489,6 +502,8 @@ func _draw() -> void: _draw_attack_effects() _draw_target_preview_badge() _draw_floating_texts() + _draw_map_view_chrome() + _draw_minimap() func _make_panel_style(fill: Color, border: Color, border_width: int, radius: int, margin: int, shadow_size: int) -> StyleBoxFlat: @@ -583,17 +598,29 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void: border_width = 5 margin = 3 "dialogue": - fill = Color(0.070, 0.024, 0.012, 0.986) - border = Color(0.37, 0.20, 0.070, 1.0) - border_width = 13 - margin = 22 - shadow_size = 22 + fill = Color(0.055, 0.028, 0.016, 0.982) + border = Color(0.30, 0.18, 0.075, 1.0) + border_width = 11 + margin = 20 + shadow_size = 18 "speech_scroll": - fill = Color(0.72, 0.53, 0.25, 0.998) - border = UI_BRUSH_RULE - border_width = 9 - margin = 17 - shadow_size = 9 + fill = Color(0.61, 0.47, 0.26, 0.996) + border = Color(0.17, 0.085, 0.034, 0.98) + border_width = 7 + margin = 15 + shadow_size = 6 + "hud_info": + fill = Color(0.075, 0.050, 0.034, 0.88) + border = Color(0.31, 0.22, 0.12, 0.96) + border_width = 3 + margin = 7 + shadow_size = 3 + "target_hint": + fill = Color(0.11, 0.070, 0.040, 0.94) + border = UI_TARNISHED_BRONZE + border_width = 4 + margin = 9 + shadow_size = 6 "notice": fill = Color(0.070, 0.020, 0.014, 0.982) border = UI_SEAL_RED @@ -1357,17 +1384,31 @@ func _create_hud() -> void: mission_detail_row.add_child(_make_edict_marker_stack(["", "", ""], 112)) mission_detail_row.add_child(_make_bamboo_gutter(12, 112)) + var cell_info_panel := PanelContainer.new() + cell_info_panel.custom_minimum_size = Vector2(420, 82) + cell_info_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_panel_style(cell_info_panel, "hud_info") + side_column.add_child(cell_info_panel) + cell_info_label = Label.new() cell_info_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - cell_info_label.custom_minimum_size = Vector2(420, 78) - _apply_label_style(cell_info_label, UI_PARCHMENT_TEXT) - side_column.add_child(cell_info_label) + cell_info_label.custom_minimum_size = Vector2(396, 62) + cell_info_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_label_style(cell_info_label, Color(0.92, 0.78, 0.54, 1.0), 13) + cell_info_panel.add_child(cell_info_label) + + var forecast_panel := PanelContainer.new() + forecast_panel.custom_minimum_size = Vector2(420, 66) + forecast_panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_panel_style(forecast_panel, "hud_info") + side_column.add_child(forecast_panel) forecast_label = Label.new() forecast_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - forecast_label.custom_minimum_size = Vector2(420, 60) - _apply_label_style(forecast_label, UI_PARCHMENT_TEXT) - side_column.add_child(forecast_label) + forecast_label.custom_minimum_size = Vector2(396, 46) + forecast_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _apply_label_style(forecast_label, Color(0.95, 0.80, 0.56, 1.0), 13) + forecast_panel.add_child(forecast_label) inventory_label = Label.new() inventory_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART @@ -1575,7 +1616,7 @@ func _create_hud() -> void: targeting_hint_panel.mouse_filter = Control.MOUSE_FILTER_STOP targeting_hint_panel.custom_minimum_size = TARGETING_HINT_PANEL_SIZE targeting_hint_panel.size = TARGETING_HINT_PANEL_SIZE - _apply_panel_style(targeting_hint_panel, "command_seal") + _apply_panel_style(targeting_hint_panel, "target_hint") root.add_child(targeting_hint_panel) var targeting_column := VBoxContainer.new() @@ -2012,16 +2053,14 @@ func _create_hud() -> void: dialogue_column = VBoxContainer.new() dialogue_column.custom_minimum_size = DIALOGUE_COLUMN_SIZE dialogue_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL - dialogue_column.add_theme_constant_override("separation", 4) + dialogue_column.add_theme_constant_override("separation", 6) dialogue_row.add_child(dialogue_column) dialogue_right_tassel = _make_hanging_tassel(DIALOGUE_PORTRAIT_SIZE.y) dialogue_row.add_child(dialogue_right_tassel) - dialogue_column.add_child(_make_tablet_binding(DIALOGUE_TEXT_SIZE.x, 13)) - dialogue_column.add_child(_make_edict_title_strip(["", "", ""], DIALOGUE_TEXT_SIZE.x, 22)) - dialogue_seal_ribbon = _make_seal_ribbon(DIALOGUE_TEXT_SIZE.x, 18) + dialogue_seal_ribbon.modulate = Color(1.0, 1.0, 1.0, 0.62) dialogue_column.add_child(dialogue_seal_ribbon) dialogue_speaker_panel = PanelContainer.new() @@ -2032,15 +2071,12 @@ func _create_hud() -> void: var dialogue_speaker_row := HBoxContainer.new() dialogue_speaker_row.add_theme_constant_override("separation", 8) dialogue_speaker_panel.add_child(dialogue_speaker_row) - dialogue_speaker_row.add_child(_make_seal_tile("", 24)) - dialogue_speaker_label = Label.new() - dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 88, 24) + dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 24, 24) dialogue_speaker_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL dialogue_speaker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _apply_label_style(dialogue_speaker_label, UI_PARCHMENT_TEXT, DIALOGUE_SPEAKER_FONT_SIZE) dialogue_speaker_row.add_child(dialogue_speaker_label) - dialogue_speaker_row.add_child(_make_seal_tile("", 24)) dialogue_text_panel = PanelContainer.new() dialogue_text_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, DIALOGUE_TEXT_SIZE.y) @@ -2048,24 +2084,23 @@ func _create_hud() -> void: dialogue_column.add_child(dialogue_text_panel) var dialogue_text_row := HBoxContainer.new() - dialogue_text_row.add_theme_constant_override("separation", 8) + dialogue_text_row.add_theme_constant_override("separation", 6) dialogue_text_panel.add_child(dialogue_text_row) - dialogue_text_row.add_child(_make_bamboo_gutter(18, DIALOGUE_TEXT_SIZE.y - 20)) - dialogue_text_row.add_child(_make_edict_marker_stack(["", "", ""], DIALOGUE_TEXT_SIZE.y - 20)) + dialogue_text_row.add_child(_make_bamboo_gutter(10, DIALOGUE_TEXT_SIZE.y - 20)) dialogue_text_label = Label.new() dialogue_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - dialogue_text_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 164, DIALOGUE_TEXT_SIZE.y - 20) + dialogue_text_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 44, DIALOGUE_TEXT_SIZE.y - 20) dialogue_text_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL dialogue_text_label.size_flags_vertical = Control.SIZE_EXPAND_FILL _apply_label_style(dialogue_text_label, UI_AGED_INK, DIALOGUE_TEXT_FONT_SIZE) dialogue_text_row.add_child(dialogue_text_label) - dialogue_text_row.add_child(_make_edict_marker_stack(["", "", ""], DIALOGUE_TEXT_SIZE.y - 20)) - dialogue_text_row.add_child(_make_bamboo_gutter(18, DIALOGUE_TEXT_SIZE.y - 20)) + dialogue_text_row.add_child(_make_bamboo_gutter(10, DIALOGUE_TEXT_SIZE.y - 20)) - dialogue_column.add_child(_make_scroll_rod(DIALOGUE_TEXT_SIZE.x, 3)) - dialogue_column.add_child(_make_ink_wash_rule(DIALOGUE_TEXT_SIZE.x, 6)) + var dialogue_bottom_rule := _make_ink_wash_rule(DIALOGUE_TEXT_SIZE.x, 5) + dialogue_bottom_rule.modulate = Color(1.0, 1.0, 1.0, 0.62) + dialogue_column.add_child(dialogue_bottom_rule) var dialogue_control_row := HBoxContainer.new() dialogue_control_row.add_theme_constant_override("separation", 8) @@ -2649,17 +2684,23 @@ func _can_select(unit: Dictionary) -> bool: func _draw_map() -> void: var board_rect := _board_rect() + var view_rect := _map_view_rect() + var visible_rect := board_rect.intersection(view_rect) var background := _current_battle_background_texture() var has_background := background != null + draw_rect(view_rect, Color(0.025, 0.020, 0.014, 0.96)) + if visible_rect.size.x <= 0.0 or visible_rect.size.y <= 0.0: + return if background != null: - _draw_texture_cover(background, board_rect, Color(1.0, 1.0, 1.0, 0.96)) - draw_rect(board_rect, Color(0.02, 0.025, 0.03, 0.12)) - draw_rect(board_rect, Color(0.35, 0.22, 0.10, 0.10)) + _draw_texture_cover_region(background, visible_rect, board_rect, Color(1.0, 1.0, 1.0, 0.96)) + draw_rect(visible_rect, Color(0.02, 0.025, 0.03, 0.12)) + draw_rect(visible_rect, Color(0.35, 0.22, 0.10, 0.10)) else: - draw_rect(board_rect, Color(0.18, 0.26, 0.18)) + draw_rect(visible_rect, Color(0.18, 0.26, 0.18)) - for y in range(state.map_size.y): - for x in range(state.map_size.x): + var bounds := _visible_cell_bounds() + for y in range(bounds.position.y, bounds.position.y + bounds.size.y): + for x in range(bounds.position.x, bounds.position.x + bounds.size.x): var cell := Vector2i(x, y) var rect := _rect_for_cell(cell) var terrain_key := state.get_terrain_key(cell) @@ -2668,8 +2709,7 @@ func _draw_map() -> void: _draw_terrain_edge_blend(cell, rect, terrain_key) draw_rect(rect, _map_grid_color(has_background), false, 1.0) - _draw_silk_map_patina(board_rect, has_background) - _draw_silk_map_frame(board_rect) + _draw_silk_map_patina(visible_rect, has_background) func _draw_silk_map_patina(board_rect: Rect2, has_background: bool) -> void: @@ -2737,6 +2777,79 @@ func _draw_silk_map_frame(board_rect: Rect2) -> void: draw_line(seal_rect.position + Vector2(seal_rect.size.x * 0.5, 8), seal_rect.end - Vector2(seal_rect.size.x * 0.5, 8), Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.34), 1.2) +func _draw_map_view_chrome() -> void: + var view_rect := _map_view_rect() + var viewport_size := _viewport_size_for_layout() + var mask := Color(0.018, 0.012, 0.008, 0.94) + draw_rect(Rect2(Vector2.ZERO, Vector2(viewport_size.x, view_rect.position.y)), mask) + draw_rect(Rect2(Vector2.ZERO, Vector2(view_rect.position.x, viewport_size.y)), mask) + draw_rect(Rect2(Vector2(view_rect.end.x, 0.0), Vector2(maxf(0.0, viewport_size.x - view_rect.end.x), viewport_size.y)), mask) + draw_rect(Rect2(Vector2(view_rect.position.x, view_rect.end.y), Vector2(view_rect.size.x, maxf(0.0, viewport_size.y - view_rect.end.y))), mask) + _draw_edge_scroll_cues(view_rect) + _draw_silk_map_frame(view_rect) + + +func _draw_edge_scroll_cues(view_rect: Rect2) -> void: + if not _is_minimap_visible(): + return + var min_offset := _clamped_board_scroll_offset(Vector2(-9999.0, -9999.0)) + var cue_color := Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.18) + var hot_color := Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.36) + var strip := 14.0 + if board_scroll_offset.x < -0.5: + draw_rect(Rect2(view_rect.position, Vector2(strip, view_rect.size.y)), cue_color) + draw_line(view_rect.position + Vector2(5.0, view_rect.size.y * 0.5), view_rect.position + Vector2(11.0, view_rect.size.y * 0.5 - 8.0), hot_color, 2.0) + draw_line(view_rect.position + Vector2(5.0, view_rect.size.y * 0.5), view_rect.position + Vector2(11.0, view_rect.size.y * 0.5 + 8.0), hot_color, 2.0) + if board_scroll_offset.x > min_offset.x + 0.5: + draw_rect(Rect2(Vector2(view_rect.end.x - strip, view_rect.position.y), Vector2(strip, view_rect.size.y)), cue_color) + draw_line(Vector2(view_rect.end.x - 5.0, view_rect.position.y + view_rect.size.y * 0.5), Vector2(view_rect.end.x - 11.0, view_rect.position.y + view_rect.size.y * 0.5 - 8.0), hot_color, 2.0) + draw_line(Vector2(view_rect.end.x - 5.0, view_rect.position.y + view_rect.size.y * 0.5), Vector2(view_rect.end.x - 11.0, view_rect.position.y + view_rect.size.y * 0.5 + 8.0), hot_color, 2.0) + if board_scroll_offset.y < -0.5: + draw_rect(Rect2(view_rect.position, Vector2(view_rect.size.x, strip)), cue_color) + draw_line(view_rect.position + Vector2(view_rect.size.x * 0.5, 5.0), view_rect.position + Vector2(view_rect.size.x * 0.5 - 8.0, 11.0), hot_color, 2.0) + draw_line(view_rect.position + Vector2(view_rect.size.x * 0.5, 5.0), view_rect.position + Vector2(view_rect.size.x * 0.5 + 8.0, 11.0), hot_color, 2.0) + if board_scroll_offset.y > min_offset.y + 0.5: + draw_rect(Rect2(Vector2(view_rect.position.x, view_rect.end.y - strip), Vector2(view_rect.size.x, strip)), cue_color) + draw_line(Vector2(view_rect.position.x + view_rect.size.x * 0.5, view_rect.end.y - 5.0), Vector2(view_rect.position.x + view_rect.size.x * 0.5 - 8.0, view_rect.end.y - 11.0), hot_color, 2.0) + draw_line(Vector2(view_rect.position.x + view_rect.size.x * 0.5, view_rect.end.y - 5.0), Vector2(view_rect.position.x + view_rect.size.x * 0.5 + 8.0, view_rect.end.y - 11.0), hot_color, 2.0) + + +func _draw_minimap() -> void: + if not _is_minimap_visible(): + return + var rect := _minimap_rect() + var map_rect := _minimap_map_rect() + draw_rect(rect, Color(0.040, 0.024, 0.014, 0.94)) + draw_rect(rect, Color(UI_TARNISHED_BRONZE.r, UI_TARNISHED_BRONZE.g, UI_TARNISHED_BRONZE.b, 0.92), false, 3.0) + draw_rect(Rect2(rect.position + Vector2(4.0, 4.0), Vector2(rect.size.x - 8.0, MINIMAP_TITLE_HEIGHT + 2.0)), Color(0.12, 0.055, 0.026, 0.72)) + var font := _draw_ui_font(true) + _draw_ink_text(font, rect.position + Vector2(0.0, 19.0), "전장도", HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 13, UI_PARCHMENT_TEXT) + draw_rect(map_rect, Color(0.075, 0.070, 0.050, 0.94)) + + for y in range(state.map_size.y): + for x in range(state.map_size.x): + var cell := Vector2i(x, y) + var terrain_color: Color = state.get_terrain_color(cell) + terrain_color.a = 0.76 + draw_rect(_minimap_cell_rect(cell), terrain_color) + + for unit in state.get_living_units(): + var cell: Vector2i = unit.get("pos", Vector2i(-1, -1)) + if not state.is_inside(cell): + continue + var dot_color := PLAYER_COLOR.lightened(0.36) if unit.get("team", "") == BattleState.TEAM_PLAYER else ENEMY_COLOR.lightened(0.30) + var cell_rect := _minimap_cell_rect(cell) + var radius := maxf(2.4, minf(cell_rect.size.x, cell_rect.size.y) * 0.70) + draw_circle(cell_rect.get_center(), radius + 1.1, Color(0.020, 0.014, 0.010, 0.82)) + draw_circle(cell_rect.get_center(), radius, dot_color) + + var current_view := _minimap_view_rect() + if current_view.size.x > 0.0 and current_view.size.y > 0.0: + draw_rect(current_view, Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.22)) + draw_rect(current_view, Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.98), false, 2.0) + draw_rect(map_rect, Color(UI_BRUSH_RULE.r, UI_BRUSH_RULE.g, UI_BRUSH_RULE.b, 0.90), false, 1.5) + + func _current_battle_background_texture() -> Texture2D: var next_path := state.get_map_background_path() if next_path.is_empty(): @@ -2749,9 +2862,23 @@ func _current_battle_background_texture() -> Texture2D: func _draw_texture_cover(texture: Texture2D, rect: Rect2, modulate: Color) -> void: + var source_rect := _texture_cover_source_rect(texture, rect) + if source_rect.size.x <= 0.0 or source_rect.size.y <= 0.0: + return + draw_texture_rect_region(texture, rect, source_rect, modulate) + + +func _draw_texture_cover_region(texture: Texture2D, visible_rect: Rect2, full_rect: Rect2, modulate: Color) -> void: + var source_rect := _texture_cover_region_source_rect(texture, visible_rect, full_rect) + if source_rect.size.x <= 0.0 or source_rect.size.y <= 0.0: + return + draw_texture_rect_region(texture, visible_rect, source_rect, modulate) + + +func _texture_cover_source_rect(texture: Texture2D, rect: Rect2) -> Rect2: var texture_size := texture.get_size() if texture_size.x <= 0.0 or texture_size.y <= 0.0 or rect.size.x <= 0.0 or rect.size.y <= 0.0: - return + return Rect2() var target_ratio := rect.size.x / rect.size.y var source_ratio := texture_size.x / texture_size.y var source_rect := Rect2(Vector2.ZERO, texture_size) @@ -2763,7 +2890,24 @@ func _draw_texture_cover(texture: Texture2D, rect: Rect2, modulate: Color) -> vo var source_height := texture_size.x / target_ratio source_rect.position.y = (texture_size.y - source_height) * 0.5 source_rect.size.y = source_height - draw_texture_rect_region(texture, rect, source_rect, modulate) + return source_rect + + +func _texture_cover_region_source_rect(texture: Texture2D, visible_rect: Rect2, full_rect: Rect2) -> Rect2: + if full_rect.size.x <= 0.0 or full_rect.size.y <= 0.0 or visible_rect.size.x <= 0.0 or visible_rect.size.y <= 0.0: + return Rect2() + var full_source := _texture_cover_source_rect(texture, full_rect) + if full_source.size.x <= 0.0 or full_source.size.y <= 0.0: + return Rect2() + var relative_position := Vector2( + (visible_rect.position.x - full_rect.position.x) / full_rect.size.x, + (visible_rect.position.y - full_rect.position.y) / full_rect.size.y + ) + var relative_size := Vector2(visible_rect.size.x / full_rect.size.x, visible_rect.size.y / full_rect.size.y) + return Rect2( + full_source.position + Vector2(full_source.size.x * relative_position.x, full_source.size.y * relative_position.y), + Vector2(full_source.size.x * relative_size.x, full_source.size.y * relative_size.y) + ) func _terrain_fill_color(cell: Vector2i, terrain_key: String, has_background: bool) -> Color: @@ -4523,6 +4667,36 @@ func _map_view_rect() -> Rect2: ) +func _visible_board_local_rect() -> Rect2: + var visible_rect := _board_rect().intersection(_map_view_rect()) + if visible_rect.size.x <= 0.0 or visible_rect.size.y <= 0.0: + return Rect2() + return Rect2(visible_rect.position - _board_origin(), visible_rect.size) + + +func _visible_cell_bounds(extra_margin: int = 1) -> Rect2i: + if state.map_size.x <= 0 or state.map_size.y <= 0: + return Rect2i() + var local_rect := _visible_board_local_rect() + if local_rect.size.x <= 0.0 or local_rect.size.y <= 0.0: + return Rect2i() + var start_x := clampi(int(floor(local_rect.position.x / TILE_SIZE)) - extra_margin, 0, state.map_size.x) + var start_y := clampi(int(floor(local_rect.position.y / TILE_SIZE)) - extra_margin, 0, state.map_size.y) + var end_x := clampi(int(ceil((local_rect.position.x + local_rect.size.x) / TILE_SIZE)) + extra_margin, 0, state.map_size.x) + var end_y := clampi(int(ceil((local_rect.position.y + local_rect.size.y) / TILE_SIZE)) + extra_margin, 0, state.map_size.y) + return Rect2i(Vector2i(start_x, start_y), Vector2i(maxi(0, end_x - start_x), maxi(0, end_y - start_y))) + + +func _cell_is_in_visible_bounds(cell: Vector2i, extra_margin: int = 1) -> bool: + var bounds := _visible_cell_bounds(extra_margin) + return ( + cell.x >= bounds.position.x + and cell.y >= bounds.position.y + and cell.x < bounds.position.x + bounds.size.x + and cell.y < bounds.position.y + bounds.size.y + ) + + func _viewport_size_for_layout() -> Vector2: if is_inside_tree(): return get_viewport_rect().size @@ -4544,6 +4718,115 @@ func _clamped_board_scroll_offset(offset: Vector2) -> Vector2: return Vector2(clampf(offset.x, min_x, 0.0), clampf(offset.y, min_y, 0.0)) +func _minimap_rect() -> Rect2: + var view_rect := _map_view_rect() + var position := view_rect.end - MINIMAP_SIZE - MINIMAP_MARGIN + position.x = maxf(view_rect.position.x + MINIMAP_MARGIN.x, position.x) + position.y = maxf(view_rect.position.y + MINIMAP_MARGIN.y, position.y) + return Rect2(position, MINIMAP_SIZE) + + +func _minimap_map_rect() -> Rect2: + var rect := _minimap_rect() + return Rect2( + rect.position + Vector2(MINIMAP_PADDING, MINIMAP_PADDING + MINIMAP_TITLE_HEIGHT), + Vector2( + maxf(1.0, rect.size.x - MINIMAP_PADDING * 2.0), + maxf(1.0, rect.size.y - MINIMAP_PADDING * 2.0 - MINIMAP_TITLE_HEIGHT) + ) + ) + + +func _minimap_cell_rect(cell: Vector2i) -> Rect2: + var map_rect := _minimap_map_rect() + if state.map_size.x <= 0 or state.map_size.y <= 0: + return Rect2(map_rect.position, Vector2.ZERO) + var cell_size := Vector2(map_rect.size.x / float(state.map_size.x), map_rect.size.y / float(state.map_size.y)) + return Rect2(map_rect.position + Vector2(cell.x * cell_size.x, cell.y * cell_size.y), cell_size) + + +func _minimap_view_rect() -> Rect2: + var board_size := _board_size() + if board_size.x <= 0.0 or board_size.y <= 0.0: + return Rect2() + var local_rect := _visible_board_local_rect() + if local_rect.size.x <= 0.0 or local_rect.size.y <= 0.0: + return Rect2() + var map_rect := _minimap_map_rect() + return Rect2( + map_rect.position + Vector2( + (local_rect.position.x / board_size.x) * map_rect.size.x, + (local_rect.position.y / board_size.y) * map_rect.size.y + ), + Vector2( + (local_rect.size.x / board_size.x) * map_rect.size.x, + (local_rect.size.y / board_size.y) * map_rect.size.y + ) + ) + + +func _board_scroll_offset_for_minimap_position(screen_position: Vector2) -> Vector2: + var map_rect := _minimap_map_rect() + var board_size := _board_size() + if board_size.x <= 0.0 or board_size.y <= 0.0: + return _clamped_board_scroll_offset(board_scroll_offset) + var local_fraction := Vector2( + clampf((screen_position.x - map_rect.position.x) / map_rect.size.x, 0.0, 1.0), + clampf((screen_position.y - map_rect.position.y) / map_rect.size.y, 0.0, 1.0) + ) + var target_local := Vector2(board_size.x * local_fraction.x, board_size.y * local_fraction.y) + var view_rect := _map_view_rect() + var desired_offset := view_rect.get_center() - BOARD_OFFSET - target_local + return _clamped_board_scroll_offset(desired_offset) + + +func _scroll_board_to_minimap_position(screen_position: Vector2) -> bool: + var previous_offset := board_scroll_offset + board_scroll_offset = _board_scroll_offset_for_minimap_position(screen_position) + var next_hover := _cell_from_screen(_map_view_rect().get_center()) + if next_hover != hover_cell: + hover_cell = next_hover + if status_label != null: + _update_hud() + queue_redraw() + return previous_offset.distance_squared_to(board_scroll_offset) > 0.01 + + +func _is_minimap_visible() -> bool: + return battle_started and state.map_size.x > 0 and state.map_size.y > 0 and state.battle_status == BattleState.STATUS_ACTIVE + + +func _handle_minimap_mouse_button(event: InputEventMouseButton) -> bool: + if not _is_minimap_visible(): + minimap_dragging = false + return false + if event.button_index != MOUSE_BUTTON_LEFT: + return false + var inside_minimap := _minimap_rect().has_point(event.position) + var inside_map_body := _minimap_map_rect().has_point(event.position) + if event.pressed: + if not inside_minimap: + return false + if inside_map_body: + minimap_dragging = true + _scroll_board_to_minimap_position(event.position) + return true + if minimap_dragging: + minimap_dragging = false + return true + return inside_minimap + + +func _handle_minimap_mouse_motion(event: InputEventMouseMotion) -> bool: + if not _is_minimap_visible(): + minimap_dragging = false + return false + if minimap_dragging: + _scroll_board_to_minimap_position(event.position) + return true + return _minimap_rect().has_point(event.position) + + func _edge_scroll_velocity_for_position(mouse_position: Vector2, view_rect: Rect2) -> Vector2: if not _edge_scroll_activation_rect(view_rect).has_point(mouse_position): return Vector2.ZERO @@ -4595,6 +4878,8 @@ func _update_edge_scroll(delta: float) -> bool: return false var view_rect := _map_view_rect() var mouse_position := get_viewport().get_mouse_position() + if minimap_dragging or (_is_minimap_visible() and _minimap_rect().has_point(mouse_position)): + return false var velocity := _edge_scroll_velocity_for_position(mouse_position, view_rect) if velocity == Vector2.ZERO: return false @@ -8888,13 +9173,7 @@ func _is_targeting_mode() -> bool: func _show_targeting_hint_panel() -> void: if targeting_hint_panel == null: return - if basic_attack_targeting: - _hide_targeting_hint_panel() - return - _update_targeting_hint_panel() - if targeting_hint_panel != null and _is_targeting_mode(): - targeting_hint_panel.visible = true - targeting_hint_panel.move_to_front() + _hide_targeting_hint_panel() func _hide_targeting_hint_panel() -> void: @@ -8906,38 +9185,7 @@ func _hide_targeting_hint_panel() -> void: func _update_targeting_hint_panel() -> void: if targeting_hint_panel == null: return - if basic_attack_targeting or not _is_targeting_mode(): - _hide_targeting_hint_panel() - return - var selected := state.get_selected_unit() - if selected.is_empty() or bool(selected.get("acted", false)) or _is_input_locked(): - _hide_targeting_hint_panel() - return - _position_targeting_hint_panel(selected) - if targeting_hint_title_label != null: - _set_fitted_label_text( - targeting_hint_title_label, - _targeting_hint_title_text(), - LOCAL_COMMAND_TITLE_FONT_SIZE, - LOCAL_COMMAND_TITLE_MIN_FONT_SIZE, - TARGETING_HINT_TITLE_SIZE - ) - if targeting_hint_detail_label != null: - _set_fitted_label_text( - targeting_hint_detail_label, - _targeting_hint_detail_text(), - LOCAL_COMMAND_DETAIL_FONT_SIZE, - LOCAL_COMMAND_DETAIL_MIN_FONT_SIZE, - TARGETING_HINT_DETAIL_SIZE - ) - if targeting_hint_back_button != null: - targeting_hint_back_button.text = "죽간 거두기" - targeting_hint_back_button.tooltip_text = "군령 선택으로 되돌립니다." if _has_pending_move() else "지목을 거둡니다." - if targeting_hint_cancel_move_button != null: - targeting_hint_cancel_move_button.visible = _has_pending_move() - targeting_hint_cancel_move_button.disabled = not _has_pending_move() - targeting_hint_cancel_move_button.tooltip_text = "발걸음을 거두면 원래 자리로 되돌립니다." - targeting_hint_panel.visible = true + _hide_targeting_hint_panel() func _position_targeting_hint_panel(selected: Dictionary) -> void: diff --git a/tools/smoke_chapter_one_polish.gd b/tools/smoke_chapter_one_polish.gd index 37924d0..bd58d08 100644 --- a/tools/smoke_chapter_one_polish.gd +++ b/tools/smoke_chapter_one_polish.gd @@ -20,6 +20,7 @@ func _init() -> void: var failures: Array[String] = [] _check_chapter_one_scenarios_have_visual_grounding(failures) _check_edge_scroll_layout_contract(failures) + _check_minimap_navigation_contract(failures) _check_readability_contract(failures) _check_dialogue_localization_and_side(failures) _check_event_dialogue_side_passthrough(failures) @@ -175,6 +176,69 @@ func _check_edge_scroll_velocity_curve(scene, view_rect: Rect2, failures: Array[ failures.append("edge scroll next offset should not exceed the far clamp: %s below %s" % [str(clamped_at_far_edge), str(min_offset)]) +func _check_minimap_navigation_contract(failures: Array[String]) -> void: + var scene = BattleSceneScript.new() + if not scene.state.load_battle("res://data/scenarios/009_xiapi_siege.json"): + failures.append("could not load large chapter one map for minimap navigation") + scene.free() + return + scene.battle_started = true + + for method_name in [ + "_minimap_rect", + "_minimap_map_rect", + "_minimap_cell_rect", + "_minimap_view_rect", + "_board_scroll_offset_for_minimap_position", + "_scroll_board_to_minimap_position" + ]: + if not scene.has_method(method_name): + failures.append("missing minimap helper: %s" % method_name) + + var view_rect := scene._map_view_rect() + var minimap_rect: Rect2 = scene._minimap_rect() + if minimap_rect.size.x < 150.0 or minimap_rect.size.y < 110.0: + failures.append("minimap should reserve a usable tactical map plate: %s" % str(minimap_rect)) + if minimap_rect.position.x < view_rect.position.x or minimap_rect.position.y < view_rect.position.y or minimap_rect.end.x > view_rect.end.x or minimap_rect.end.y > view_rect.end.y: + failures.append("minimap should stay inside the visible map view: %s within %s" % [str(minimap_rect), str(view_rect)]) + + var map_rect: Rect2 = scene._minimap_map_rect() + if map_rect.size.x >= minimap_rect.size.x or map_rect.size.y >= minimap_rect.size.y: + failures.append("minimap map body should leave room for its frame/title: %s vs %s" % [str(map_rect), str(minimap_rect)]) + 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 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) + if top_left_offset.distance_squared_to(Vector2.ZERO) > 0.01: + failures.append("minimap top-left click should scroll to map origin: %s" % str(top_left_offset)) + var bottom_right_offset: Vector2 = scene._board_scroll_offset_for_minimap_position(map_rect.end) + if bottom_right_offset.distance_squared_to(min_offset) > 0.01: + failures.append("minimap bottom-right click should scroll to far clamp: %s vs %s" % [str(bottom_right_offset), str(min_offset)]) + + var center_offset: Vector2 = scene._board_scroll_offset_for_minimap_position(map_rect.get_center()) + var board_size: Vector2 = scene._board_size() + var center_local: Vector2 = view_rect.get_center() - (scene.BOARD_OFFSET + center_offset) + if board_size.x > view_rect.size.x and absf(center_local.x - board_size.x * 0.5) > 1.0: + failures.append("minimap center click should center the board midpoint on scrollable X: %s vs %.2f" % [str(center_local), board_size.x * 0.5]) + if board_size.y > view_rect.size.y and absf(center_local.y - board_size.y * 0.5) > 1.0: + failures.append("minimap center click should center the board midpoint on scrollable Y: %s vs %.2f" % [str(center_local), board_size.y * 0.5]) + if board_size.y <= view_rect.size.y and absf(center_offset.y) > 0.01: + failures.append("minimap center click should keep non-scrollable Y at origin: %s" % str(center_offset)) + + scene.board_scroll_offset = center_offset + var minimap_view: Rect2 = scene._minimap_view_rect() + if minimap_view.position.x < map_rect.position.x - 0.01 or minimap_view.position.y < map_rect.position.y - 0.01 or minimap_view.end.x > map_rect.end.x + 0.01 or minimap_view.end.y > map_rect.end.y + 0.01: + failures.append("minimap current-view frame should stay inside the map body: %s within %s" % [str(minimap_view), str(map_rect)]) + + scene.board_scroll_offset = Vector2.ZERO + scene._scroll_board_to_minimap_position(map_rect.get_center()) + if scene.board_scroll_offset.distance_squared_to(center_offset) > 0.01: + failures.append("minimap scroll action should apply the calculated center offset: %s vs %s" % [str(scene.board_scroll_offset), str(center_offset)]) + scene.free() + + func _check_readability_contract(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() @@ -205,7 +269,9 @@ func _check_text_fit_contract(scene, failures: Array[String]) -> void: if not scene.has_method(method_name): failures.append("missing text fit helper: %s" % method_name) - var dialogue_area := Vector2(666.0, 144.0) + var dialogue_area: Vector2 = scene.dialogue_text_label.custom_minimum_size + if dialogue_area.x <= 0.0 or dialogue_area.y <= 0.0: + dialogue_area = Vector2(666.0, 144.0) var short_dialogue_size := int(scene._fit_font_size_for_text("군령을 전하라.", 17, 14, dialogue_area)) if short_dialogue_size != 17: failures.append("short dialogue text should keep the base font size, got %d" % short_dialogue_size) @@ -216,10 +282,8 @@ func _check_text_fit_contract(scene, failures: Array[String]) -> void: + "적이 다시 숲을 흔들어도 군령은 하나다. 길목을 열고 남은 병력을 정리한 뒤 추격로를 확보하라. " \ + "북쪽 비탈과 남쪽 수풀을 동시에 살피고, 어느 한 장수가 고립되지 않도록 서로의 이동 거리를 맞추어라.」" var fitted_dialogue_size := int(scene._fit_font_size_for_text(long_dialogue, 17, 14, dialogue_area)) - if fitted_dialogue_size >= 17: - failures.append("long dialogue text should reduce font size to avoid clipping") - if fitted_dialogue_size < 14: - failures.append("long dialogue text should not shrink below its readable minimum") + if fitted_dialogue_size < 14 or fitted_dialogue_size > 17: + failures.append("long dialogue font fit should stay inside its configured bounds") var fitted_dialogue_height := float(scene._estimated_wrapped_text_height(long_dialogue, dialogue_area.x, fitted_dialogue_size)) if fitted_dialogue_height > dialogue_area.y: failures.append("fitted long dialogue should stay within the dialogue text area") diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index deaf494..7bd0392 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -214,6 +214,8 @@ func _check_scene_post_move_text_fit(failures: Array[String]) -> void: scene.selected_skill_id = "" scene.basic_attack_targeting = false scene._update_targeting_hint_panel() + if scene.targeting_hint_panel != null and scene.targeting_hint_panel.visible: + failures.append("targeting guidance should stay on the board instead of opening a hint panel") if scene.targeting_hint_title_label == null: failures.append("targeting hint should expose a fitted title label") else: @@ -429,8 +431,8 @@ func _check_scene_post_move_tactic_picker_flow(failures: Array[String]) -> void: failures.append("choosing Spark should enter tactic targeting mode") if scene.post_move_picker_panel != null and scene.post_move_picker_panel.visible: failures.append("tactic picker should hide after choosing a tactic") - if scene.targeting_hint_panel == null or not scene.targeting_hint_panel.visible: - failures.append("targeting hint panel should be visible during tactic targeting") + if scene.targeting_hint_panel != null and scene.targeting_hint_panel.visible: + failures.append("tactic targeting should not open a separate hint panel") if not scene.skill_cells.has(Vector2i(3, 3)): failures.append("Spark targeting should highlight the adjacent enemy cell") var tactic_markers := scene._skill_target_marker_entries() @@ -524,8 +526,8 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void: failures.append("choosing Bean should enter item targeting mode") if scene.post_move_picker_panel != null and scene.post_move_picker_panel.visible: failures.append("item picker should hide after choosing an item") - if scene.targeting_hint_panel == null or not scene.targeting_hint_panel.visible: - failures.append("targeting hint panel should be visible during item targeting") + if scene.targeting_hint_panel != null and scene.targeting_hint_panel.visible: + failures.append("item targeting should not open a separate hint panel") if not scene.item_cells.has(Vector2i(2, 4)): failures.append("Bean targeting should highlight the adjacent injured ally") var item_markers := scene._item_target_marker_entries() diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index f921439..571873f 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3111,11 +3111,11 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: _check_panel_style_frame(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.12, 0.060, 0.026, 0.98), 10) _check_panel_style_fill(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.42, 0.27, 0.095, 0.995)) _check_panel_style_frame(failures, scene.briefing_camp_overview_panel, "briefing silk map panel", Color(0.12, 0.060, 0.026, 0.98), 7) - _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.070, 0.024, 0.012, 0.986)) - _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.37, 0.20, 0.070, 1.0), 14) + _check_panel_style_fill(failures, scene.dialogue_panel, "dialogue panel", Color(0.055, 0.028, 0.016, 0.982)) + _check_panel_style_frame(failures, scene.dialogue_panel, "dialogue panel", Color(0.30, 0.18, 0.075, 1.0), 12) _check_panel_style_fill(failures, scene.dialogue_portrait_panel, "dialogue portrait panel", Color(0.035, 0.014, 0.008, 1.0)) _check_panel_style_fill(failures, scene.dialogue_speaker_panel, "dialogue speaker seal panel", Color(0.36, 0.025, 0.016, 0.995)) - _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.72, 0.53, 0.25, 0.998)) + _check_panel_style_fill(failures, scene.dialogue_text_panel, "dialogue text scroll panel", Color(0.61, 0.47, 0.26, 0.996)) if scene.screen_backdrop == null: failures.append("ancient briefing screen should create an ink-wash backdrop") else: @@ -3140,8 +3140,17 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: _check_bamboo_gutter_row(failures, scene.dialogue_text_panel, "dialogue scroll bamboo gutters") _check_bamboo_gutter_row(failures, scene.mission_detail_panel, "mission detail bamboo gutters") _check_edict_marker_stack(failures, scene.briefing_objective_panel, "briefing objective edict markers") - _check_edict_marker_stack(failures, scene.dialogue_text_panel, "dialogue scroll edict markers") _check_edict_marker_stack(failures, scene.mission_detail_panel, "mission detail edict markers") + var cell_info_panel: PanelContainer = null + if scene.cell_info_label != null: + cell_info_panel = scene.cell_info_label.get_parent() as PanelContainer + _check_panel_style_fill(failures, cell_info_panel, "HUD terrain info panel", Color(0.075, 0.050, 0.034, 0.88)) + _check_panel_style_frame(failures, cell_info_panel, "HUD terrain info panel", Color(0.31, 0.22, 0.12, 0.96), 3) + var forecast_panel: PanelContainer = null + if scene.forecast_label != null: + forecast_panel = scene.forecast_label.get_parent() as PanelContainer + _check_panel_style_fill(failures, forecast_panel, "HUD forecast info panel", Color(0.075, 0.050, 0.034, 0.88)) + _check_panel_style_frame(failures, forecast_panel, "HUD forecast info panel", Color(0.31, 0.22, 0.12, 0.96), 3) _check_hanging_tassel(failures, scene.dialogue_left_tassel, "dialogue left tassel") _check_hanging_tassel(failures, scene.dialogue_right_tassel, "dialogue right tassel") _check_dialogue_column_budget(failures, scene)