using System.Collections.Generic; using UnityEngine; using Platformer.Mechanics; using Platformer.Gameplay; using static Platformer.Core.Simulation; namespace EerieVillage.Skills.Effectors { /// /// 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초 유지) /// 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(FindObjectsSortMode.None); foreach (var e in enemies) { if (e == null) continue; var h = e.GetComponent(); if (h == null || !h.IsAlive) 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 = nearest != null ? (Vector2)nearest.transform.position : playerPos; spawnPos += data.OffsetXY; // 독 늪 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(true)) { 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(); instance.Init(data, Mathf.Max(data.BaseCooldown, 1f), swampGo); } } /// /// 독 늪 인스턴스 — 6초 유지·BoxCollider2D isTrigger·적 OnTrigger 시 PoisonedEnemyMarker 부착·marker duration 5초 갱신. /// 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(); _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(); if (rb == null) rb = gameObject.AddComponent(); rb.bodyType = RigidbodyType2D.Kinematic; rb.simulated = true; rb.gravityScale = 0f; rb.useFullKinematicContacts = true; } 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(); if (e == null) return; var h = other.GetComponent(); if (h == null || !h.IsAlive) return; // 마커 부착 또는 duration 5초 갱신 (늪 위 있을 때 상시 5초 유지) var marker = e.GetComponent(); if (marker == null) marker = e.gameObject.AddComponent(); marker.Refresh(_data, 5f); } } /// /// 독 마커 — 적 자식 부착·매 초 10 피해·FX_Venom_Spray 자식 spawn·duration 만료 시 자가 Destroy. /// 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(); 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(true)) { ps.Play(true); } FxAutoDestroyUnscaled.Attach(fx, 1.5f); } if (!h.IsAlive) { var e = GetComponent(); if (e != null) Schedule().enemy = e; } } } }