using UnityEngine; using Platformer.Mechanics; using EerieVillage.Skills; namespace EerieVillage.Skills.Effectors { /// /// 투사체 생성기. IEffector 구현체. /// SkillFireEvent.Execute 에서 ActiveCategory.Projectile 분기 시 호출. /// BT12-Dev Phase 2-B §4-4. /// /// 다중 발사: PlayerStats.ExtraProjectiles 반영 (P08 투사체증폭). /// 궤적 분기: ActiveSkillData.Trajectory — Line → Projectile, Homing → HomingProjectile. /// public class ProjectileSpawner : IEffector { public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory) { var data = runtime.ActiveData; // 플레이어 위치·방향 취득 Transform playerTransform = inventory.transform; Vector2 spawnPos = playerTransform.position; // PlayerController.Facing 참조 Vector2 facing = Vector2.right; var pc = inventory.GetComponent(); if (pc != null) facing = pc.Facing; // 프리팹 로드 GameObject prefab = LoadProjectilePrefab(data); if (prefab == null) { Debug.LogWarning($"[ProjectileSpawner] 투사체 프리팹 로드 실패 (data.id={data.CardId})"); return; } // 다중 발사 수 (기본 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); var go = Object.Instantiate(prefab, (Vector3)spawnPos, Quaternion.identity); Projectile proj; if (data.Trajectory == ProjectileTrajectory.Homing) proj = go.GetComponent() ?? go.AddComponent(); else proj = go.GetComponent() ?? go.AddComponent(); proj.Initialize(runtime, inventory, dir); } } /// /// 투사체 프리팹 로드. /// Phase 2-C 에서 data.projectilePrefab 필드를 추가하면 해당 경로 우선 사용. /// 현재는 Resources/Skills/Projectiles/Default 폴백. /// 폴백 실패 시 Collider2D + SpriteRenderer 부착 빈 오브젝트를 반환한다. /// PD 지시 2026-05-09 — 시각화: 흰색 원 sprite (Unity 기본 whiteTexture 사용). /// private static GameObject LoadProjectilePrefab(ActiveSkillData data) { var prefab = Resources.Load("Skills/Projectiles/Default"); if (prefab != null) return prefab; // 폴백 — Collider + SpriteRenderer (시각 표시·판정 동시) var go = new GameObject($"Projectile_{data.CardId}"); var col = go.AddComponent(); col.isTrigger = true; col.radius = 0.2f; var sr = go.AddComponent(); 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); } } }