459 lines
19 KiB
C#
459 lines
19 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace TKS
|
||
{
|
||
/// <summary>
|
||
/// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격.
|
||
/// 유닛 탭(아군·적군) = 그 유닛의 사거리 표시(RangeIndicator) · 빈 곳 드래그 = 전장 스크롤 · 휠 = 줌 · 미니맵 탭 = 그 지점으로 이동.
|
||
/// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기.
|
||
/// 궁병도 다른 병종과 같이 놓으면 바로 행동한다(조준 방향에 적이 없으면 목표점까지 이동 · PD 2026-11-04 2차 지시).
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>결과 화면 탭(= 계속) 을 건너뛸 자리인가(화면 좌표) — 결과 패널 「다시」 버튼 위(#1104).</summary>
|
||
public System.Func<Vector2, bool> HoldResultTap;
|
||
|
||
/// <summary>이 자리(화면 좌표) 누름은 전장 조작으로 보지 않는다(스킬 테스트 씬 패널 위 · 2026-11-09). 뗄 때까지 무시.</summary>
|
||
public System.Func<Vector2, bool> BlockPress;
|
||
bool pressBlocked;
|
||
|
||
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 ⑤)
|
||
SpriteRenderer teleportArea; // 변진(장합) 목표 지점 반경(PD #1093 B)
|
||
readonly List<Unit> highlighted = new List<Unit>();
|
||
|
||
/// <summary>방향 결정 UI = IB GameTouchArrow 이식(고리 + 화살표 · PD 2026-11-10 「IB 와 같이 방향 결정 UI 등을 그대로 이식」) — 전에 쓰던 조준선 · 당김선 대신.</summary>
|
||
IbAimArrow ibArrow;
|
||
/// <summary>슬링샷 진행 중 캐릭터 연출(IB 이식): 행동할 유닛 발밑 원 · 몸 둘레 빛 + 조준 중 모든 유닛 속성 방울.</summary>
|
||
IbUnitMarks marks;
|
||
|
||
public IbUnitMarks Marks
|
||
{
|
||
get { return marks; }
|
||
}
|
||
public bool AimArrowVisible
|
||
{
|
||
get { return ibArrow != null && ibArrow.ArrowVisible; }
|
||
}
|
||
|
||
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; }
|
||
}
|
||
|
||
/// <summary>책사 광역 원의 지름(월드) = strategistSplashRadius × 2 · 중심 = SplashCenter.</summary>
|
||
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);
|
||
teleportArea = ProceduralSprites.NewSprite(transform, "TeleportArea", ProceduralSprites.Circle, new Color(1f, 0.85f, 0.3f, 0.18f), 1f, -8);
|
||
teleportArea.gameObject.SetActive(false);
|
||
|
||
var rgo = new GameObject("RangeIndicator");
|
||
rgo.transform.SetParent(transform, false);
|
||
range = rgo.AddComponent<RangeIndicator>();
|
||
range.Init(b.Config.archerRangeRatio);
|
||
|
||
var ago = new GameObject("IbAimArrow");
|
||
ago.transform.SetParent(transform, false);
|
||
ibArrow = ago.AddComponent<IbAimArrow>();
|
||
ibArrow.Init(b.Board);
|
||
marks = ago.AddComponent<IbUnitMarks>();
|
||
}
|
||
|
||
LineRenderer MakeLine(string name, float width, Color color, int order)
|
||
{
|
||
var go = new GameObject(name);
|
||
go.transform.SetParent(transform, false);
|
||
var lr = go.AddComponent<LineRenderer>();
|
||
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) && (HoldResultTap == null || !HoldResultTap(Input.mousePosition))) battle.ContinueAfterResult();
|
||
return;
|
||
}
|
||
|
||
Vector2 screen = Input.mousePosition;
|
||
if (Input.GetMouseButtonDown(0)) pressBlocked = BlockPress != null && BlockPress(screen);
|
||
if (pressBlocked)
|
||
{
|
||
if (Input.GetMouseButtonUp(0) || !Input.GetMouseButton(0)) pressBlocked = false;
|
||
return;
|
||
}
|
||
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);
|
||
|
||
// 조준 중(첫 손가락으로 당기는 중) 다른 손가락으로 스킬 버튼 탭 = 예약/취소(PD #1093 B)
|
||
if (dragging && hud != null && Input.touchCount > 1)
|
||
{
|
||
for (int i = 1; i < Input.touchCount; i++)
|
||
{
|
||
var t = Input.GetTouch(i);
|
||
if (t.phase == TouchPhase.Began && hud.TrySkillTap(t.position) && selected != null && previewValid) ShowPreview(selected, currentTarget, null);
|
||
}
|
||
}
|
||
RefreshTeleportArea();
|
||
|
||
float wheel = Input.mouseScrollDelta.y;
|
||
if (Mathf.Abs(wheel) > 0.01f && camCtrl != null) camCtrl.Zoom(1f - wheel * 0.1f);
|
||
}
|
||
|
||
/// <summary>변진 목표 지점 대기 중이면 반경 원 표시.</summary>
|
||
void RefreshTeleportArea()
|
||
{
|
||
var sys = battle.Skills;
|
||
var u = sys != null ? sys.PendingUnit : null;
|
||
bool on = u != null && u.IsAlive && sys.PendingRadius > 0f; // 변진 목표 · 맹장 복귀 지점(조조 방향 탭 = 반경 없음)
|
||
if (on)
|
||
{
|
||
float d = sys.PendingRadius * 2f;
|
||
Vector2 c = sys.PendingKind == SkillSystem.TapKind.Teleport ? u.Position : sys.PendingCenter;
|
||
teleportArea.transform.position = new Vector3(c.x, c.y, 0f);
|
||
teleportArea.transform.localScale = new Vector3(d, d, 1f);
|
||
}
|
||
if (teleportArea.gameObject.activeSelf != on) teleportArea.gameObject.SetActive(on);
|
||
}
|
||
|
||
public bool TeleportAreaVisible
|
||
{
|
||
get { return teleportArea != null && teleportArea.gameObject.activeSelf; }
|
||
}
|
||
|
||
void OnPress(Vector2 screen)
|
||
{
|
||
lastScreen = screen;
|
||
panPixels = 0f;
|
||
dragging = false;
|
||
panning = false;
|
||
emptyPress = false;
|
||
previewValid = false;
|
||
|
||
// 스킬 버튼(HUD) 탭 = 예약/취소/즉시 발동 · 변진 대기 중 = 목표 지점 탭(PD #1093 B)
|
||
if (hud != null && hud.TrySkillTap(screen)) return;
|
||
string tpMsg;
|
||
if (battle.Skills != null && battle.Skills.TryTeleportTap(ScreenToWorld(screen), out tpMsg))
|
||
{
|
||
if (!string.IsNullOrEmpty(tpMsg)) battle.LastMessage = tpMsg;
|
||
return;
|
||
}
|
||
|
||
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);
|
||
if (ibArrow != null) ibArrow.Begin(u); // IB Ready: 누른 순간 고리
|
||
if (marks != null) marks.ShowBubbles(battle.Units); // IB: 조준 중 모든 유닛 속성 방울
|
||
}
|
||
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();
|
||
if (ibArrow != null) ibArrow.Aim(selected, Vector2.zero); // 최소 당김 안 = 고리만 · 화살표 숨김
|
||
previewValid = false;
|
||
return;
|
||
}
|
||
float sling = SkillMods.SlingMax(selected); // 동남풍·적토마(PD #1093 B)
|
||
if (mag > sling) launch = launch / mag * sling;
|
||
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 (ibArrow != null) ibArrow.End();
|
||
if (marks != null) marks.HideBubbles();
|
||
if (u.View != null) u.View.SetAiming(false);
|
||
Select(null);
|
||
battle.RequestPlayerAction(u, t);
|
||
}
|
||
else if (emptyPress && panPixels <= TapPixels)
|
||
{
|
||
battle.OnEmptyTap();
|
||
}
|
||
else
|
||
{
|
||
HidePreview();
|
||
if (ibArrow != null) ibArrow.End();
|
||
if (marks != null) marks.HideBubbles();
|
||
if (dragging && selected != null && selected.View != null) selected.View.SetAiming(false);
|
||
}
|
||
dragging = false;
|
||
panning = false;
|
||
emptyPress = false;
|
||
previewValid = false;
|
||
}
|
||
|
||
/// <summary>유닛 선택(아군·적군 · null = 해제): 선택 링 + HUD 정보 + 사거리 표시(궁병은 도넛 · 범위 안 적 유무로 색).</summary>
|
||
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));
|
||
}
|
||
if (marks != null) marks.ShowTurn(u != null && u.Faction == Faction.Player && battle.CanAct(u) ? u : null); // IB ShowUserTurnMark
|
||
}
|
||
|
||
/// <summary>조준 표시(자동 플레이 캡처에서도 호출). 반환 = 공격 예정 적 수.</summary>
|
||
public int ShowPreview(Unit u, Vector2 target, Vector2? pointer)
|
||
{
|
||
Vector2 aim = target;
|
||
target = battle.Resolver.ReachPoint(u, target); // 늪·고지 = 실제 정지점(PD #1097)
|
||
if (ibArrow != null)
|
||
{
|
||
// IB 방향 결정 UI: 화살표 = 쏠 방향 · 길이 = IB 공식(당김 × 0.6 · 유닛 반지름 기준) — 조준선 · 당김선 대신(PD 2026-11-10)
|
||
ibArrow.Aim(u, aim - u.Position);
|
||
aimLine.enabled = false;
|
||
pullLine.enabled = false;
|
||
}
|
||
else
|
||
{
|
||
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));
|
||
pullLine.enabled = pointer.HasValue;
|
||
if (pointer.HasValue)
|
||
{
|
||
pullLine.SetPosition(0, new Vector3(u.Position.x, u.Position.y, 0f));
|
||
pullLine.SetPosition(1, new Vector3(pointer.Value.x, pointer.Value.y, 0f));
|
||
}
|
||
}
|
||
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, aim, moveOnly);
|
||
var gl = battle.Gimmicks;
|
||
if (gl != null) gl.Preview.Show(u, aim, moveOnly, shot); // 예상 궤적(튕김·밀림·연쇄·반작용) · 기믹 반응 · 지대 띠(PD #1097 · #1098)
|
||
if (shot != null)
|
||
{
|
||
return ShowShot(u, shot);
|
||
}
|
||
HideShot();
|
||
foreach (var e in battle.Resolver.PredictTargets(u, aim, moveOnly)) Highlight(e, false);
|
||
return highlighted.Count;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 궁병·책사 조준(PD 2026-11-05 #1087 ⑤⑥): 화살 경로선(쏘는 위치 → 첫 피격 지점) + 첫 피격 유닛 강조(같은 편이면 붉게)
|
||
/// · 책사 = 맞을 자리 광역 원 + 휩쓸릴 유닛 강조(같은 편 붉게). 반환 = 맞을 상대 유닛 수.
|
||
/// </summary>
|
||
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.HasSplash; // 덤불·수레에 막혀도 그 자리 광역(PD #1097)
|
||
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 = shot.SplashRadius * 2f; // 화공 = 반경 ×(PD #1093 B) · 안개 중심 = ×p1
|
||
splashArea.transform.position = new Vector3(shot.SplashCenter.x, shot.SplashCenter.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();
|
||
if (battle != null && battle.Gimmicks != null) battle.Gimmicks.Preview.Hide();
|
||
}
|
||
|
||
void ClearHighlights()
|
||
{
|
||
foreach (var h in highlighted)
|
||
{
|
||
if (h != null && h.View != null) h.View.SetHighlight(false);
|
||
}
|
||
highlighted.Clear();
|
||
}
|
||
}
|
||
}
|