using UnityEngine; namespace TKS { /// 지형 기믹 질의(PD #1097) — 장애물 콜라이더 · 투사체 pass/block/destroy · 안개 · 기믹 피해 공식. public static partial class Combat { /// true 인 동안 투사체가 장애물 기믹을 통과(황충 노익장 · 스킬 관통 사격 · 호출 쪽이 켜고 끔). public static bool ArrowIgnoreGimmicks; /// 전장 가장자리 벽까지만(기믹·유닛 무시) 나아갈 수 있는 거리. public static float WallTravel(Unit actor, Vector2 origin, Vector2 dir, float dist) { float min = dist; foreach (var h in Cast(actor, origin, dir, dist)) { if (h.collider.GetComponent() != null) min = Mathf.Min(min, h.distance); } return Mathf.Max(0f, min - Epsilon); } /// 경로(원 캐스트) 위 첫 장애물 기믹. hitDist = 닿기까지 중심 이동 거리 · normal = 닿은 면의 바깥 방향(튕김 방향). public static GimmickInstance FirstGimmickOnPath(Unit actor, Vector2 origin, Vector2 dir, float dist, out float hitDist, out Vector2 normal) { hitDist = 0f; normal = -dir; if (GimmickLayer.Current == null || !GimmickLayer.Current.HasAny) return null; foreach (var h in Cast(actor, origin, dir, dist)) { var g = GimmickLayer.InstOf(h.collider); if (g == null || !g.BlocksMove) continue; hitDist = h.distance; normal = h.normal.sqrMagnitude > 1e-6f ? h.normal.normalized : -dir; if (Vector2.Dot(normal, dir) > 0f) normal = -dir; // 출발부터 겹친 경우(법선 미정) return g; } return null; } /// /// 투사체(PD 2026-11-05 #1087 ⑥ + 기믹 #1097): 첫 유닛 또는 막는 기믹. 목책·강 = 통과(pass) · 덤불·바위·성문·성벽 = 막힘(gimmick 에 담고 null) · /// 폭약 수레 = 맞으면 파괴(gimmick 에 담고 null) · 안개 안 유닛도 정상 피격(피해만 ×p1 · PD 2026-11-06 개정). /// public static Unit FirstUnitOnArrow(Unit shooter, Vector2 from, Vector2 dir, float dist, float arrowRadius, Faction? passFaction, out float travel, out GimmickInstance gimmick) { travel = dist; gimmick = null; Physics2D.SyncTransforms(); var hits = Physics2D.CircleCastAll(from, arrowRadius, dir, dist); System.Array.Sort(hits, (a, b) => a.distance.CompareTo(b.distance)); foreach (var h in hits) { if (h.collider.GetComponent() != null) { travel = h.distance; return null; } var g = GimmickLayer.InstOf(h.collider); if (g != null) { if (!g.BlocksMove || g.Def.projectile == GimmickProjectile.Pass || ArrowIgnoreGimmicks) continue; // 황충 노익장 = 장애물 통과(2026-11-10 #1112) travel = h.distance; gimmick = g; return null; } var u = UnitOf(h.collider); if (u == null || u == shooter || !u.IsAlive) continue; if (passFaction.HasValue && u.Faction == passFaction.Value) continue; travel = h.distance; return u; } return null; } /// /// 기믹 피해(바위 굴림 · 폭약 폭발) = (공격 × 스킬 공 배율 × 군기 × mult − 방어 × defenseFactor) × 난수 · 상성·크리 없음(「공 ×p」 · 해석 = PD 확인 대상). /// public static int GimmickDamage(BalanceConfig cfg, Unit attacker, Unit defender, float mult, System.Random rng) { if (attacker == null || defender == null) return 0; float atk = attacker.Atk * SkillMods.AttackMult(attacker, true) * GimmickLayer.AttackMultOf(attacker); float raw = atk * mult - defender.Def * cfg.defenseFactor; return Mathf.Max(1, Mathf.RoundToInt(raw * Variance(cfg, rng))); } /// 피해 난수(가상 실행 = 분산 0 · 적 AI 예측 · PD #1098). public static float Variance(BalanceConfig cfg, System.Random rng) { if (Sim.Active || rng == null) return 1f; return 1f + ((float)rng.NextDouble() * 2f - 1f) * cfg.damageVariance; } /// 함정 정지(숨은 함정에 중심이 들면 그 자리) — FreeTravel 이 쓴다. static float TrapStop(Unit actor, Vector2 origin, Vector2 dir, float dist) { var l = GimmickLayer.Current; return l == null || !l.HasAny ? dist : l.TrapStop(actor, origin, dir, dist); } } }