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); static readonly Color ShotLineColor = new Color(1f, 0.92f, 0.6f, 0.95f); static readonly Color ShotLineFriendlyColor = new Color(1f, 0.2f, 0.2f, 0.95f); static readonly Color SplashAreaColor = new Color(0.7f, 0.45f, 1f, 0.22f); static readonly Color SplashAreaFriendlyColor = new Color(1f, 0.25f, 0.25f, 0.26f); 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; LineRenderer shotLine; // 궁병·책사 투사체 경로(쏘는 위치 → 첫 피격 지점 · PD 2026-11-05 #1087 ⑥) SpriteRenderer targetRing; SpriteRenderer splashArea; // 책사 광역 원(중심 = 첫 피격 유닛 · #1087 ⑤) readonly List highlighted = new List(); public Unit Selected { get { return selected; } } public bool ShotLineVisible { get { return shotLine != null && shotLine.enabled; } } public bool SplashVisible { get { return splashArea != null && splashArea.gameObject.activeSelf; } } /// 책사 광역 원의 지름(월드) = strategistSplashRadius × 2 · 중심 = SplashCenter. public float SplashDiameter { get { return splashArea != null ? splashArea.transform.localScale.x : 0f; } } public Vector2 SplashCenter { get { return splashArea != null ? (Vector2)splashArea.transform.position : Vector2.zero; } } 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); shotLine = MakeLine("ShotLine", 0.05f, ShotLineColor, 10); targetRing = ProceduralSprites.NewSprite(transform, "TargetRing", ProceduralSprites.Ring, Color.white, 0.7f, 11); targetRing.enabled = false; splashArea = ProceduralSprites.NewSprite(transform, "SplashArea", ProceduralSprites.Circle, SplashAreaColor, 1f, -8); splashArea.gameObject.SetActive(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 && UnitTypeInfo.IsRanged(u.Type)) 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(); var shot = battle.Resolver.PreviewShot(u, target, moveOnly); if (shot != null) { return ShowShot(u, shot); } HideShot(); foreach (var e in battle.Resolver.PredictTargets(u, target, moveOnly)) Highlight(e, false); return highlighted.Count; } /// /// 궁병·책사 조준(PD 2026-11-05 #1087 ⑤⑥): 화살 경로선(쏘는 위치 → 첫 피격 지점) + 첫 피격 유닛 강조(같은 편이면 붉게) /// · 책사 = 맞을 자리 광역 원 + 휩쓸릴 유닛 강조(같은 편 붉게). 반환 = 맞을 상대 유닛 수. /// int ShowShot(Unit u, ActionResolver.ShotPlan shot) { if (!shot.Fires) { HideShot(); return 0; } bool friendlyCenter = shot.Hit != null && shot.Hit.Faction == u.Faction; shotLine.enabled = true; shotLine.startColor = shotLine.endColor = friendlyCenter ? ShotLineFriendlyColor : ShotLineColor; shotLine.SetPosition(0, new Vector3(shot.From.x, shot.From.y, 0f)); shotLine.SetPosition(1, new Vector3(shot.To.x, shot.To.y, 0f)); int foes = 0; if (shot.Hit != null) { Highlight(shot.Hit, friendlyCenter); if (!friendlyCenter) foes++; } bool splash = u.Type == UnitType.Strategist && shot.Hit != null; if (splash) { bool anyFriendly = friendlyCenter; foreach (var s in shot.Splash) { bool friendly = s.Faction == u.Faction; Highlight(s, friendly); if (friendly) anyFriendly = true; else foes++; } float d = battle.Config.strategistSplashRadius * 2f; splashArea.transform.position = new Vector3(shot.Hit.Position.x, shot.Hit.Position.y, 0f); splashArea.transform.localScale = new Vector3(d, d, 1f); splashArea.color = anyFriendly ? SplashAreaFriendlyColor : SplashAreaColor; } if (splashArea.gameObject.activeSelf != splash) splashArea.gameObject.SetActive(splash); return foes; } void HideShot() { shotLine.enabled = false; splashArea.gameObject.SetActive(false); } void Highlight(Unit e, bool friendly) { if (e == null || highlighted.Contains(e)) return; if (e.View != null) e.View.SetHighlight(true, friendly); highlighted.Add(e); } public void HidePreview() { aimLine.enabled = false; pullLine.enabled = false; targetRing.enabled = false; HideShot(); ClearHighlights(); } void ClearHighlights() { foreach (var h in highlighted) { if (h != null && h.View != null) h.View.SetHighlight(false); } highlighted.Clear(); } } }