194 lines
9.9 KiB
C#
194 lines
9.9 KiB
C#
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;
|
||
|
||
// PD 지시 2026-05-13 — 시전 FX (A08 저주의 화살 등) Player 위치 spawn
|
||
// PD 지시 2026-05-13 — 진단 (회수 의무): CastFx·ProjectilePrefab·OnHitFx 영역 매핑 측정
|
||
string castName = data.CastFxPrefab != null ? data.CastFxPrefab.name : "NULL";
|
||
string projName = data.ProjectilePrefab != null ? data.ProjectilePrefab.name : "NULL";
|
||
string hitName = data.OnHitFxPrefab != null ? data.OnHitFxPrefab.name : "NULL";
|
||
UnityEngine.Debug.Log($"[ProjectileSpawner] card={data.CardId} CastFx={castName} ProjPrefab={projName} OnHitFx={hitName}");
|
||
if (data.CastFxPrefab != null)
|
||
{
|
||
var castFx = Object.Instantiate(data.CastFxPrefab, playerTransform.position, Quaternion.Euler(0f, 0f, data.FxRotation));
|
||
castFx.hideFlags = HideFlags.DontSave;
|
||
castFx.transform.localScale *= data.HitFxScale;
|
||
// PD 지시 2026-05-13 — ParticleSystem 명시 Play
|
||
foreach (var ps in castFx.GetComponentsInChildren<ParticleSystem>(true)) ps.Play(true);
|
||
FxAutoDestroyUnscaled.Attach(castFx, 2f);
|
||
UnityEngine.Debug.Log($"[ProjectileSpawner] CastFx spawned name={castFx.name} pos=({castFx.transform.position.x:F2},{castFx.transform.position.y:F2})");
|
||
}
|
||
|
||
// 프리팹 로드 (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 — ParticleSystem 명시 Play (ProjectilePrefab 영역 자체 FX·A08 FX_PinkMagicArrow 등)
|
||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) ps.Play(true);
|
||
|
||
// 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);
|
||
}
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
}
|
||
}
|