using UnityEngine;
namespace TKS
{
///
/// 넓은 전장용 카메라: 화면보다 큰 전장을 빈 곳 드래그로 스크롤 · 휠로 줌 · 행동 중인 유닛 자동 추적 · 전장 밖으로 못 나가게 고정.
///
public class CameraController : MonoBehaviour
{
public Board Board;
public float ViewHeight = 18f;
public float MinViewHeight = 10f;
public float MaxViewHeight = 36f;
public float Margin = 1f;
[Tooltip("위쪽 HUD(스테이지·턴 글자) 밑에 유닛이 깔리지 않도록 전장 위 끝을 더 내려 볼 수 있는 여유")]
public float TopMargin = 2.6f;
[Tooltip("아래쪽 HUD(안내·유닛 정보) 여유")]
public float BottomMargin = 2.2f;
public float FollowSpeed = 7f;
Camera cam;
Vector2? focusTarget;
Transform followTarget;
public Camera Cam
{
get
{
if (cam == null) cam = GetComponent();
return cam;
}
}
public void Init(Board board, float viewHeight, float minViewHeight, float maxViewHeight)
{
Board = board;
MinViewHeight = Mathf.Max(1f, minViewHeight);
MaxViewHeight = Mathf.Max(MinViewHeight, maxViewHeight);
ViewHeight = Mathf.Clamp(viewHeight, MinViewHeight, MaxViewHeight);
Cam.orthographicSize = ViewHeight * 0.5f;
SetPosition(transform.position);
}
/// 화면 1 픽셀이 월드 몇 단위인가(드래그 스크롤 환산).
public float WorldUnitsPerPixel
{
get { return Cam.orthographicSize * 2f / Mathf.Max(1, Cam.pixelHeight); }
}
public Rect ViewRect
{
get
{
float halfH = Cam.orthographicSize;
float halfW = halfH * Cam.aspect;
Vector2 c = transform.position;
return new Rect(c.x - halfW, c.y - halfH, halfW * 2f, halfH * 2f);
}
}
public void PanBy(Vector2 worldDelta)
{
focusTarget = null;
followTarget = null;
SetPosition((Vector2)transform.position + worldDelta);
}
public void FocusOn(Vector2 worldPos)
{
followTarget = null;
focusTarget = worldPos;
}
public void SnapTo(Vector2 worldPos)
{
followTarget = null;
focusTarget = null;
SetPosition(worldPos);
}
public void Follow(Transform t)
{
focusTarget = null;
followTarget = t;
}
public void StopFollow()
{
followTarget = null;
focusTarget = null;
}
public void Zoom(float factor)
{
SetViewHeight(ViewHeight * factor);
}
public void SetViewHeight(float height)
{
ViewHeight = Mathf.Clamp(height, MinViewHeight, MaxViewHeight);
Cam.orthographicSize = ViewHeight * 0.5f;
SetPosition(transform.position);
}
// 화면 흔들림(타격감 · PD 2026-11-10 「타격감을 위한 화면 효과(화면 흔들림 등)도 IB 를 참고해서」) — 추적·스크롤 위치(기준)에 매 프레임 흔들림만 더하고
// 다음 프레임 처음에 빼서 기준 위치를 흔들지 않는다. 값 = 표(Resources/Skills/hit_feel.csv · HitFeel).
Vector2 shakeApplied;
float shakeTime;
float shakeDuration;
float shakeMagnitude;
/// 흔들기 시작(진행 중이면 더 센 쪽을 남김) · duration 초 · magnitude = 최대 흔들림(월드 단위) · 끝으로 갈수록 약해짐.
public void Shake(float duration, float magnitude)
{
if (duration <= 0f || magnitude <= 0f) return;
float remain = shakeDuration > 0f ? shakeMagnitude * Mathf.Clamp01(1f - shakeTime / shakeDuration) : 0f;
if (magnitude < remain) return;
shakeDuration = duration;
shakeMagnitude = magnitude;
shakeTime = 0f;
}
public bool Shaking
{
get { return shakeDuration > 0f && shakeTime < shakeDuration; }
}
void LateUpdate()
{
if (shakeApplied != Vector2.zero) // 지난 프레임 흔들림 빼기(기준 위치 복원)
{
transform.position -= (Vector3)shakeApplied;
shakeApplied = Vector2.zero;
}
Follow();
if (Shaking)
{
shakeTime += Time.unscaledDeltaTime;
float k = Mathf.Clamp01(1f - shakeTime / shakeDuration);
shakeApplied = Random.insideUnitCircle * (shakeMagnitude * k);
transform.position += (Vector3)shakeApplied;
}
}
void Follow()
{
Vector2? goal = null;
if (followTarget != null && followTarget.gameObject.activeInHierarchy) goal = (Vector2)followTarget.position;
else if (focusTarget.HasValue) goal = focusTarget;
if (!goal.HasValue) return;
Vector2 target = Clamp(goal.Value);
Vector2 cur = transform.position;
float k = 1f - Mathf.Exp(-FollowSpeed * Time.deltaTime);
Vector2 next = Vector2.Lerp(cur, target, k);
SetPosition(next);
if (focusTarget.HasValue && Vector2.Distance(next, target) < 0.02f) focusTarget = null;
}
void SetPosition(Vector2 p)
{
p = Clamp(p);
transform.position = new Vector3(p.x, p.y, -10f);
}
public Vector2 Clamp(Vector2 p)
{
if (Board == null) return p;
float halfH = Cam.orthographicSize;
float halfW = halfH * Cam.aspect;
var r = Board.Rect;
float minX = r.xMin - Margin + halfW;
float maxX = r.xMax + Margin - halfW;
float minY = r.yMin - BottomMargin + halfH;
float maxY = r.yMax + TopMargin - halfH;
p.x = minX > maxX ? 0f : Mathf.Clamp(p.x, minX, maxX);
p.y = minY > maxY ? 0f : Mathf.Clamp(p.y, minY, maxY);
return p;
}
}
}