diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 6a7536e..6f79f33 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -39,7 +39,7 @@ 4. Show scenario briefing with the campaign chapter header, battle header, objective summary, chapter overview, selectable camp Talk conversations, manual checkpoint menu, and optional pre-battle shop, Armory, Roster, and Formation setup. 5. Player selects a unit. 6. Briefing completion dispatches battle-begin events, including opening dialogue. -7. Unit moves, attacks, chooses a tactic or consumable item from the side menu, changes equipment, counters, gains EXP, levels up, promotes at class thresholds, or waits. Clicking an enemy that can be reached by moving first performs the combined move-and-attack action directly, while the map marks those targets with compact move-attack badges before the click; the selected attack origin favors defeat chances and low counter risk, and hover badges show possible counter danger even when a miss could cause it. Tactic buttons show MP cost, kind, range, area radius, and power, support, debuff, movement modifier, accuracy/evasion modifier, damage-over-time, or action-lock effect. Physical attacks roll hit chance from unit agility and target terrain avoid, add facing-based side/rear damage bonuses, and equipped weapons can add small damage bonuses against matching target move types; misses give reduced EXP. The scene plays placeholder SFX, a short movement slide, class-specific physical attack motion profiles, low-HP warning rings, hover target preview badges, area tactic overlays, an enemy threat overlay with physical damage and hostile tactic hints, selected/hover unit role, movement, facing, terrain, status, and equipment details, and floating combat text for core action and UI feedback. +7. Unit moves, attacks, chooses a tactic or consumable item from the side menu, changes equipment, counters, gains EXP, levels up, promotes at class thresholds, or waits. Clicking an enemy that can be reached by moving first performs the combined move-and-attack action directly, while the map marks those targets with compact move-attack badges plus the chosen advance origin and attack line before the click; the selected attack origin favors defeat chances and low counter risk, and hover badges show possible counter danger even when a miss could cause it. Tactic buttons show MP cost, kind, range, area radius, and power, support, debuff, movement modifier, accuracy/evasion modifier, damage-over-time, or action-lock effect. Physical attacks roll hit chance from unit agility and target terrain avoid, add facing-based side/rear damage bonuses, and equipped weapons can add small damage bonuses against matching target move types; misses give reduced EXP. The scene plays placeholder SFX, a short movement slide, class-specific physical attack motion profiles, low-HP warning rings, hover target preview badges, area tactic overlays, an enemy threat overlay with physical damage and hostile tactic hints, selected/hover unit role, movement, facing, terrain, status, and equipment details, and floating combat text for core action and UI feedback. 8. Player ends turn. 9. Enemy AI moves, picks physical attacks from damage, defeat, priority, and weapon-effectiveness scores, and can cast single-target or area damage, healing, support, debuff, movement-modifier, accuracy/evasion-modifier, damage-over-time, or action-lock tactics through the same combat and skill resolvers. 10. Battle checks scenario-defined defeat conditions first, including commander loss and turn limits, then victory conditions such as enemy defeat or reaching a marked destination tile. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 2053dd6..4eeeb68 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -3542,6 +3542,8 @@ func _draw_target_selection_markers() -> void: var rect := _rect_for_cell(cell) var marker_rect := Rect2(rect.position + Vector2(7.0, TILE_SIZE - 24.0), Vector2(TILE_SIZE - 14.0, 18.0)) var color := _target_preview_badge_color(str(marker.get("kind", "damage"))) + if bool(marker.get("requires_move", false)): + _draw_auto_attack_origin_marker(marker, color, font) draw_rect(rect.grow(-3.0), Color(color.r, color.g, color.b, 0.18)) draw_rect(rect.grow(-6.0), color, false, 2.2) draw_rect(marker_rect, Color(0.025, 0.028, 0.034, 0.88)) @@ -3549,6 +3551,55 @@ func _draw_target_selection_markers() -> void: _draw_ink_text(font, marker_rect.position + Vector2(0, 13), str(marker.get("text", "표")), HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 12, color) +func _draw_auto_attack_origin_marker(marker: Dictionary, target_color: Color, font: Font) -> void: + var origin: Vector2i = marker.get("origin", Vector2i(-1, -1)) + var target_cell: Vector2i = marker.get("cell", Vector2i(-1, -1)) + if not state.is_inside(origin) or not state.is_inside(target_cell): + return + if _cell_is_in_visible_bounds(origin) and _cell_is_in_visible_bounds(target_cell): + var route_points := _auto_attack_route_points(origin, target_cell) + if route_points.size() >= 2: + var from_point := route_points[0] + var to_point := route_points[1] + var route_color := Color(target_color.r, target_color.g, target_color.b, 0.62) + draw_line(from_point, to_point, Color(0.02, 0.014, 0.010, 0.82), 4.0) + draw_line(from_point, to_point, route_color, 2.2) + var direction := (to_point - from_point).normalized() + if direction.length() > 0.0: + var side := Vector2(-direction.y, direction.x) + draw_line(to_point, to_point - direction * 8.5 + side * 4.2, route_color, 2.0) + draw_line(to_point, to_point - direction * 8.5 - side * 4.2, route_color, 2.0) + if not _cell_is_in_visible_bounds(origin): + return + var origin_rect := _rect_for_cell(origin) + var move_color := _target_preview_badge_color("move") + var marker_rect := _auto_attack_origin_marker_rect(origin) + draw_rect(origin_rect.grow(-8.0), Color(move_color.r, move_color.g, move_color.b, 0.17)) + draw_rect(origin_rect.grow(-10.0), move_color, false, 1.8) + draw_rect(marker_rect, Color(0.025, 0.028, 0.034, 0.86)) + draw_rect(marker_rect, move_color, false, 1.2) + _draw_ink_text(font, marker_rect.position + Vector2(0, 13), "진", HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 12, move_color) + + +func _auto_attack_origin_marker_rect(origin: Vector2i) -> Rect2: + var rect := _rect_for_cell(origin) + return Rect2(rect.position + Vector2(7.0, 5.0), Vector2(22.0, 18.0)) + + +func _auto_attack_route_points(origin: Vector2i, target_cell: Vector2i) -> PackedVector2Array: + if not state.is_inside(origin) or not state.is_inside(target_cell): + return PackedVector2Array() + var from_point := _rect_for_cell(origin).get_center() + var to_point := _rect_for_cell(target_cell).get_center() + var delta := to_point - from_point + var distance := delta.length() + if distance > 1.0: + var direction := delta / distance + from_point += direction * 19.0 + to_point -= direction * 19.0 + return PackedVector2Array([from_point, to_point]) + + func _draw_unit_nameplate(rect: Rect2, label: String) -> void: if label.is_empty(): return diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index 5f1b233..7ba893f 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -613,6 +613,26 @@ func _check_scene_auto_attack_safe_origin_and_counter_preview(failures: Array[St failures.append("safe auto attack marker should expose no counter risk: %s" % str(safe_marker)) if str(safe_marker.get("kind", "")) != "advance": failures.append("safe auto attack marker should use advance color: %s" % str(safe_marker)) + var origin_marker_rect: Rect2 = scene._auto_attack_origin_marker_rect(safe_marker.get("origin", Vector2i.ZERO)) + if not scene._rect_for_cell(Vector2i(3, 3)).encloses(origin_marker_rect): + failures.append("auto attack origin marker should fit inside the chosen origin cell: %s" % str(origin_marker_rect)) + var route_points: PackedVector2Array = scene._auto_attack_route_points(safe_marker.get("origin", Vector2i.ZERO), safe_marker.get("cell", Vector2i.ZERO)) + if route_points.size() != 2: + failures.append("auto attack route should expose two drawable points: %s" % str(route_points)) + elif not scene._rect_for_cell(Vector2i(3, 3)).has_point(route_points[0]) or not scene._rect_for_cell(Vector2i(5, 3)).has_point(route_points[1]): + failures.append("auto attack route should connect the origin and target cells: %s" % str(route_points)) + var far_route_points: PackedVector2Array = scene._auto_attack_route_points(Vector2i(-1, -1), safe_marker.get("cell", Vector2i.ZERO)) + if not far_route_points.is_empty(): + failures.append("auto attack route should not draw from invalid off-board origins: %s" % str(far_route_points)) + + scene.basic_attack_targeting = true + scene.attack_cells.clear() + scene.attack_cells.append(Vector2i(5, 3)) + for marker in scene._target_selection_marker_entries(): + if bool(marker.get("requires_move", false)): + failures.append("formal attack targeting should not show move+attack origin routes: %s" % str(marker)) + scene.basic_attack_targeting = false + scene._refresh_ranges() scene.hover_cell = Vector2i(5, 3) var safe_badge := scene._target_preview_badge()