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();
}
}
}