296 lines
11 KiB
C#
296 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace TKS
|
|
{
|
|
/// <summary>
|
|
/// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격.
|
|
/// 유닛 탭(아군·적군) = 그 유닛의 사거리 표시(RangeIndicator) · 빈 곳 드래그 = 전장 스크롤 · 휠 = 줌 · 미니맵 탭 = 그 지점으로 이동.
|
|
/// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기.
|
|
/// 궁병(PD 2026-11-04): 조준 방향 슬링샷 거리 + 사거리 안에 적이 없으면 놓아도 행동하지 않는다(선택 유지 · 안내 문구).
|
|
/// </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 TargetBlockedColor = new Color(0.65f, 0.65f, 0.65f, 0.7f);
|
|
|
|
BattleManager battle;
|
|
Camera cam;
|
|
BattleHUD hud;
|
|
CameraController camCtrl;
|
|
RangeIndicator range;
|
|
|
|
Unit selected;
|
|
bool dragging;
|
|
bool panning;
|
|
bool emptyPress;
|
|
bool previewValid;
|
|
bool archerBlocked;
|
|
Vector2 lastScreen;
|
|
float panPixels;
|
|
Vector2 currentTarget;
|
|
|
|
LineRenderer aimLine;
|
|
LineRenderer pullLine;
|
|
SpriteRenderer targetRing;
|
|
readonly List<Unit> highlighted = new List<Unit>();
|
|
|
|
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<RangeIndicator>();
|
|
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<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)) 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;
|
|
archerBlocked = 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;
|
|
archerBlocked = false;
|
|
return;
|
|
}
|
|
if (mag > selected.SlingMaxDistance) launch = launch / mag * selected.SlingMaxDistance;
|
|
currentTarget = battle.Board.ClampInside(selected.Position + launch, selected.Radius);
|
|
previewValid = true;
|
|
int targets = ShowPreview(selected, currentTarget, w);
|
|
archerBlocked = selected.Type == UnitType.Archer && !selected.BonusMoveAvailable && targets == 0;
|
|
if (archerBlocked)
|
|
{
|
|
targetRing.color = TargetBlockedColor;
|
|
targetRing.transform.localScale = new Vector3(0.7f, 0.7f, 1f);
|
|
}
|
|
}
|
|
|
|
void OnRelease(Vector2 screen)
|
|
{
|
|
if (dragging && selected != null && previewValid && battle.CanAct(selected))
|
|
{
|
|
if (archerBlocked)
|
|
{
|
|
// 궁병: 조준 방향 슬링샷 범위 + 비율 안에 적 없음 → 행동 취소(선택·사거리 표시 유지)
|
|
HidePreview();
|
|
if (selected.View != null) selected.View.SetAiming(false);
|
|
int pct = Mathf.RoundToInt(battle.Config.archerRangeRatio * 100f);
|
|
battle.LastMessage = selected.DisplayName + "(궁병) — 슬링샷 범위 +" + pct + " % 안에 적이 없어 행동할 수 없습니다";
|
|
battle.NotifyChanged();
|
|
}
|
|
else
|
|
{
|
|
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;
|
|
archerBlocked = 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 && u.Type == UnitType.Archer) range.SetArcherReachable(battle.AnyEnemyWithinArcherReach(u));
|
|
}
|
|
}
|
|
|
|
/// <summary>조준 표시(자동 플레이 캡처에서도 호출). 반환 = 공격 예정 적 수.</summary>
|
|
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();
|
|
}
|
|
}
|
|
}
|