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; // 페이드 보간 기준 (= _originalScale × ProjectileFxScale·매 frame 갱신) protected Vector3 _originalScale; // PD 정합 2026-05-13 — Inspector ProjFxScale 변경 영역 매 frame 영역 영역 base 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 — 투사체 root = 박스(판정) 정합. FxRotation 미적용 (시각 전용·박스 회전 금지). 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; // PD 지시 2026-05-13 — Time.timeScale=0 (LevelUp 카드 선택) 영역 Time.time 정지 → unscaledTime 영역 lifetime check 정합 _spawnTime = Time.unscaledTime; // PD 지시 2026-05-13 — 투사체 사정거리·속도 Inspector 직접 조절 (RangeTier·camWidth·mults 계산 폐기) _maxRange = (_data.MaxRange > 0.01f) ? _data.MaxRange : 10f; _speed = (_data.ProjectileSpeed > 0.01f) ? _data.ProjectileSpeed : 6f; // PD 정합 2026-05-13 — Inspector ProjFxScale 매 frame 영역 영역 _originalScale 저장 _originalScale = transform.localScale; transform.localScale *= _data.ProjectileFxScale; _baseScale = transform.localScale; // PD 지시 2026-05-13 — 판정 영역 시각화 (자체 Collider2D bounds 영역 자식 박스·이동·페이드 정합) SpawnHitboxDebugChild(); // PD 지시 2026-05-13 — 투사체 사거리 파란 박스 시각화 (발사 후 3초 유지) HitboxDebug.SpawnRange(_spawnPosition, _direction, _maxRange, 3f); // 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; } // PD 지시 2026-05-13 — Invoke 폐기 (Time.timeScale=0 영역 호출 X·영구 잔존 원인)·unscaledTime backup 영역 lifetime 영역 보장 // CancelInvoke 호출 영역 추가 안전 (이전 영역 영역 Invoke 영역 잔존 차단) CancelInvoke(); // PD 지시 2026-05-13 — 진단 (회수 의무·잔존 추적) Debug.Log($"[Projectile][Initialize] name={name} dir=({_direction.x:F2},{_direction.y:F2}) speed={_speed:F2} maxRange={_maxRange:F2} lifetime={_lifetime:F2} t={Time.unscaledTime:F2}"); } // 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() { // PD 지시 2026-05-13 — Initialize 호출 이전 Update 발화 차단·_data null 시 즉시 SelfDestruct (잔존 차단) if (_data == null) { SelfDestruct(); return; } // PD 지시 2026-05-13 — unscaledTime 영역 lifetime check (Time.timeScale=0 영역 Time.time 정지 영역 회피·재시작 시 영구 잔존 차단) if (_spawnTime > 0f && Time.unscaledTime - _spawnTime > _lifetime) { Debug.Log($"[Projectile][LifetimeForce] name={name} spawn={_spawnTime:F2} now={Time.unscaledTime:F2} lifetime={_lifetime:F2}"); SelfDestruct(); return; } // PD 지시 2026-05-13 — Inspector HitboxSize 변경 즉시 반영 SyncHitboxToData(); 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 회피·PD 지시 2026-05-13 unscaledTime 정합. if (Time.unscaledTime - _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) { // PD 지시 2026-05-13 — Initialize 호출 이전 OnTriggerEnter2D 발화 영역 NullReferenceException 차단 // ProjectileSpawner.Trigger 영역 collider 부착 후 Initialize 호출 영역 race 영역 영역 발화 가능 if (_runtime == null || _data == null) return; if (_hitTargets.Contains(other)) return; // PD 지시 2026-05-13 — 투사체끼리 충돌 X·통과 정합 (Projectile 컴포넌트 동족 skip·Wall 판정 이전) if (other.GetComponent() != null) 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); // PD 지시 2026-05-13 — 피격 이펙트 spawn·HitFxScale·FxRotation 적용 if (_data != null && _data.OnHitFxPrefab != null) { var fx = Object.Instantiate(_data.OnHitFxPrefab, other.transform.position, Quaternion.Euler(0f, 0f, _data.FxRotation)); fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피 fx.transform.localScale *= _data.HitFxScale; 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() { // PD 지시 2026-05-13 — 투사체 잔상 진단 (회수 의무) Debug.Log($"[Projectile][SelfDestruct] name={name} pos=({transform.position.x:F2},{transform.position.y:F2}) t={Time.time:F2}"); CancelInvoke(nameof(SelfDestruct)); Destroy(gameObject); } // PD 지시 2026-05-13 — 투사체 잔상 진단 (회수 의무·Destroy 호출 외 경로 영역 영역 영역 검출) void OnDestroy() { Debug.Log($"[Projectile][OnDestroy] name={name} t={Time.time:F2}"); } // PD 지시 2026-05-13 — 시각화 박스 자식 reference (Update 영역 매 frame Inspector 정합 갱신용) protected Transform _debugBoxTransform; // PD 지시 2026-05-13 — Projectile 자체 Collider2D bounds 영역 자식 박스 부착 (이동·페이드 정합·HitboxSize 정합) void SpawnHitboxDebugChild() { var col = GetComponent(); if (col == null) return; Vector2 size = (col is BoxCollider2D box) ? box.size : new Vector2(col.bounds.size.x / Mathf.Max(0.01f, Mathf.Abs(transform.lossyScale.x)), col.bounds.size.y / Mathf.Max(0.01f, Mathf.Abs(transform.lossyScale.y))); Vector2 offset = (col is BoxCollider2D box2) ? box2.offset : (col is CircleCollider2D cc ? cc.offset : Vector2.zero); var go = new GameObject("ProjectileHitbox_Debug"); // PD 지시 2026-05-13 — 런타임 spawn 박스 Scene 저장 회피 go.hideFlags = HideFlags.DontSave; go.transform.SetParent(transform, false); go.transform.localPosition = new Vector3(offset.x, offset.y, 0f); go.transform.localScale = new Vector3(size.x, size.y, 1f); var sr = go.AddComponent(); sr.sprite = HitboxDebug.GetWhiteSprite(); sr.color = new Color(1f, 0f, 0f, 0.35f); sr.sortingOrder = 100; sr.enabled = HitboxDebug.ShowDebugVisuals; _debugBoxTransform = go.transform; } // PD 지시 2026-05-13 — Inspector 영역 HitboxSize·ProjFxScale 변경 시 발사 중인 Projectile 도 즉시 반영. protected void SyncHitboxToData() { if (_data == null) return; // HitboxSize 영역 BoxCollider2D + 자식 박스 정합 var box = GetComponent(); if (box != null && box.size != _data.HitboxSize) { box.size = _data.HitboxSize; } if (_debugBoxTransform != null) { var s = _debugBoxTransform.localScale; if (Mathf.Abs(s.x - _data.HitboxSize.x) > 0.001f || Mathf.Abs(s.y - _data.HitboxSize.y) > 0.001f) { _debugBoxTransform.localScale = new Vector3(_data.HitboxSize.x, _data.HitboxSize.y, 1f); } } // PD 정합 — ProjectileFxScale 영역 _baseScale 영역 매 frame 영역 (Inspector 변경 영역 즉시) _baseScale = _originalScale * _data.ProjectileFxScale; } // 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; } // PD 지시 2026-05-13 — FX 잔상 safety cap 5초 Object.Destroy(fxGo, Mathf.Min(lifetime, 5f)); } } }