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

177 lines
8.5 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 UnityEngine;
using Platformer.Mechanics;
using EerieVillage.Skills;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// 투사체 생성기. IEffector 구현체.
/// SkillFireEvent.Execute 에서 ActiveCategory.Projectile 분기 시 호출.
/// BT12-Dev Phase 2-B §4-4.
///
/// 다중 발사: PlayerStats.ExtraProjectiles 반영 (P08 투사체증폭).
/// 궤적 분기: ActiveSkillData.Trajectory — Line → Projectile, Homing → HomingProjectile.
/// </summary>
public class ProjectileSpawner : IEffector
{
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
{
var data = runtime.ActiveData;
// 플레이어 위치·방향 취득
Transform playerTransform = inventory.transform;
// PlayerController.Facing 참조
Vector2 facing = Vector2.right;
var pc = inventory.GetComponent<PlayerController>();
if (pc != null) facing = pc.Facing;
// PD 지시 2026-05-13 — 게임 시작 시 Player.Facing=(0,0) 영역 fallback (정지·잔존 투사체 차단)
if (facing.sqrMagnitude < 0.01f) facing = Vector2.right;
// PD 정합 2026-05-13 — OffsetDistance.x = facing 방향 거리·OffsetDistance.y = 직각 거리·OffsetXY = 이펙트 절대
Vector2 perpDir = new Vector2(-facing.y, facing.x);
Vector2 spawnPos = (Vector2)playerTransform.position
+ facing * data.OffsetDistance.x
+ perpDir * data.OffsetDistance.y
+ data.OffsetXY;
// 프리팹 로드 (data.ProjectilePrefab 우선·없으면 fallback)
GameObject prefab = LoadProjectilePrefab(data);
// 다중 발사 수 (기본 1 + ExtraProjectiles)
int count = 1 + Mathf.Max(0, inventory.Stats.ExtraProjectiles);
float angleStep = count > 1 ? 10f : 0f;
float startAngle = -angleStep * (count - 1) * 0.5f;
for (int i = 0; i < count; i++)
{
float angle = startAngle + angleStep * i;
Vector2 dir = RotateVector(facing, angle);
// BT12-Dev 2026-05-09 잔존 투사체 fix:
// prefab 부재 시 Scene GameObject를 prefab으로 Instantiate하면 원본 Scene GO가 잔존.
// → fallback 영역 매번 새 GameObject 직접 생성 (Instantiate X·자기 자신 발사체).
GameObject go = prefab != null
? Object.Instantiate(prefab, (Vector3)spawnPos, Quaternion.identity)
: CreateFallbackProjectile(data, (Vector3)spawnPos);
// PD 지시 2026-05-13 — 런타임 spawn 투사체 Scene 저장 회피 (Edit Mode execute 시 잔존 방지)
go.hideFlags = HideFlags.DontSave;
// PD 지시 2026-05-13 — 시각화 ↔ 판정 정합 — BoxCollider2D size = HitboxSize·isTrigger=true.
// FX prefab 영역 기존 Collider2D 있으면 size 만 정합·없으면 신규 BoxCollider2D 부착.
var existingCol = go.GetComponent<Collider2D>();
BoxCollider2D box;
if (existingCol is BoxCollider2D) {
box = (BoxCollider2D)existingCol;
} else {
if (existingCol != null) Object.Destroy(existingCol);
box = go.AddComponent<BoxCollider2D>();
}
box.isTrigger = true;
box.size = data.HitboxSize;
box.offset = Vector2.zero;
Projectile proj;
if (data.Trajectory == ProjectileTrajectory.Homing)
proj = go.GetComponent<HomingProjectile>() ?? go.AddComponent<HomingProjectile>();
else if (data.Trajectory == ProjectileTrajectory.Arc)
// PD 지시 2026-05-13 — A13 천둥발 관통 영역 PiercingProjectile (Arc 영역 영역 영역)
proj = go.GetComponent<PiercingProjectile>() ?? go.AddComponent<PiercingProjectile>();
else
proj = go.GetComponent<Projectile>() ?? go.AddComponent<Projectile>();
proj.Initialize(runtime, inventory, dir);
}
// PD 지시 2026-05-13 — 진단 (회수 의무·잔존 추적)
UnityEngine.Debug.Log($"[ProjectileSpawner] Trigger card={data.CardId} count={count} facing=({facing.x:F2},{facing.y:F2}) t={Time.time:F2}");
}
/// <summary>
/// 투사체 prefab 로드. data.ProjectilePrefab (Phase 2-E 신규 필드) 우선·부재 시 Resources fallback.
/// </summary>
private static GameObject LoadProjectilePrefab(ActiveSkillData data = null)
{
if (data != null && data.ProjectilePrefab != null) return data.ProjectilePrefab;
return Resources.Load<GameObject>("Skills/Projectiles/Default");
}
/// <summary>
/// fallback 발사체 GameObject 직접 생성. Instantiate 영역 X — 자기 자신 발사체.
/// BT12-Dev 2026-05-09 — 이전 LoadProjectilePrefab fallback 영역에서 분리.
/// 원본 Scene GameObject 영구 잔존 버그 근본 해결 (PD 보고 — 맵에 투사체 영구 잔존).
/// PD 지시 2026-05-09 시각화 — 16×16 RGBA32 동적 알파 원 Texture2D + 속성별 색상.
/// </summary>
private static GameObject CreateFallbackProjectile(ActiveSkillData data, Vector3 spawnPos)
{
var go = new GameObject($"Projectile_{data.CardId}");
go.transform.position = spawnPos;
var col = go.AddComponent<CircleCollider2D>();
col.isTrigger = true;
col.radius = 0.2f;
// BT12-Dev 2026-05-09 — Rigidbody2D 부재 유지 (Static Collider).
// Enemy = KinematicObject 상속 → Rigidbody2D Kinematic.
// Static vs Kinematic = OnTriggerEnter2D 발화 정합 (Unity 2D Physics 표준).
// 직전 시도 Kinematic Rigidbody2D 추가는 Kinematic vs Kinematic 영역 OnTriggerEnter2D 발화 X 영역 회귀 → 폐기.
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = GetOrCreateFallbackSprite();
sr.color = GetColorByAttribute(data.AttributeTags);
sr.sortingOrder = 50; // Player·Enemy 위
// 0.4 unit 직경 정도의 작은 원
go.transform.localScale = new Vector3(0.4f, 0.4f, 1f);
return go;
}
// 캐시: Sprite 매번 생성하지 않도록 정적 보존
static Sprite _fallbackSprite;
static Sprite GetOrCreateFallbackSprite()
{
if (_fallbackSprite != null) return _fallbackSprite;
// 16×16 원형 알파 텍스처 (둥근 형태)
const int size = 16;
var tex = new Texture2D(size, size, TextureFormat.RGBA32, false);
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
float r = size * 0.5f;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float dx = x - r + 0.5f;
float dy = y - r + 0.5f;
float dist = Mathf.Sqrt(dx * dx + dy * dy);
float alpha = Mathf.Clamp01(1f - (dist - (r - 1.5f)));
tex.SetPixel(x, y, new Color(1f, 1f, 1f, alpha));
}
}
tex.Apply();
_fallbackSprite = Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), size);
return _fallbackSprite;
}
static Color GetColorByAttribute(AttributeTag attr)
{
// 속성별 색상 (시각 구분 영역)
if ((attr & AttributeTag.Fire) != 0) return new Color(1f, 0.5f, 0.2f);
if ((attr & AttributeTag.Frost) != 0) return new Color(0.5f, 0.85f, 1f);
if ((attr & AttributeTag.Dark) != 0) return new Color(0.6f, 0.3f, 0.85f);
if ((attr & AttributeTag.Lightning) != 0) return new Color(1f, 1f, 0.4f);
if ((attr & AttributeTag.Physical) != 0) return new Color(0.95f, 0.95f, 0.95f);
return Color.white;
}
private static Vector2 RotateVector(Vector2 v, float degrees)
{
float rad = degrees * Mathf.Deg2Rad;
float cos = Mathf.Cos(rad);
float sin = Mathf.Sin(rad);
return new Vector2(v.x * cos - v.y * sin, v.x * sin + v.y * cos);
}
}
}