diff --git a/Assets/Resources/Skills/Active/A06_dok_neup.asset b/Assets/Resources/Skills/Active/A06_dok_neup.asset new file mode 100644 index 0000000..c671b95 --- /dev/null +++ b/Assets/Resources/Skills/Active/A06_dok_neup.asset @@ -0,0 +1,68 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 69566f3f65e99394d8a0ccd0b395ac77, type: 3} + m_Name: A06_dok_neup + m_EditorClassIdentifier: Assembly-CSharp::EerieVillage.Skills.ActiveSkillData + CardId: A06 + DisplayName: "독 늪 소환" + EnglishName: Poison Swamp + Icon: {fileID: 0} + Description: "10초마다 화면 내 가장 가까운 적 + 위치에 독 늪을 생성해 6초간 유지한다. + 독에 닿은 적은 늪을 벗어나도 매 초 + 10 피해를 입는다 (늪 위 적은 5초 갱신)." + AttributeTags: 16 + TypeTags: 2 + maxLevel: 5 + Category: 2 + Trigger: 0 + BaseCooldown: 10 + BaseDamage: 10 + HitboxSize: {x: 3, y: 1.5} + OffsetDistance: {x: 0, y: 0} + Trajectory: 0 + MinionPrefab: {fileID: 0} + ChainCount: 0 + DotDuration: 5 + DotInterval: 1 + StunDuration: 0 + SlowDuration: 0 + SlowMultiplier: 0.5 + KnockbackForce: 0 + MaxConcurrent: 1 + MinionLifetime: 6 + AuraTickInterval: 1 + AuraRadius: 3 + CritDamageMultiplier: 2 + IFrameDuration: 0 + DebuffStackLimit: 3 + FireProbability: 1 + Range: 2 + MaxRange: 10 + ProjectileSpeed: 6 + ProjectilePrefab: {fileID: 0} + OnHitFxPrefab: {fileID: 113285305800631535, guid: df9dcbcdcd9d1c94caff85fc8dab3ff5, + type: 3} + ExtraHitFxPrefab: {fileID: 0} + CastFxPrefab: {fileID: 0} + OnDotFxPrefab: {fileID: 1856636965874036819, guid: 5eb649ddc4489a449bc8dceb03e0b999, + type: 3} + DotDamageMultiplier: 1 + ProjectileFxScale: 1 + HitFxScale: 1 + DotFxScale: 1 + FxRotation: 0 + OffsetXY: {x: 0, y: 0} + DamageFrameDelay: 0 + EnableRepeatDamage: 0 + MaxHitCount: 1 + RepeatFrameInterval: 30 diff --git a/Assets/Resources/Skills/Active/A11_jeongnyeongbul.asset b/Assets/Resources/Skills/Active/A11_jeongnyeongbul.asset new file mode 100644 index 0000000..c8bee95 --- /dev/null +++ b/Assets/Resources/Skills/Active/A11_jeongnyeongbul.asset @@ -0,0 +1,66 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 69566f3f65e99394d8a0ccd0b395ac77, type: 3} + m_Name: A11_jeongnyeongbul + m_EditorClassIdentifier: Assembly-CSharp::EerieVillage.Skills.ActiveSkillData + CardId: A11 + DisplayName: "정령불" + EnglishName: Spirit Fire + Icon: {fileID: 0} + Description: "15초마다 정령불 방패를 8초간 소환한다. + 플레이어 주위 회전하며 근접한 적에게 + 매 초 5의 피해·날아오는 적 투사체 소멸." + AttributeTags: 1 + TypeTags: 2 + maxLevel: 5 + Category: 3 + Trigger: 0 + BaseCooldown: 15 + BaseDamage: 5 + HitboxSize: {x: 2.5, y: 2.5} + OffsetDistance: {x: 0, y: 0} + Trajectory: 0 + MinionPrefab: {fileID: 0} + ChainCount: 0 + DotDuration: 0 + DotInterval: 1 + StunDuration: 0 + SlowDuration: 0 + SlowMultiplier: 0.5 + KnockbackForce: 0 + MaxConcurrent: 1 + MinionLifetime: 8 + AuraTickInterval: 1 + AuraRadius: 2.5 + CritDamageMultiplier: 2 + IFrameDuration: 0 + DebuffStackLimit: 3 + FireProbability: 1 + Range: 2 + MaxRange: 10 + ProjectileSpeed: 6 + ProjectilePrefab: {fileID: 0} + OnHitFxPrefab: {fileID: 1589202452151042601, guid: 16c1c1de9992a43449c144f995588c02, + type: 3} + ExtraHitFxPrefab: {fileID: 0} + CastFxPrefab: {fileID: 0} + OnDotFxPrefab: {fileID: 0} + DotDamageMultiplier: 0.25 + ProjectileFxScale: 1 + HitFxScale: 1 + DotFxScale: 1 + FxRotation: 0 + OffsetXY: {x: 0, y: 0} + DamageFrameDelay: 0 + EnableRepeatDamage: 0 + MaxHitCount: 1 + RepeatFrameInterval: 30 diff --git a/Assets/Scripts/Skills/Effectors/PoisonSwampSpawner.cs b/Assets/Scripts/Skills/Effectors/PoisonSwampSpawner.cs new file mode 100644 index 0000000..7244687 --- /dev/null +++ b/Assets/Scripts/Skills/Effectors/PoisonSwampSpawner.cs @@ -0,0 +1,173 @@ +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; + } + else + { + swampGo = new GameObject("PoisonSwamp_Fallback"); + swampGo.transform.position = spawnPos; + } + swampGo.hideFlags = HideFlags.DontSave; + + var instance = swampGo.AddComponent(); + instance.Init(data, Mathf.Max(data.BaseCooldown, 1f)); + } + } + + /// + /// 독 늪 인스턴스 — 6초 유지·BoxCollider2D isTrigger·적 OnTrigger 시 PoisonedEnemyMarker 부착·marker duration 5초 갱신. + /// + public class PoisonSwampInstance : MonoBehaviour + { + ActiveSkillData _data; + float _spawnTime; + float _duration; + BoxCollider2D _col; + + public void Init(ActiveSkillData data, float duration) + { + _data = data; + _duration = duration; + _spawnTime = Time.unscaledTime; + + // 자식 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) + { + 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; + FxAutoDestroyUnscaled.Attach(fx, 1.5f); + } + + if (!h.IsAlive) + { + var e = GetComponent(); + if (e != null) Schedule().enemy = e; + } + } + } +} diff --git a/Assets/Scripts/Skills/Effectors/SpiritFireSpawner.cs b/Assets/Scripts/Skills/Effectors/SpiritFireSpawner.cs new file mode 100644 index 0000000..27bfc73 --- /dev/null +++ b/Assets/Scripts/Skills/Effectors/SpiritFireSpawner.cs @@ -0,0 +1,114 @@ +using UnityEngine; +using Platformer.Mechanics; +using Platformer.Gameplay; +using static Platformer.Core.Simulation; + +namespace EerieVillage.Skills.Effectors +{ + /// + /// A11 정령불 Effector — Category D (Minion). + /// PD 지시 2026-05-13: + /// - 15초마다 (BaseCooldown 15) Player 자식 spawn + /// - 8초 유지 (FX_Rotating shield) + /// - 지속 시간 동안 적 투사체 SelfDestruct·근접 적 매 초 5 피해 + /// + public class SpiritFireSpawner : IEffector + { + public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory) + { + var data = runtime.ActiveData; + Vector2 spawnPos = (Vector2)inventory.transform.position + data.OffsetXY; + + GameObject shieldGo; + if (data.OnHitFxPrefab != null) + { + shieldGo = Object.Instantiate(data.OnHitFxPrefab, spawnPos, Quaternion.identity, inventory.transform); + shieldGo.transform.localScale *= data.HitFxScale; + } + else + { + shieldGo = new GameObject("SpiritFire_Fallback"); + shieldGo.transform.SetParent(inventory.transform, false); + } + shieldGo.hideFlags = HideFlags.DontSave; + + float duration = data.MinionLifetime > 0.1f ? data.MinionLifetime : 8f; + float radius = data.AuraRadius > 0.1f ? data.AuraRadius : 2.5f; + int damage = data.BaseDamage > 0 ? data.BaseDamage : 5; + + var instance = shieldGo.AddComponent(); + instance.Init(inventory.transform, duration, radius, damage); + } + } + + /// + /// 정령불 인스턴스 — Player 자식 부착·duration 동안 OverlapCircle 영역 적 투사체 SelfDestruct·근접 적 매 초 5 피해. + /// + public class SpiritFireInstance : MonoBehaviour + { + Transform _player; + float _spawnTime; + float _duration; + float _radius; + int _damage; + float _lastDamageTime; + + public void Init(Transform player, float duration, float radius, int damage) + { + _player = player; + _duration = duration; + _radius = radius; + _damage = damage; + _spawnTime = Time.unscaledTime; + } + + void Update() + { + if (Time.unscaledTime - _spawnTime >= _duration) + { + Destroy(gameObject); + return; + } + + Vector2 center = _player != null ? (Vector2)_player.position : (Vector2)transform.position; + + // 적 투사체 SelfDestruct (Projectile 컴포넌트 영역 적 발사·향후 Enemy 측 투사체 구현 시 정합) + var allProjectiles = Object.FindObjectsByType(FindObjectsSortMode.None); + foreach (var p in allProjectiles) + { + if (p == null) continue; + float d = Vector2.Distance(center, p.transform.position); + if (d <= _radius) + { + // PD 명시 — 적 투사체만 SelfDestruct. 현 Projectile = Player 발사 only → 영향 X 영역 (방어 코드). + // 향후 Enemy 측 Projectile 분리 시 friendly check 추가 필요. + } + } + + // 매 1초 근접 적 피해 + if (Time.unscaledTime - _lastDamageTime >= 1f) + { + _lastDamageTime = Time.unscaledTime; + ApplyDamageAround(center); + } + } + + void ApplyDamageAround(Vector2 center) + { + var cf = new ContactFilter2D { useTriggers = false }; + var results = new Collider2D[32]; + int n = Physics2D.OverlapCircle(center, _radius, cf, results); + for (int i = 0; i < n; i++) + { + var c = results[i]; + if (c == null) continue; + var e = c.GetComponent(); + if (e == null) continue; + var h = c.GetComponent(); + if (h == null || !h.IsAlive) continue; + h.DecrementBypassInvuln(_damage); + if (!h.IsAlive) Schedule().enemy = e; + } + } + } +} diff --git a/Assets/Scripts/Skills/Events/SkillFireEvent.cs b/Assets/Scripts/Skills/Events/SkillFireEvent.cs index 43267b5..f8eea40 100644 --- a/Assets/Scripts/Skills/Events/SkillFireEvent.cs +++ b/Assets/Scripts/Skills/Events/SkillFireEvent.cs @@ -44,7 +44,16 @@ namespace EerieVillage.Skills else effector = new MeleeAreaSpawner(); break; - // Phase 2-C~ 예정: PlacementPersistent·Minion·Debuff·SpecialJudge + // PD 지시 2026-05-13 Phase B — A06 독 늪 (PlacementPersistent)·A11 정령불 (Minion) + case ActiveCategory.PlacementPersistent: + effector = new PoisonSwampSpawner(); + break; + + case ActiveCategory.Minion: + effector = new SpiritFireSpawner(); + break; + + // Phase 2-C~ 예정: Debuff·SpecialJudge default: return; } diff --git a/Assets/Scripts/Skills/Runtime/SkillRuntimeFactory.cs b/Assets/Scripts/Skills/Runtime/SkillRuntimeFactory.cs index b305f53..02cd4dd 100644 --- a/Assets/Scripts/Skills/Runtime/SkillRuntimeFactory.cs +++ b/Assets/Scripts/Skills/Runtime/SkillRuntimeFactory.cs @@ -74,7 +74,9 @@ namespace EerieVillage.Skills { "A02", "A13", "A04", "A05", "A_Laser", // PD 지시 2026-05-13 Phase A — A08 저주의 화살·A12 정화의 빛 신규 추가 - "A08", "A12" + "A08", "A12", + // PD 지시 2026-05-13 Phase B — A06 독 늪·A11 정령불 신규 추가 + "A06", "A11" }; /// diff --git a/Assets/Scripts/Skills/Test/TestSkillFireOn1to5.cs b/Assets/Scripts/Skills/Test/TestSkillFireOn1to5.cs index d895058..48b9187 100644 --- a/Assets/Scripts/Skills/Test/TestSkillFireOn1to5.cs +++ b/Assets/Scripts/Skills/Test/TestSkillFireOn1to5.cs @@ -28,6 +28,9 @@ namespace EerieVillage.Skills.Test readonly LightningStrikeSpawner _lightningSpawner = new LightningStrikeSpawner(); readonly MeleeAreaSpawner _meleeSpawner = new MeleeAreaSpawner(); readonly LaserSpawner _laserSpawner = new LaserSpawner(); + // PD 지시 2026-05-13 Phase B — 1키 A06 독 늪·2키 A11 정령불 매핑 + readonly PoisonSwampSpawner _poisonSwampSpawner = new PoisonSwampSpawner(); + readonly SpiritFireSpawner _spiritFireSpawner = new SpiritFireSpawner(); void Awake() { @@ -73,13 +76,20 @@ namespace EerieVillage.Skills.Test } else if (data.Category == ActiveCategory.MeleeArea) { - // 키 2 (A04 번개 충격) — LightningStrikeSpawner - // 키 3 (A05 학익진) — MeleeAreaSpawner - // 키 4 (레이저) — LaserSpawner - if (idx == 1) _lightningSpawner.Trigger(rt, _inventory); - else if (idx == 3) _laserSpawner.Trigger(rt, _inventory); + // CardId 기반 분기 (SkillFireEvent 동일 패턴) + if (data.CardId == "A04") _lightningSpawner.Trigger(rt, _inventory); + else if (data.CardId == "A_Laser") _laserSpawner.Trigger(rt, _inventory); else _meleeSpawner.Trigger(rt, _inventory); } + // PD 지시 2026-05-13 Phase B — A06 독 늪 (PlacementPersistent)·A11 정령불 (Minion) + else if (data.Category == ActiveCategory.PlacementPersistent) + { + _poisonSwampSpawner.Trigger(rt, _inventory); + } + else if (data.Category == ActiveCategory.Minion) + { + _spiritFireSpawner.Trigger(rt, _inventory); + } } } }