2026-05-09 10:00:27 +00:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using Platformer.Mechanics;
|
2026-05-09 12:23:25 +00:00
|
|
|
|
using EerieVillage.Skills;
|
2026-05-09 10:00:27 +00:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
Vector2 spawnPos = playerTransform.position;
|
|
|
|
|
|
|
|
|
|
|
|
// PlayerController.Facing 참조
|
|
|
|
|
|
Vector2 facing = Vector2.right;
|
|
|
|
|
|
var pc = inventory.GetComponent<PlayerController>();
|
|
|
|
|
|
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<HomingProjectile>() ?? go.AddComponent<HomingProjectile>();
|
|
|
|
|
|
else
|
|
|
|
|
|
proj = go.GetComponent<Projectile>() ?? go.AddComponent<Projectile>();
|
|
|
|
|
|
|
|
|
|
|
|
proj.Initialize(runtime, inventory, dir);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 투사체 프리팹 로드.
|
|
|
|
|
|
/// Phase 2-C 에서 data.projectilePrefab 필드를 추가하면 해당 경로 우선 사용.
|
|
|
|
|
|
/// 현재는 Resources/Skills/Projectiles/Default 폴백.
|
2026-05-09 12:23:25 +00:00
|
|
|
|
/// 폴백 실패 시 Collider2D + SpriteRenderer 부착 빈 오브젝트를 반환한다.
|
|
|
|
|
|
/// PD 지시 2026-05-09 — 시각화: 흰색 원 sprite (Unity 기본 whiteTexture 사용).
|
2026-05-09 10:00:27 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static GameObject LoadProjectilePrefab(ActiveSkillData data)
|
|
|
|
|
|
{
|
|
|
|
|
|
var prefab = Resources.Load<GameObject>("Skills/Projectiles/Default");
|
|
|
|
|
|
if (prefab != null) return prefab;
|
|
|
|
|
|
|
2026-05-09 12:23:25 +00:00
|
|
|
|
// 폴백 — Collider + SpriteRenderer (시각 표시·판정 동시)
|
2026-05-09 10:00:27 +00:00
|
|
|
|
var go = new GameObject($"Projectile_{data.CardId}");
|
|
|
|
|
|
var col = go.AddComponent<CircleCollider2D>();
|
|
|
|
|
|
col.isTrigger = true;
|
|
|
|
|
|
col.radius = 0.2f;
|
2026-05-09 12:23:25 +00:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-05-09 10:00:27 +00:00
|
|
|
|
return go;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 12:23:25 +00:00
|
|
|
|
// 캐시: 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-09 10:00:27 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|