EerieVillage/Assets/Scripts/Skills/Effectors/Projectile.cs

325 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
using Platformer.Mechanics;
using Platformer.Gameplay;
using static Platformer.Core.Simulation;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// 투사체 기본 컴포넌트. Line 궤적 직선 이동·단일 적 타격 후 소멸.
/// BT12-Dev Phase 2-B §4-2.
/// 파생: <see cref="HomingProjectile"/> (A15 추적 화염구).
/// </summary>
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<Collider2D> _hitTargets = new HashSet<Collider2D>();
// 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;
/// <summary>
/// ProjectileSpawner.Trigger 에서 Instantiate 직후 호출.
/// </summary>
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 미적용 (시각 전용·박스 회전 금지).
// ProjectileAngleOffset = sprite 기본 방향 보정 (예: A08 FX_PinkMagicArrow sprite left → 180)
float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg + _data.ProjectileAngleOffset;
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<Renderer>();
_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();
}
// 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)
{
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<Projectile>() != null) return;
// PD 지시 2026-05-09 후속 방어 — 자기(Player) hit·자기 자신·hit 방어.
if (other.GetComponent<PlayerController>() != null) return;
// Enemy 레이어 한정.
int enemyLayer = LayerMask.NameToLayer("Enemy");
bool isEnemy = (enemyLayer != -1 && other.gameObject.layer == enemyLayer)
|| other.GetComponent<EnemyController>() != null;
if (isEnemy)
{
var health = other.GetComponent<Health>();
// 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;
// PD 지시 2026-05-13 — ParticleSystem 명시 Play
foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(true)) ps.Play(true);
AutoDestroyOnParticleEnd(fx);
// PD 지시 2026-05-13 — 진단 (회수 의무)
Debug.Log($"[Projectile] OnHit card={_data.CardId} OnHitFx spawned name={fx.name} pos=({fx.transform.position.x:F2},{fx.transform.position.y:F2})");
}
// 부가 효과 (DoT·Stun·Slow·DebuffStack) — StatusApplier 위임
var enemy = other.GetComponent<EnemyController>();
if (enemy != null)
{
StatusApplier.Apply(_data, enemy);
}
// Enemy 즉사 시 EnemyDeath 체인 발동
if (!health.IsAlive && enemy != null)
{
Schedule<EnemyDeath>().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);
}
// PD 지시 2026-05-13 — 시각화 박스 자식 reference (Update 영역 매 frame Inspector 정합 갱신용)
protected Transform _debugBoxTransform;
// PD 지시 2026-05-13 — Projectile 자체 Collider2D bounds 영역 자식 박스 부착 (이동·페이드 정합·HitboxSize 정합)
void SpawnHitboxDebugChild()
{
var col = GetComponent<Collider2D>();
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<SpriteRenderer>();
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<BoxCollider2D>();
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<ParticleSystem>();
if (ps == null) ps = fxGo.GetComponentInChildren<ParticleSystem>();
float lifetime = 3f; // fallback
if (ps != null)
{
var main = ps.main;
lifetime = main.duration + main.startLifetime.constantMax + 0.2f;
}
// PD 지시 2026-05-13 — unscaledTime cap (Time.timeScale=0 영역 잔존 차단)
FxAutoDestroyUnscaled.Attach(fxGo, Mathf.Min(lifetime, 5f));
}
}
}