using System.Collections.Generic; using UnityEngine; using Platformer.Mechanics; using Platformer.Gameplay; using static Platformer.Core.Simulation; namespace EerieVillage.Skills.Effectors { /// /// 투사체 기본 컴포넌트. Line 궤적 직선 이동·단일 적 타격 후 소멸. /// BT12-Dev Phase 2-B §4-2. /// 파생: (A15 추적 화염구). /// public class Projectile : MonoBehaviour { protected ActiveSkillData _data; protected ActiveSkillRuntime _runtime; protected PlayerSkillInventory _inventory; protected Vector2 _direction; protected float _speed = 6f; // BT12-Dev 2026-05-10 PD — 거리 차이 체감 영역 영역 (12 → 6) protected float _lifetime = 5f; // 영역 영역 영역 (Long 18.67 / 6 = 3.11s 영역 영역) // BT12-Dev 2026-05-10 (PD #1·#2) — 거리 제한·벽 충돌 영역 protected Vector2 _spawnPosition; protected float _maxRange; protected float _spawnTime; // 동일 투사체로 동일 Collider 중복 타격 방지 protected readonly HashSet _hitTargets = new HashSet(); // BT12-Dev 2026-05-13 — 페이드아웃·축소 (PD 지시: 사거리 80%~100% 영역 alpha 0·scale 추가 50% 축소) protected Vector3 _baseScale; // 50% 축소 후 시작 scale (페이드 보간 기준) protected Renderer[] _renderers; protected MaterialPropertyBlock _mpb; protected float[] _baseAlphas; const float FADE_START_RATIO = 0.8f; /// /// ProjectileSpawner.Trigger 에서 Instantiate 직후 호출. /// public virtual void Initialize(ActiveSkillRuntime runtime, PlayerSkillInventory inventory, Vector2 direction) { _runtime = runtime; _data = runtime.ActiveData; _inventory = inventory; _direction = direction.normalized; _hitTargets.Clear(); // PD 지시 2026-05-13 — 투사체 방향 정렬 (좌·우·각도) float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.Euler(0f, 0f, angle); // BT12-Dev 2026-05-10 (PD #1) — 거리 제한 영역 영역 spawn 위치 저장 _spawnPosition = transform.position; _spawnTime = Time.time; // BT12-Dev 2026-05-10 PD — 사정거리 5단계 (Camera 가로 배수) float camWidth = 12.44f; // fallback (ortho size 3.5·16:9) var cam = Camera.main; if (cam != null && cam.orthographic) { float aspect = (cam.aspect > 0.01f) ? cam.aspect : (16f / 9f); camWidth = cam.orthographicSize * 2f * aspect; } // BT12-Dev 2026-05-10 Camera ortho 3.5→5.0 정합 정정 (camWidth 12.44→17.78·1.43배). // 기존 maxRange 동등 유지 위해 mults 1/1.43 비례 축소. float[] mults = { 0.14f, 0.35f, 0.467f, 0.7f, 1.05f }; int idx = Mathf.Clamp((int)_data.Range, 0, mults.Length - 1); _maxRange = camWidth * mults[idx]; // BT12-Dev 2026-05-13 — 기본 크기 50% 축소 + 페이드 시작 scale 저장 (PD 지시) transform.localScale *= 0.5f; _baseScale = transform.localScale; // Renderer·MaterialPropertyBlock 캐싱 + 기본 alpha 저장 _renderers = GetComponentsInChildren(); _mpb = new MaterialPropertyBlock(); _baseAlphas = new float[_renderers.Length]; for (int i = 0; i < _renderers.Length; i++) { var r = _renderers[i]; if (r.sharedMaterial != null && r.sharedMaterial.HasProperty("_TintColor")) _baseAlphas[i] = r.sharedMaterial.GetColor("_TintColor").a; else if (r.sharedMaterial != null && r.sharedMaterial.HasProperty("_Color")) _baseAlphas[i] = r.sharedMaterial.GetColor("_Color").a; else _baseAlphas[i] = 1f; } // Phase 2-B: 풀링 미도입 — Invoke 기반 자동 소멸 (거리 제한 영역 영역 영역 영역 영역 안전망) Invoke(nameof(SelfDestruct), _lifetime); } // BT12-Dev 2026-05-13 — 사거리 80~100% 영역 scale·alpha 보간 (PD 지시) // ratio < 0.8 : scale=1.0배 (시작 50% 그대로)·alpha 그대로 // ratio 0.8~1.0 : t=(ratio-0.8)/0.2 → scale = base × (1 - 0.5t)·alpha = base × (1 - t) protected void ApplyFadeoutByRange(float ratio) { if (_renderers == null) return; float t = Mathf.Clamp01((ratio - FADE_START_RATIO) / (1f - FADE_START_RATIO)); float scaleMul = 1f - 0.5f * t; float alphaMul = 1f - t; transform.localScale = _baseScale * scaleMul; for (int i = 0; i < _renderers.Length; i++) { var r = _renderers[i]; if (r == null) continue; r.GetPropertyBlock(_mpb); string propName = (r.sharedMaterial != null && r.sharedMaterial.HasProperty("_TintColor")) ? "_TintColor" : "_Color"; Color baseCol = (r.sharedMaterial != null && r.sharedMaterial.HasProperty(propName)) ? r.sharedMaterial.GetColor(propName) : Color.white; baseCol.a = _baseAlphas[i] * alphaMul; _mpb.SetColor(propName, baseCol); r.SetPropertyBlock(_mpb); } } // BT12-Dev 2026-05-10 회귀 정정 — Wall Layer 실측 (Player Layer=13·Enemy Layer=14·Level Tilemap Layer=0). // Layer 0 (Default) Solid Collider: Level TilemapCollider2D + GameObject·Alien BoxCollider2D — Wall 정합. // Trigger collider (CinemachineConfiner Polygon·Token·DeathZone Box) = useTriggers=false 영역 자동 제외. // Player·Enemy 영역 Layer 0 외 영역 → OverlapPoint hit 무관. protected static readonly int WallLayerMask = (1 << 0); protected virtual void Update() { transform.position += (Vector3)(_direction * _speed * Time.deltaTime); // BT12-Dev 2026-05-10 (PD #1) — 거리 제한 영역 영역 SelfDestruct float dist = Vector2.Distance(transform.position, _spawnPosition); if (dist >= _maxRange) { SelfDestruct(); return; } // BT12-Dev 2026-05-13 — 사거리 80~100% 영역 페이드아웃·축소 (PD 지시) if (_maxRange > 0.01f) { ApplyFadeoutByRange(dist / _maxRange); } // BT12-Dev 2026-05-10 (PD #2 fix·재발 정정 #2) — Wall OverlapPoint·useTriggers=false (CinemachineConfiner Trigger 영역 영역 영역). // grace period 0.05s 영역 spawn 시점 즉시 SelfDestruct 회피. if (Time.time - _spawnTime > 0.05f) { var filter = new ContactFilter2D(); filter.useTriggers = false; // Trigger collider (CinemachineConfiner 영역) 영역 영역 filter.useLayerMask = true; filter.layerMask = WallLayerMask; var results = new Collider2D[1]; int hitCount = Physics2D.OverlapPoint(transform.position, filter, results); if (hitCount > 0) { SelfDestruct(); } } } protected virtual void OnTriggerEnter2D(Collider2D other) { if (_hitTargets.Contains(other)) return; // PD 지시 2026-05-09 후속 방어 — 자기(Player) hit·자기 자신·hit 방어. if (other.GetComponent() != null) return; // Enemy 레이어 한정. int enemyLayer = LayerMask.NameToLayer("Enemy"); bool isEnemy = (enemyLayer != -1 && other.gameObject.layer == enemyLayer) || other.GetComponent() != null; if (isEnemy) { var health = other.GetComponent(); // PD 지시 2026-05-13 — 죽은 Enemy 와 충돌 시도해도 Projectile 영역 SelfDestruct (영역 영역 통과 회피) if (health == null || !health.IsAlive) { SelfDestruct(); return; } _hitTargets.Add(other); // 유효 대미지 산출 — BT12-Dev 2026-05-10 임시 (PD 지시): 기본 공격력 5 하한 강제. int damage = Mathf.Max(_runtime.CalculateEffectiveDamage(), 5); // 피해 적용 health.Decrement(damage); // BT12-Dev 2026-05-13 — 피격 이펙트 spawn (OnHitFxPrefab 영역·적 위치·자동 destroy) // PD 지시 2026-05-13 — 피격 이펙트 크기 50% 축소 if (_data != null && _data.OnHitFxPrefab != null) { var fx = Object.Instantiate(_data.OnHitFxPrefab, other.transform.position, Quaternion.identity); fx.transform.localScale *= 0.5f; AutoDestroyOnParticleEnd(fx); } // 부가 효과 (DoT·Stun·Slow·DebuffStack) — StatusApplier 위임 var enemy = other.GetComponent(); if (enemy != null) { StatusApplier.Apply(_data, enemy); } // Enemy 즉사 시 EnemyDeath 체인 발동 if (!health.IsAlive && enemy != null) { Schedule().enemy = enemy; } // 단일 적 타격 후 소멸 (관통 미지원 — Phase 2 범위 내) SelfDestruct(); return; } // BT12-Dev 2026-05-10 (PD #2) — 벽 충돌 시 SelfDestruct. // Layer 0 (Default·Ground) · Layer 16 (Foreground·발판) 영역 영역 Tilemap·Composite·Box collider 영역 정합. // 레이저 영역 영역 영역 영역 영역 X — 본 Projectile 영역 영역 (영역 영역 영역 영역 X) — 모든 Projectile 영역 SelfDestruct. int otherLayer = other.gameObject.layer; bool isWall = (otherLayer == 0 || otherLayer == 16); if (isWall) { SelfDestruct(); } } protected void SelfDestruct() { CancelInvoke(nameof(SelfDestruct)); Destroy(gameObject); } // BT12-Dev 2026-05-13 — ParticleSystem 영역 자동 destroy. main.duration + startLifetime.constantMax 영역 영역 후 Destroy. protected static void AutoDestroyOnParticleEnd(GameObject fxGo) { if (fxGo == null) return; var ps = fxGo.GetComponent(); if (ps == null) ps = fxGo.GetComponentInChildren(); float lifetime = 3f; // fallback if (ps != null) { var main = ps.main; lifetime = main.duration + main.startLifetime.constantMax + 0.2f; } Object.Destroy(fxGo, lifetime); } } }