using System.Collections.Generic; using UnityEngine; namespace TKS { /// /// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격. /// 유닛 탭(아군·적군) = 그 유닛의 사거리 표시(RangeIndicator) · 빈 곳 드래그 = 전장 스크롤 · 휠 = 줌 · 미니맵 탭 = 그 지점으로 이동. /// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기. /// 궁병도 다른 병종과 같이 놓으면 바로 행동한다(조준 방향에 적이 없으면 목표점까지 이동 · PD 2026-11-04 2차 지시). /// public class SlingshotInput : MonoBehaviour { static readonly Color TargetFreeColor = Color.white; static readonly Color TargetHitColor = new Color(1f, 0.6f, 0.1f); BattleManager battle; Camera cam; BattleHUD hud; CameraController camCtrl; RangeIndicator range; Unit selected; bool dragging; bool panning; bool emptyPress; bool previewValid; Vector2 lastScreen; float panPixels; Vector2 currentTarget; LineRenderer aimLine; LineRenderer pullLine; SpriteRenderer targetRing; readonly List highlighted = new List(); public Unit Selected { get { return selected; } } public RangeIndicator Range { get { return range; } } float TapPixels { get { return Mathf.Max(8f, Screen.height * 0.012f); } } public void Init(BattleManager b, Camera c, BattleHUD h, CameraController cc) { battle = b; cam = c; hud = h; camCtrl = cc; aimLine = MakeLine("AimLine", 0.08f, new Color(1f, 1f, 1f, 0.9f), 10); pullLine = MakeLine("PullLine", 0.035f, new Color(1f, 1f, 1f, 0.35f), 9); targetRing = ProceduralSprites.NewSprite(transform, "TargetRing", ProceduralSprites.Ring, Color.white, 0.7f, 11); targetRing.enabled = false; var rgo = new GameObject("RangeIndicator"); rgo.transform.SetParent(transform, false); range = rgo.AddComponent(); range.Init(b.Config.archerRangeRatio); } LineRenderer MakeLine(string name, float width, Color color, int order) { var go = new GameObject(name); go.transform.SetParent(transform, false); var lr = go.AddComponent(); lr.useWorldSpace = true; lr.positionCount = 2; lr.startWidth = width; lr.endWidth = width; lr.material = new Material(Shader.Find("Sprites/Default")); lr.startColor = color; lr.endColor = color; lr.sortingOrder = order; lr.numCapVertices = 4; lr.enabled = false; return lr; } Vector2 ScreenToWorld(Vector2 screen) { Vector3 p = screen; p.z = -cam.transform.position.z; return cam.ScreenToWorldPoint(p); } void Update() { if (battle == null || battle.Config == null) return; var phase = battle.Phase; if (phase == BattlePhase.StageClear || phase == BattlePhase.Defeat) { if (Input.GetMouseButtonDown(0)) battle.ContinueAfterResult(); return; } Vector2 screen = Input.mousePosition; if (Input.GetMouseButtonDown(0)) OnPress(screen); else if (Input.GetMouseButton(0)) { if (dragging) OnDrag(ScreenToWorld(screen)); else if (panning) OnPan(screen); } if (Input.GetMouseButtonUp(0)) OnRelease(screen); float wheel = Input.mouseScrollDelta.y; if (Mathf.Abs(wheel) > 0.01f && camCtrl != null) camCtrl.Zoom(1f - wheel * 0.1f); } void OnPress(Vector2 screen) { lastScreen = screen; panPixels = 0f; dragging = false; panning = false; emptyPress = false; previewValid = false; Vector2 mapWorld; if (hud != null && camCtrl != null && hud.TryMinimapTap(screen, out mapWorld)) { camCtrl.FocusOn(mapWorld); return; } Vector2 w = ScreenToWorld(screen); var u = battle.UnitNear(w, Faction.Player); if (u == null) u = battle.UnitNear(w, null); // 적 유닛 터치 = 적의 사거리 표시 Select(u); if (u != null && battle.CanAct(u)) { dragging = true; if (u.View != null) u.View.SetAiming(true); } else if (u == null) { panning = true; emptyPress = true; } } void OnPan(Vector2 screen) { Vector2 d = screen - lastScreen; lastScreen = screen; panPixels += d.magnitude; if (panPixels > TapPixels && camCtrl != null) { camCtrl.PanBy(-d * camCtrl.WorldUnitsPerPixel); } } void OnDrag(Vector2 w) { if (selected == null) return; Vector2 pull = w - selected.Position; Vector2 launch = -pull; float mag = launch.magnitude; if (mag < battle.Config.minDragDistance) { HidePreview(); previewValid = false; return; } if (mag > selected.SlingMaxDistance) launch = launch / mag * selected.SlingMaxDistance; currentTarget = battle.Board.ClampInside(selected.Position + launch, selected.Radius); previewValid = true; ShowPreview(selected, currentTarget, w); } void OnRelease(Vector2 screen) { if (dragging && selected != null && previewValid && battle.CanAct(selected)) { var t = currentTarget; var u = selected; HidePreview(); if (u.View != null) u.View.SetAiming(false); Select(null); battle.RequestPlayerAction(u, t); } else if (emptyPress && panPixels <= TapPixels) { battle.OnEmptyTap(); } else { HidePreview(); if (dragging && selected != null && selected.View != null) selected.View.SetAiming(false); } dragging = false; panning = false; emptyPress = false; previewValid = false; } /// 유닛 선택(아군·적군 · null = 해제): 선택 링 + HUD 정보 + 사거리 표시(궁병은 도넛 · 범위 안 적 유무로 색). public void Select(Unit u) { if (selected != null && selected.View != null) { selected.View.SetSelected(false); selected.View.SetAiming(false); } selected = u; if (u != null && u.View != null) u.View.SetSelected(true); if (hud != null) hud.SetSelected(u); if (range != null) { range.Show(u); if (u != null && u.Type == UnitType.Archer) range.SetArcherReachable(battle.AnyEnemyWithinArcherReach(u)); } } /// 조준 표시(자동 플레이 캡처에서도 호출). 반환 = 공격 예정 적 수. public int ShowPreview(Unit u, Vector2 target, Vector2? pointer) { aimLine.enabled = true; aimLine.SetPosition(0, new Vector3(u.Position.x, u.Position.y, 0f)); aimLine.SetPosition(1, new Vector3(target.x, target.y, 0f)); if (pointer.HasValue) { pullLine.enabled = true; pullLine.SetPosition(0, new Vector3(u.Position.x, u.Position.y, 0f)); pullLine.SetPosition(1, new Vector3(pointer.Value.x, pointer.Value.y, 0f)); } else { pullLine.enabled = false; } targetRing.enabled = true; targetRing.transform.position = new Vector3(target.x, target.y, 0f); bool moveOnly = u.BonusMoveAvailable; var finalHit = moveOnly ? null : Combat.EnemyAtPoint(u, target, battle.Config.finalHitTolerance); targetRing.color = finalHit != null ? TargetHitColor : TargetFreeColor; targetRing.transform.localScale = finalHit != null ? new Vector3(0.9f, 0.9f, 1f) : new Vector3(0.7f, 0.7f, 1f); ClearHighlights(); foreach (var e in battle.Resolver.PredictTargets(u, target, moveOnly)) { if (e.View != null) e.View.SetHighlight(true); highlighted.Add(e); } return highlighted.Count; } public void HidePreview() { aimLine.enabled = false; pullLine.enabled = false; targetRing.enabled = false; ClearHighlights(); } void ClearHighlights() { foreach (var h in highlighted) { if (h != null && h.View != null) h.View.SetHighlight(false); } highlighted.Clear(); } } }