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

225 lines
9.6 KiB
C#
Raw Normal View History

using System.Collections.Generic;
using UnityEngine;
using Platformer.Mechanics;
using Platformer.Gameplay;
using static Platformer.Core.Simulation;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// A06 독 늪 Effector — Category C (PlacementPersistent).
/// PD 지시 2026-05-13:
/// - 10초마다 (BaseCooldown 10) 화면 내 가장 가까운 적 위치에 독 늪 spawn
/// - 6초 유지 (FX_Venom_Swamp)
/// - 독에 한번이라도 닿은 적은 늪을 벗어나도 매 초 10 피해·FX_Venom_Spray 재생 (PoisonedEnemyMarker)
/// - 늪 위 적은 marker 지속 시간 5초로 갱신 (상시 5초 유지)
/// </summary>
public class PoisonSwampSpawner : IEffector
{
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
{
var data = runtime.ActiveData;
// 화면 내 가장 가까운 적 검출
var cam = Camera.main;
if (cam == null) return;
float halfH = cam.orthographicSize;
float halfW = halfH * cam.aspect;
Vector2 camPos = cam.transform.position;
EnemyController nearest = null;
float minDist = float.MaxValue;
Vector2 playerPos = inventory.transform.position;
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
foreach (var e in enemies)
{
if (e == null) continue;
var h = e.GetComponent<Health>();
if (h == null || !h.IsAlive) continue;
// PD 지시 2026-05-15 — IsFlying 강제 재 check (Awake race·controller late assign 케이스 보강)
e.RecheckFlyingFromAnimator();
// PD 지시 2026-05-15 — 공중 몬스터 (박쥐 등) skip · 독 늪 ground spawn 의도
if (e.IsFlying) continue;
var p = e.transform.position;
if (p.x < camPos.x - halfW || p.x > camPos.x + halfW || p.y < camPos.y - halfH || p.y > camPos.y + halfH) continue;
float d = Vector2.Distance(playerPos, p);
if (d < minDist) { minDist = d; nearest = e; }
}
Vector2 spawnPos;
int groundLayerMask = (1 << 0) | (1 << 16); // Level Tilemap + Floating 발판
if (nearest != null)
{
// 지상 Enemy 영역 — Enemy 위치 ground Raycast (정확한 ground.y)
Vector2 candidate = (Vector2)nearest.transform.position + data.OffsetXY;
RaycastHit2D groundHit = Physics2D.Raycast(candidate, Vector2.down, 20f, groundLayerMask);
if (groundHit.collider == null) return;
spawnPos = new Vector2(candidate.x, groundHit.point.y);
}
else
{
// PD 지시 2026-05-15 — 공격 가능 적 없을 시 Player.x 위치·Player.y - 0.2 직접 (ground Raycast 영역 X)
spawnPos = new Vector2(playerPos.x + data.OffsetXY.x, playerPos.y - 0.2f + data.OffsetXY.y);
}
// 독 늪 GO spawn (OnHitFxPrefab = FX_Venom_Swamp)
GameObject swampGo;
if (data.OnHitFxPrefab != null)
{
swampGo = Object.Instantiate(data.OnHitFxPrefab, spawnPos, Quaternion.identity);
swampGo.transform.localScale *= data.HitFxScale;
// PD 지시 2026-05-13 — ParticleSystem 명시 Play (playOnAwake 영역 정합·재발 안전망)
foreach (var ps in swampGo.GetComponentsInChildren<ParticleSystem>(true))
{
var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy;
ps.Play(true);
}
}
else
{
swampGo = new GameObject("PoisonSwamp_Fallback");
swampGo.transform.position = spawnPos;
}
swampGo.hideFlags = HideFlags.DontSave;
// PD 지시 2026-05-13 — BoxCollider2D·Rigidbody2D 영역 자식 GO 분리 (ParticleSystem 영역 root 영향 차단)
var colliderGo = new GameObject("PoisonSwamp_Collider");
colliderGo.hideFlags = HideFlags.DontSave;
colliderGo.transform.SetParent(swampGo.transform, false);
colliderGo.transform.localPosition = Vector3.zero;
var instance = colliderGo.AddComponent<PoisonSwampInstance>();
instance.Init(data, Mathf.Max(data.BaseCooldown, 1f), swampGo);
}
}
/// <summary>
/// 독 늪 인스턴스 — 6초 유지·BoxCollider2D isTrigger·적 OnTrigger 시 PoisonedEnemyMarker 부착·marker duration 5초 갱신.
/// </summary>
public class PoisonSwampInstance : MonoBehaviour
{
ActiveSkillData _data;
float _spawnTime;
float _duration;
BoxCollider2D _col;
GameObject _swampVisualRoot; // PD 지시 2026-05-13 — FX GO 영역 별도·duration 종료 시 함께 Destroy
public void Init(ActiveSkillData data, float duration, GameObject visualRoot)
{
_data = data;
_duration = duration;
_spawnTime = Time.unscaledTime;
_swampVisualRoot = visualRoot;
// 자식 BoxCollider2D 부착·HitboxSize 영역 정합
_col = gameObject.AddComponent<BoxCollider2D>();
_col.isTrigger = true;
Vector2 size = data.HitboxSize.sqrMagnitude > 0.01f ? data.HitboxSize : new Vector2(3f, 1.5f);
_col.size = size;
_col.offset = Vector2.zero;
// Kinematic Rigidbody2D — Kinematic vs Kinematic OnTriggerStay 발화 정합 (Enemy = KinematicObject)
var rb = GetComponent<Rigidbody2D>();
if (rb == null) rb = gameObject.AddComponent<Rigidbody2D>();
rb.bodyType = RigidbodyType2D.Kinematic;
rb.simulated = true;
rb.gravityScale = 0f;
rb.useFullKinematicContacts = true;
feat(BT12-Dev): A06·A11 판정 박스 시각화 + A11 OverlapBox 전환 + 페이드 (PD 지시 2026-05-14) PD 발화 2건: 1. "독 늪 소환의 판정(중독 효과 발생 가능한 범위)와 정령불의 판정 범위도 박스 형태로 보여줘 (정령불의 범위에 있는 적은 정령불이 유지되는 동안 일정한 피해 간격마다 피해를 입어야 해)" 2. "정령불 이펙트는 소멸되기 0.5초 전부터 알파값을 증가시키고 크기를 최대 현재 크기 기준으로 최대 50%까지 줄어들며 사라지도록 해줘." A06 독 늪 (PoisonSwampInstance): - PoisonSwampHitbox_Debug 자식 GO 신규 — BoxCollider2D.size 일관 SpriteRenderer (1, 0, 0, 0.35) · sortingOrder 100 · ShowDebugVisuals 토글 A11 정령불 (SpiritFireSpawner / Instance): - OverlapCircle → OverlapBox (HitboxSize 사용·박스 시각 ↔ 판정 정합) - AuraRadius 의존 제거 · _radius (float) → _boxSize (Vector2) - SpiritFireHitbox_Debug Player 자식 GO — Player 이동 동조 - 피해 간격 = DotInterval (기본 1초·Inspector 조절) - 소멸 0.5초 전 페이드 (PD 의도 "투명도 증가" = alpha 1→0): · FADE_DURATION=0.5 · alphaMul = 1-t · scaleMul = 1-0.5t · Renderer·MaterialPropertyBlock·_baseAlphas 캐싱 (Projectile 동일 패턴) · _TintColor 또는 _Color 자동 분기 PlayerSkillInventory.StaleSpawnNames: - PoisonSwampHitbox_Debug·SpiritFireHitbox_Debug·MeleeHitbox_Debug2 추가 - 게임 재실행 시 cleanup catch 정합 검증 (Play 모드): - A06 발사 → PoisonSwampHitbox_Debug visible=True (parent FX_Venom_Swamp) - A11 발사 → SpiritFireHitbox_Debug visible=True (parent Player·scale 2.5,2.5) - A11 HitboxSize=(2.5, 2.5)·DotInterval=1·MinionLifetime=8 정합 SOT (스킬_이펙트_확정_v1.md) §4 변경 이력 갱신. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 14:14:05 +00:00
// PD 지시 2026-05-14 — 판정 박스 시각화 (붉은 반투명 박스·ShowDebugVisuals 토글)
var dbg = new GameObject("PoisonSwampHitbox_Debug");
dbg.hideFlags = HideFlags.DontSave;
dbg.transform.SetParent(transform, false);
dbg.transform.localPosition = Vector3.zero;
dbg.transform.localScale = new Vector3(size.x, size.y, 1f);
var sr = dbg.AddComponent<SpriteRenderer>();
sr.sprite = HitboxDebug.GetWhiteSprite();
sr.color = new Color(1f, 0f, 0f, 0.35f);
sr.sortingOrder = 100;
sr.enabled = HitboxDebug.ShowDebugVisuals;
}
void Update()
{
if (Time.unscaledTime - _spawnTime >= _duration)
{
// FX 영역 root 영역 함께 Destroy
if (_swampVisualRoot != null) Destroy(_swampVisualRoot);
else Destroy(gameObject);
}
}
void OnTriggerEnter2D(Collider2D other) { TryMark(other); }
void OnTriggerStay2D(Collider2D other) { TryMark(other); }
void TryMark(Collider2D other)
{
if (other == null || _data == null) return;
var e = other.GetComponent<EnemyController>();
if (e == null) return;
var h = other.GetComponent<Health>();
if (h == null || !h.IsAlive) return;
// 마커 부착 또는 duration 5초 갱신 (늪 위 있을 때 상시 5초 유지)
var marker = e.GetComponent<PoisonedEnemyMarker>();
if (marker == null) marker = e.gameObject.AddComponent<PoisonedEnemyMarker>();
marker.Refresh(_data, 5f);
}
}
/// <summary>
/// 독 마커 — 적 자식 부착·매 초 10 피해·FX_Venom_Spray 자식 spawn·duration 만료 시 자가 Destroy.
/// </summary>
public class PoisonedEnemyMarker : MonoBehaviour
{
ActiveSkillData _data;
float _lastTickTime;
float _expireTime;
public void Refresh(ActiveSkillData data, float duration)
{
_data = data;
_expireTime = Time.unscaledTime + duration;
}
void Update()
{
if (_data == null) { Destroy(this); return; }
if (Time.unscaledTime >= _expireTime) { Destroy(this); return; }
if (Time.unscaledTime - _lastTickTime >= 1f)
{
_lastTickTime = Time.unscaledTime;
Tick();
}
}
void Tick()
{
var h = GetComponent<Health>();
if (h == null || !h.IsAlive) { Destroy(this); return; }
int dmg = _data.BaseDamage > 0 ? _data.BaseDamage : 10;
h.DecrementBypassInvuln(dmg);
// FX_Venom_Spray 자식 spawn (적 위치)
if (_data.OnDotFxPrefab != null)
{
var fx = Object.Instantiate(_data.OnDotFxPrefab, transform.position, Quaternion.Euler(0f, 0f, _data.FxRotation), transform);
fx.hideFlags = HideFlags.DontSave;
fx.transform.localScale *= _data.DotFxScale;
// PD 지시 2026-05-13 — ParticleSystem 명시 Play (playOnAwake 영역 정합·재발 안전망)
foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(true))
{
var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy;
ps.Play(true);
}
FxAutoDestroyUnscaled.Attach(fx, 1.5f);
}
if (!h.IsAlive)
{
var e = GetComponent<EnemyController>();
if (e != null) Schedule<EnemyDeath>().enemy = e;
}
}
}
}