using CodeStage.AntiCheat.ObscuredTypes; using System.Collections.Generic; using UnityEngine; public class Projectile : MonoBehaviour { public float m_Speed = 12f; public float radius = 0.1f; Vector2 dir; Vector2 prevPos; ObscuredInt m_ReflectCount; float m_LifeTime; ProjectileData m_ProjectileData; #region 투사체 매니저 ProjectileMgr owner; string prefabKey; public void SetOwner(ProjectileMgr mgr, string key) { owner = mgr; prefabKey = key; } public string GetPoolKey() { return prefabKey; } void Kill() { owner.Return(this); // 이펙트 표시 필요 } public ProjectileTableData Get_ProjectileTData() { return m_ProjectileData.m_Data; } #endregion public void Set(ProjectileData pd) { m_ProjectileData = pd; m_LifeTime = m_ProjectileData.m_Data.n_ProjectileLife; if (!m_ProjectileData.IsPC) m_ProjectileData.Ignore_WallHP = false; Vector3 pos = pd.tf_Start.position; Quaternion rot = pd.tf_Start.rotation; /* ============================= * 직선 화살 위치 분산 * ============================= */ if (pd.PreviousArrow > 0) { int count = pd.PreviousArrow + 1; float spacing = 0.5f; float offset = (pd.LineIndex - (count - 1) * 0.5f) * spacing; pos += pd.tf_Start.right * offset; } transform.SetPositionAndRotation(pos, rot); /* ============================= * 사선 화살 각도 분산 * ============================= */ float angle = 0f; if (pd.DiagonalArrow > 0 && pd.DiagIndex > 0) { int n = pd.DiagonalArrow; // 전체 90도 안에서 분산 float step = 22.5f / (n + 1); int index = pd.DiagIndex; // 1부터 int tier = (index + 1) / 2; // 1,1,2,2,3,3... float offset = step * tier; bool right = (index % 2) == 0; angle += right ? offset : -offset; } transform.rotation = Quaternion.AngleAxis(angle, transform.forward) * transform.rotation; if (!pd.IsPC) transform.eulerAngles += new Vector3(0f, 0f, 180f); dir = transform.up.normalized; m_ReflectCount = m_ProjectileData.m_Data.n_AttackBounceLimit; if (pd.IsPC) m_ReflectCount += (int)IngameMgr.Ins.Get_SkillValue(eSkillType.Reflect); if (m_ProjectileData.m_Data.n_ProjectileID == 3002) { transform.eulerAngles = Vector3.zero; var lv = IngameMgr.Ins.Get_SkillLv(eSkillType.Explosion); var skillTdata = table_skill.Ins.Get_Data(eSkillType.Explosion); var scale = skillTdata.f_ExplosionScale + (skillTdata.f_ExplosionScalePerLv * (lv - 1)); transform.localScale = Vector3.one * scale; m_ReflectCount = 0; } m_ReflectCount.RandomizeCryptoKey(); } void Update() { if (m_LifeTime > 0) { m_LifeTime -= Time.deltaTime; if (m_LifeTime <= 0f) { Kill(); return; } } if (m_ProjectileData.m_Data.n_ProjectileID == 3002 || m_ProjectileData.m_Data.n_ProjectileID == 3301) return; prevPos = transform.position; float moveDist = m_Speed * Time.deltaTime; transform.position += (Vector3)(dir * moveDist); } private void OnTriggerEnter2D(Collider2D collision) { if (!m_ProjectileData.IsPC && collision.tag == "Mob") return; if (m_ProjectileData.Ignore_WallHP && collision.tag == "Wall_HP") { m_ProjectileData.Ignore_WallHP = false; return; } Check_Hit(collision); if (m_ProjectileData.m_Data.n_ProjectileID == 3002 || m_ProjectileData.m_Data.n_ProjectileID == 3301) return; if (m_ProjectileData.Pierce > 0) { --m_ProjectileData.Pierce; return; } if (m_ReflectCount <= 0) { Kill(); return; } Vector2 currentPos = transform.position; Vector2 moveVec = currentPos - prevPos; float distance = moveVec.magnitude; Vector2 moveDir = distance > 0 ? moveVec / distance : dir; // CircleCast로 정확한 충돌 지점(centroid)과 법선 탐색 RaycastHit2D[] hits = Physics2D.CircleCastAll(prevPos, radius, moveDir, distance); RaycastHit2D hit = new RaycastHit2D(); bool found = false; foreach (var h in hits) { if (h.collider == collision) { // 자기 자신(시작 위치에서 걸리는 경우) 제외 로직이 필요할 수 있으나, // distance > 0 체크 등으로 보완. 여기서는 가장 먼저 닿는 지점을 찾음. hit = h; found = true; break; } } Vector2 normal; Vector2 targetPos; if (found) { normal = hit.normal; // CircleCast의 centroid는 충돌 시의 원의 중심 위치입니다. targetPos = hit.centroid; } else { // fallback: ClosestPoint Vector2 closestRef = collision.ClosestPoint(prevPos); normal = (prevPos - closestRef).normalized; if (normal == Vector2.zero) { normal = (prevPos - (Vector2)collision.bounds.center).normalized; } targetPos = closestRef + normal * radius; } // 반사 dir = Vector2.Reflect(dir, normal).normalized; m_ReflectCount--; m_ReflectCount.RandomizeCryptoKey(); // 회전 float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f; transform.rotation = Quaternion.Euler(0, 0, angle); // 위치 보정 transform.position = targetPos; } void Check_Hit(Collider2D collision) { if (m_ProjectileData.IsPC) { switch(collision.tag) { case "Mob": Show_Hit_Effect(); collision.GetComponent().Get_Dmg(m_ProjectileData); Spawn_LeftRight(collision.transform); Spawn_UpDown(collision.transform); Spawn_Bounce(collision.GetComponent()); break; case "MobShield": Show_Hit_Effect(); collision.GetComponent().Get_Dmg(); break; } } else { if (collision.tag == "Wall_HP") { Show_Hit_Effect(); IngameMgr.Ins.Get_Dmg(m_ProjectileData.Dmg); } } } void Show_Hit_Effect() { if (!string.IsNullOrEmpty(m_ProjectileData.m_Data.s_HitEffect)) EffectMgr.Ins.Show_Effect(m_ProjectileData.m_Data.s_HitEffect, transform.position); } public void SetDirection(Vector2 direction) { dir = direction.normalized; // 방향에 맞게 회전도 맞춰줌 float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg - 90f; transform.rotation = Quaternion.Euler(0, 0, angle); } List GetOffsets(int count, float spacing) { List offsets = new List(); float start = -(count - 1) * 0.5f; for (int i = 0; i < count; i++) offsets.Add((start + i) * spacing); return offsets; } void SpawnExtra(Vector3 pos, Vector3 dir) { var p = ProjectileMgr.Ins.Get(m_ProjectileData.m_Data.s_ProjectilePrefabs); p.Set(new ProjectileData { IsPC = m_ProjectileData.IsPC, m_Data = m_ProjectileData.m_Data, tf_Start = transform, Dmg = m_ProjectileData.Dmg, Ignore_WallHP = false, ArrowLeftRight = 0, ArrowUpDown = 0, Bounce = m_ProjectileData.Bounce, Pierce = m_ProjectileData.Pierce, }); p.transform.position = pos; p.SetDirection(dir); } void Spawn_LeftRight(Transform tf) { if (m_ProjectileData.ArrowLeftRight <= 0) return; Vector3 hitPos = tf.position; float spacing = 0.5f; int count = m_ProjectileData.ArrowLeftRight; var offsets = GetOffsets(count, spacing); foreach (float offset in offsets) { // ▶ 오른쪽 SpawnExtra(hitPos + Vector3.up * (offset + 0.5f), Vector2.right); // ◀ 왼쪽 SpawnExtra(hitPos + Vector3.up * (offset + 0.5f), Vector2.left); } } void Spawn_UpDown(Transform tf) { if (m_ProjectileData.ArrowUpDown <= 0) return; Vector3 hitPos = tf.position; float spacing = 0.5f; int count = m_ProjectileData.ArrowUpDown; var offsets = GetOffsets(count, spacing); foreach (float offset in offsets) { // ▲ 위 SpawnExtra(hitPos + Vector3.right * offset + Vector3.up * 1.5f, Vector2.up); // ▼ 아래 SpawnExtra(hitPos + Vector3.right * offset + Vector3.down * 0.5f, Vector2.down); } } void Spawn_Bounce(MobActor mob) { if (m_ProjectileData.Bounce <= 0 || mob == null) return; MobActor target = IngameMgr.Ins.FindNearestMob(mob.transform.position, mob); if (target == null) { Kill(); return; } --m_ProjectileData.Bounce; SpawnExtra(mob.transform.position, target.transform.position - mob.transform.position); } } public class ProjectileData { public ProjectileTableData m_Data; public Transform tf_Start; ObscuredBool _IsPC; public bool IsPC { get { return _IsPC; } set { _IsPC = value; _IsPC.RandomizeCryptoKey(); } } ObscuredInt _Lv; public int Lv { get { return _Lv; } set { _Lv = value; _Lv.RandomizeCryptoKey(); } } ObscuredInt _Dmg; public int Dmg { get { return _Dmg; } set { _Dmg = value; _Dmg.RandomizeCryptoKey(); } } public bool Ignore_WallHP; // 발사 계열 public int PreviousArrow; public int DiagonalArrow; public int LineIndex; public int DiagIndex; // 피격 계열 public int ArrowLeftRight; public int ArrowUpDown; public int Bounce; public int Pierce; }