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

248 lines
11 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; // 50% 축소 후 시작 scale (페이드 보간 기준)
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 — 투사체 방향 정렬 (좌·우·각도)
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<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;
}
// 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<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);
// 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<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);
}
// 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;
}
Object.Destroy(fxGo, lifetime);
}
}
}