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

225 lines
9.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.GetSpawnAnchor(); // BT12-Dev-Clone (2026-05-15) γ
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;
// 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;
}
}
}
}