diff --git a/Fantasy-Grower/AGENTS.md b/Fantasy-Grower/AGENTS.md index fef65e16..00c1693b 100644 --- a/Fantasy-Grower/AGENTS.md +++ b/Fantasy-Grower/AGENTS.md @@ -55,14 +55,21 @@ ## 10. Code Formatting - **CSharpier**: All C# code must be formatted strictly following the **CSharpier** formatting style. Do not use custom formatting or spacing that contradicts CSharpier conventions. -## 11. Unity and C# Compatibility +## 11. Encoding and File Reading +- Treat project text files as UTF-8. When reading files through PowerShell, prefer `Get-Content -Encoding UTF8` and set UTF-8 output encoding when practical. +- The Codex/terminal output pipeline may still display Korean comments, tooltips, headers, or XML documentation as mojibake. Do not infer code behavior from garbled Korean text. +- Do not treat garbled Korean comments or Inspector strings as evidence of a source-file corruption by themselves. Verify using C# syntax, symbols, braces, declarations, references, compiler/formatter output, and Unity serialization data. +- If a line appears to contain code after `//` because of mojibake, do not edit it merely based on the displayed Korean text. First confirm that the actual C# token is missing, duplicated, unreachable, or otherwise causing a real compile/logic issue. +- When adding new Korean comments or Inspector text, save files as UTF-8 and keep executable C# statements on their own lines, separate from comments and attributes. + +## 12. Unity and C# Compatibility - The project targets Unity 6 and C# 9, but available BCL APIs are determined by Unity's configured API compatibility level. Do not assume that every API from modern standalone .NET is available. - Before introducing a new runtime or BCL type, verify that Unity can compile it under the project's current API compatibility settings. - Prefer APIs already used successfully in the project when an equivalent modern API has uncertain Unity support. - Never edit Unity-generated `.csproj` or `.sln` files manually. Configure compatibility and packages through Unity. - Do not add, remove, or upgrade packages without explicit user approval. -## 12. Unity Serialization and Asset Safety +## 13. Unity Serialization and Asset Safety - Preserve `.meta` files and Unity GUIDs. Never recreate, delete, or move assets casually. - When renaming a serialized field, use `[FormerlySerializedAs]` or provide an explicit migration so existing scenes, prefabs, and ScriptableObjects retain their values. - Do not change a serialized field's type without checking all affected scenes, prefabs, and assets. @@ -70,14 +77,14 @@ - Keep player progression/state separate from definitions such as weapon, skill, dungeon, wave, reward, and stat data. - Do not directly edit Unity YAML assets unless necessary and understood; prefer Unity Editor operations for structural scene/prefab changes. -## 13. Inspector and Runtime Validation +## 14. Inspector and Runtime Validation - `OnValidate()` is an editor-time aid, not a runtime safety mechanism. Public entry points and lifecycle methods must still handle invalid or missing dependencies safely. - Use `NaughtyAttributes` such as `ShowIf`, `HideIf`, and validation attributes when they materially improve Inspector clarity. - Log validation errors with the component context (`this`) and enough information to identify the affected object and field. - Do not spam logs every frame or repeatedly report the same recoverable error. - Required component dependencies should use `[RequireComponent]` where appropriate. -## 14. Lifecycle, Events, Coroutines, and Tweening +## 15. Lifecycle, Events, Coroutines, and Tweening - Every event subscription must have a matching unsubscription at the appropriate lifecycle boundary. - Stop or kill owned coroutines and tweens when their owner is disabled or destroyed when continued execution could access stale objects. - Before starting a replacement tween, kill the previous tween that writes to the same state or UI element. @@ -85,14 +92,14 @@ - Avoid static events unless global broadcast semantics are genuinely required; static subscribers must always unsubscribe. - Singleton components must reject duplicates safely and must not destroy unrelated components attached to the same GameObject. -## 15. Performance and Allocation Policy +## 16. Performance and Allocation Policy - Optimize measured or clearly hot paths such as per-frame combat, frequently refreshed UI, and repeated spawning. Do not complicate cold code for speculative micro-optimization. - Avoid repeated LINQ, reflection, `Find*`, string interpolation, and avoidable allocations in `Update`, combat loops, and high-frequency UI callbacks. - For allocation-sensitive TMP number displays, prefer reusable `char[]`/`Span`, project `SpanExtension` helpers, and `TMP_Text.SetCharArray` when Unity compatibility is verified. - Reuse existing caches such as `YieldInstructionCache` for repeated fixed waits. - Object pooling is preferred for frequently spawned combat effects and entities once their spawn rate justifies it. -## 16. Persistence, Networking, and Security +## 17. Persistence, Networking, and Security - PlayerPrefs is only for non-sensitive device-local preferences. Never treat it as authoritative storage for currency, progression, inventory, equipment, skills, dungeon records, or credentials. - Never store passwords, session secrets, or long-lived authentication tokens in plain PlayerPrefs or unencrypted JSON. - Server-authoritative values must be validated and calculated by the server; never trust client-submitted rewards, prices, or balances. @@ -100,14 +107,14 @@ - Network mutations that grant or consume value must be idempotent and safe against duplicate requests. - Keep DTOs separate from Unity domain objects and ScriptableObjects. -## 17. Error Handling and Data Integrity +## 18. Error Handling and Data Integrity - Validate currency balance, index bounds, null references, maximum levels, and state transitions before mutating state. - Avoid unsigned subtraction when the minuend may be smaller; validate first to prevent underflow. - A multi-step operation such as purchase, synthesis, upgrade, reward grant, or skill unlock must either complete fully or leave state unchanged. - Do not silently swallow failures. Return a meaningful result or log a clear error at the correct ownership layer. - Do not use placeholder conditions such as `if (true)` or ship test-only behavior in production paths. -## 18. Testing, Verification, and Git Safety +## 19. Testing, Verification, and Git Safety - Do not claim Unity compilation, Play Mode behavior, or Inspector wiring was verified unless it was actually checked in the Unity Editor. - Add EditMode or PlayMode tests for deterministic core logic when practical, especially currency consumption, rewards, damage, skill unlock rules, and progression calculations. - After code changes, inspect the diff and run whitespace/error checks. Report any verification that could not be performed. diff --git a/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Archer.prefab b/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Archer.prefab index 7ec73424..c4cf0ef3 100644 --- a/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Archer.prefab +++ b/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Archer.prefab @@ -3268,6 +3268,7 @@ GameObject: - component: {fileID: -934695906495670747} - component: {fileID: 4682663325673948411} - component: {fileID: 713001000000000003} + - component: {fileID: 713002000000000003} m_Layer: 0 m_Name: Archer m_TagString: Untagged @@ -6336,6 +6337,25 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: targets: {fileID: 430098703366088485} +--- !u!114 &713002000000000003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5857381939795342657} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0733f8d1a3f642ca9bcfa8813c6cb14b, type: 3} + m_Name: + m_EditorClassIdentifier: + enableKeyboardInput: 1 + slotKeys: + - 49 + - 50 + - 51 + - 52 + - 53 --- !u!114 &6034931146787275272 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Warrior.prefab b/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Warrior.prefab index b2ac5d97..c078eb7b 100644 --- a/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Warrior.prefab +++ b/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Warrior.prefab @@ -3670,6 +3670,7 @@ GameObject: - component: {fileID: 9091531221583279582} - component: {fileID: 8937113908660665095} - component: {fileID: 713001000000000002} + - component: {fileID: 713002000000000002} m_Layer: 0 m_Name: Warrior m_TagString: Untagged @@ -6748,6 +6749,25 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: targets: {fileID: 5115161450785208119} +--- !u!114 &713002000000000002 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6499316856751531827} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0733f8d1a3f642ca9bcfa8813c6cb14b, type: 3} + m_Name: + m_EditorClassIdentifier: + enableKeyboardInput: 1 + slotKeys: + - 49 + - 50 + - 51 + - 52 + - 53 --- !u!114 &6107641911005882464 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Wizard.prefab b/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Wizard.prefab index 22e9f85c..0228975b 100644 --- a/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Wizard.prefab +++ b/Fantasy-Grower/Assets/Prefabs/PlayerCharacter/Wizard.prefab @@ -3173,6 +3173,7 @@ GameObject: - component: {fileID: 4753827104680141411} - component: {fileID: 4530378413672165486} - component: {fileID: 713001000000000004} + - component: {fileID: 713002000000000004} m_Layer: 0 m_Name: Wizard m_TagString: Untagged @@ -6319,6 +6320,25 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: targets: {fileID: 6694508944490675349} +--- !u!114 &713002000000000004 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6063808467322970300} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0733f8d1a3f642ca9bcfa8813c6cb14b, type: 3} + m_Name: + m_EditorClassIdentifier: + enableKeyboardInput: 1 + slotKeys: + - 49 + - 50 + - 51 + - 52 + - 53 --- !u!114 &4616874832124039420 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Attack/SingleTargetAttackBehaviour.cs b/Fantasy-Grower/Assets/Scripts/Battle/Attack/SingleTargetAttackBehaviour.cs index f8354b76..a30d74b9 100644 --- a/Fantasy-Grower/Assets/Scripts/Battle/Attack/SingleTargetAttackBehaviour.cs +++ b/Fantasy-Grower/Assets/Scripts/Battle/Attack/SingleTargetAttackBehaviour.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEngine; /// @@ -10,6 +11,9 @@ public sealed class SingleTargetAttackBehaviour : EntityAttackBehaviour [SerializeField] private AttackTargetsSensing targets; + private readonly List extensionTargetBuffer = new(); + private readonly List nearestTargetBuffer = new(); + private void Awake() { if (targets == null) @@ -21,17 +25,83 @@ public override bool TryAttack(Entity attacker) if (attacker == null || targets == null) return false; - Entity target = targets.GetFirstTarget(); - if (target == null) + SkillTreeComponent skillTreeComponent = null; + BasicAttackSkillData basicAttack = null; + if (attacker.TryGetComponent(out SkillTreeComponent foundSkillTreeComponent)) + { + skillTreeComponent = foundSkillTreeComponent; + basicAttack = skillTreeComponent.GetUnlockedBasicAttack(); + } + + // 인식 사거리 콜라이더에 적이 1마리라도 있어야 공격 모션을 시작함 + var sensingTargets = targets.GetTargets(); + if (sensingTargets == null || sensingTargets.Count == 0) return false; - (float damage, _) = DamageCalculator.Calculate( - attacker.AttackPower, - target.DamageReduction, - attacker.CriticalPercentage - ); - target.TakeDamage(damage); - return true; + int maxTargets = + (basicAttack != null ? basicAttack.MaxTargets : 1) + + attacker.BasicAttackTargetCountBonus; + + // 연장 사거리가 있으면 거리 기반 O(N*K) 탐색으로 타겟을 수집 + System.Collections.Generic.IReadOnlyList attackTargets; + if (basicAttack != null && basicAttack.ExtensionRange > 0f) + { + float attackAreaMultiplier = + skillTreeComponent != null ? skillTreeComponent.GetAttackAreaMultiplier() : 1f; + float totalRange = + (attacker.AttackRange > 0f ? attacker.AttackRange : 0f) + + basicAttack.ExtensionRange * attackAreaMultiplier; + WaveController.TryCollectActiveEnemies(extensionTargetBuffer); + + CollectNearestInRange( + extensionTargetBuffer, + attacker.transform.position, + totalRange, + maxTargets, + nearestTargetBuffer + ); + attackTargets = nearestTargetBuffer; + } + else + { + attackTargets = sensingTargets; + } + + int attackedCount = 0; + for (int i = 0; i < attackTargets.Count && attackedCount < maxTargets; i++) + { + Entity target = attackTargets[i]; + if (target == null || target.Hp <= 0f) + continue; + + (float damage, _) = DamageCalculator.Calculate( + attacker.AttackPower, + target.DamageReduction, + attacker.CriticalPercentage, + attacker.CriticalDamageMultiplier + ); + + if (basicAttack != null) + damage *= basicAttack.DamageRate; + + if (skillTreeComponent != null) + { + damage *= skillTreeComponent.GetOutgoingDamageMultiplier(); + damage *= skillTreeComponent.GetBasicAttackDamageMultiplier(); + } + damage *= attacker.OutgoingDamageMultiplier; + + if (target is Enemy { IsEliteTarget: true } && skillTreeComponent != null) + { + damage *= skillTreeComponent.GetEliteDamageMultiplier(); + } + + float actualDamage = target.TakeDamage(damage, attacker); + attacker.NotifyDamageDealt(target, actualDamage); + attackedCount++; + } + + return attackedCount > 0; } private void OnValidate() @@ -47,4 +117,50 @@ private void OnValidate() ); } } + + /// + /// 원점으로부터 totalRange 이내의 생존 적 중 가장 가까운 K마리를 수집합니다. + /// O(N * K) 삽입 방식: 람다·Sort·Sqrt 없이 sqrMagnitude만 비교합니다. + /// + private static void CollectNearestInRange( + List candidates, + Vector3 origin, + float totalRange, + int maxTargets, + List results + ) + { + results.Clear(); + float rangeSqr = totalRange * totalRange; + int k = Mathf.Max(0, maxTargets); + + for (int i = 0; i < candidates.Count; i++) + { + Entity candidate = candidates[i]; + if (candidate == null || candidate.Hp <= 0f) + continue; + + float sqrDist = (candidate.transform.position - origin).sqrMagnitude; + if (sqrDist > rangeSqr) + continue; + + int insertIndex = results.Count; + for (int j = results.Count - 1; j >= 0; j--) + { + float existingSqrDist = (results[j].transform.position - origin).sqrMagnitude; + if (sqrDist < existingSqrDist) + insertIndex = j; + else + break; + } + + if (insertIndex < k) + { + results.Insert(insertIndex, candidate); + + if (results.Count > k) + results.RemoveAt(results.Count - 1); + } + } + } } diff --git a/Fantasy-Grower/Assets/Scripts/Battle/AttackTargetsSensing.cs b/Fantasy-Grower/Assets/Scripts/Battle/AttackTargetsSensing.cs index 90a988f8..7d6b6fc0 100644 --- a/Fantasy-Grower/Assets/Scripts/Battle/AttackTargetsSensing.cs +++ b/Fantasy-Grower/Assets/Scripts/Battle/AttackTargetsSensing.cs @@ -12,6 +12,11 @@ public class AttackTargetsSensing : MonoBehaviour private readonly List _targets = new(); + private BoxCollider2D boxCollider; + private Entity owner; + private Vector2 originalSize; + private Vector2 originalOffset; + public Entity GetFirstTarget() { if (_targets.Count > 0) @@ -28,6 +33,13 @@ private void Awake() _attackEvent = GetComponentInParent(); if (_attackEvent == null) Debug.LogError("[AttackTargets] 부모 중 IAttackEvent 구현체가 없습니다.", this); + + owner = GetComponentInParent(); + if (owner != null) + owner.OnStatsChanged += RecalculateRange; + + CacheRangeCollider(); + RecalculateRange(); } private void OnTriggerEnter2D(Collider2D other) @@ -84,4 +96,44 @@ private void OnDisable() } _targets.Clear(); } + + private void OnDestroy() + { + if (owner != null) + owner.OnStatsChanged -= RecalculateRange; + } + + private bool CacheRangeCollider() + { + if (boxCollider != null) + return true; + + boxCollider = GetComponent(); + if (boxCollider == null) + { + Debug.LogError( + "[AttackTargets] 사거리 패시브는 BoxCollider2D 감지 콜라이더에서만 동작합니다.", + this + ); + return false; + } + + originalSize = boxCollider.size; + originalOffset = boxCollider.offset; + return true; + } + + private void RecalculateRange() + { + if (!CacheRangeCollider()) + return; + + float attackRange = + owner != null && owner.AttackRange > 0f ? owner.AttackRange : originalSize.x; + float newWidth = Mathf.Max(0f, attackRange); + float widthDelta = newWidth - originalSize.x; + + boxCollider.size = new Vector2(newWidth, originalSize.y); + boxCollider.offset = new Vector2(originalOffset.x - widthDelta * 0.5f, originalOffset.y); + } } diff --git a/Fantasy-Grower/Assets/Scripts/Battle/DungeonFeature/AddressableStageFeatureBase.cs b/Fantasy-Grower/Assets/Scripts/Battle/DungeonFeature/AddressableStageFeatureBase.cs index 6bfa20e7..25576fc3 100644 --- a/Fantasy-Grower/Assets/Scripts/Battle/DungeonFeature/AddressableStageFeatureBase.cs +++ b/Fantasy-Grower/Assets/Scripts/Battle/DungeonFeature/AddressableStageFeatureBase.cs @@ -22,6 +22,8 @@ public DungeonDataAssetReference(string guid) /// public abstract class AddressableStageFeatureBase : MonoBehaviour, IStageProvider { + public static event Action OnAnyStageChanged; + [SerializeField, Tooltip("스테이지 순서대로 등록한 Addressable DungeonData 배열")] [FormerlySerializedAs("_stages")] private DungeonDataAssetReference[] stageReferences; @@ -143,6 +145,7 @@ private IEnumerator LoadAndActivateStage(int targetIndex, bool updateRecord, int } OnStageChanged?.Invoke(currentStageIndex); + OnAnyStageChanged?.Invoke(currentStageIndex); int nextStageIndex = currentStageIndex + 1; if (nextStageIndex < stageReferences.Length) diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs index 84d11b9b..2e737ef9 100644 --- a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/Enemy.cs @@ -1,11 +1,23 @@ using UnityEngine; +public enum EnemyGrade +{ + Normal, + Elite, + Boss, +} + /// /// 적 엔티티 기반 클래스. /// 사망 시 EnemyRewardData에 정의된 Gold/XP를 지급한다. /// public class Enemy : Entity { + [field: SerializeField, Header("적 등급")] + public EnemyGrade Grade { get; private set; } + + public bool IsEliteTarget => Grade is EnemyGrade.Elite or EnemyGrade.Boss; + [SerializeField, Header("공격 방식")] private EntityAttackBehaviour attackBehaviour; @@ -32,6 +44,7 @@ protected override void Awake() stageStatScaler = GetComponent(); sensor.OnBlocked += OnBlocked; sensor.OnUnBlocked += OnUnBlocked; + OnStatsChanged += HandleStatsChanged; } protected override void Start() @@ -41,7 +54,7 @@ protected override void Start() protected override void OnMove() { - rb.linearVelocityX = -moveSpeed; + rb.linearVelocityX = -moveSpeed * MoveSpeedMultiplier; } protected override void OnIdle() @@ -86,6 +99,12 @@ private void UnsubscribeSensorEvents() sensor.OnUnBlocked -= OnUnBlocked; } + private void HandleStatsChanged() + { + if (entityState[gameObject].State == PlayerState.MOVE) + OnMove(); + } + public override void Death() { base.Death(); // OnDied 이벤트 발화 (WaveController가 구독 중) @@ -109,6 +128,8 @@ protected override void OnDestroy() { base.OnDestroy(); + OnStatsChanged -= HandleStatsChanged; + // 예외 처리(만약 적이 죽어서 파괴된 게 아니라면) UnsubscribeSensorEvents(); } diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs index 541466be..f9d5543e 100644 --- a/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs +++ b/Fantasy-Grower/Assets/Scripts/Battle/Enemy/EnemyAI.cs @@ -44,7 +44,13 @@ private IEnumerator AttackLoop() { while (true) { - enemy.Attack(); + if ( + !enemy.TryGetComponent(out StatusEffectController statusEffectController) + || !statusEffectController.PreventsAction + ) + { + enemy.Attack(); + } float interval = enemy.AttackSpeed > 0f ? 1f / enemy.AttackSpeed : 2f; yield return YieldInstructionCache.WaitForSeconds(interval); diff --git a/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs b/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs index ace7d86d..6c8b9a08 100644 --- a/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs +++ b/Fantasy-Grower/Assets/Scripts/Battle/Entity.cs @@ -1,4 +1,4 @@ -using System; +using System; using UnityEngine; public enum EntityType @@ -16,6 +16,11 @@ public abstract class Entity : MonoBehaviour public float AttackPower { get; private set; } public float AttackSpeed { get; private set; } public float CriticalPercentage { get; private set; } + public float CriticalDamageMultiplier { get; private set; } = 2f; + public float AttackRange { get; private set; } = 1f; + public float MoveSpeedMultiplier { get; private set; } = 1f; + public float OutgoingDamageMultiplier { get; private set; } = 1f; + public int BasicAttackTargetCountBonus { get; private set; } [field: SerializeField, Header("엔티티 설정")] public EntityType EntityType { get; private set; } @@ -36,8 +41,17 @@ public abstract class Entity : MonoBehaviour /// 실제 피해로 HP가 감소했을 때 피해 직전과 직후의 HP를 순서대로 전달한다. public event Action OnDamageTaken; + /// 피해가 HP에 적용되기 전에 발화된다. 패시브가 피해량을 조정하거나 취소할 수 있다. + public event Action OnBeforeDamageTaken; + + /// 다른 Entity에게 피해를 입혔을 때 대상과 실제 피해량을 전달한다. + public event Action OnDamageDealt; + /// Update문이 호출될 때 발화된다. - protected event Action OnUpdated; + public event Action OnUpdated; + + /// 스탯 재계산이 끝났을 때 발화된다. + public event Action OnStatsChanged; protected virtual void Awake() { @@ -132,14 +146,22 @@ public virtual void Death() entityState[gameObject].State = PlayerState.DEATH; } - public virtual void TakeDamage(float damage) + public virtual float TakeDamage(float damage, Entity attacker = null) { if (Hp <= 0) - return; // 이미 사망 — 중복 호출 무시 + return 0f; // 이미 사망 — 중복 호출 무시 + + var damageContext = new IncomingDamageContext(this, attacker, damage); + OnBeforeDamageTaken?.Invoke(damageContext); + + if (damageContext.IsCancelled || damageContext.Damage <= 0f) + return 0f; + damage = damageContext.Damage; Debug.Log($"데미지 받음: {damage}"); float previousHp = Hp; Hp = Mathf.Max(0, Hp - damage); + float actualDamage = previousHp - Hp; if (Hp < previousHp) OnDamageTaken?.Invoke(previousHp, Hp); @@ -148,6 +170,24 @@ public virtual void TakeDamage(float damage) if (Hp <= 0) Death(); + + return actualDamage; + } + + public void Heal(float amount) + { + if (amount <= 0f || Hp <= 0f) + return; + + Hp = Mathf.Min(MaxHp, Hp + amount); + } + + public void NotifyDamageDealt(Entity target, float damage) + { + if (target == null || damage <= 0f) + return; + + OnDamageDealt?.Invoke(target, damage); } /// @@ -260,6 +300,16 @@ private void RecalculateStats() 0f, 100f ); + CriticalDamageMultiplier = Mathf.Max(1f, 2f * (1f + totalModifier.BonusCriticalDamageRate)); + AttackRange = CalculateModifiedStat( + statData.AttackRange, + 0f, + totalModifier.BonusAttackRangeRate + ); + MoveSpeedMultiplier = Mathf.Max(0f, 1f + totalModifier.BonusMoveSpeedRate); + OutgoingDamageMultiplier = Mathf.Max(0f, 1f + totalModifier.BonusOutgoingDamageRate); + BasicAttackTargetCountBonus = totalModifier.BonusBasicAttackTargetCount; + OnStatsChanged?.Invoke(); } private static float CalculateModifiedStat( diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset.meta b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects.meta similarity index 52% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset.meta rename to Fantasy-Grower/Assets/Scripts/Battle/StatusEffects.meta index 6ab5c5f9..02716526 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset.meta +++ b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects.meta @@ -1,8 +1,8 @@ fileFormatVersion: 2 -guid: da12e4dcc00447545828f224e06be591 -NativeFormatImporter: +guid: bff3da557fe64fdab586bb5ffc984e99 +folderAsset: yes +DefaultImporter: externalObjects: {} - mainObjectFileID: 11400000 userData: assetBundleName: assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectController.cs b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectController.cs new file mode 100644 index 00000000..480b224c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectController.cs @@ -0,0 +1,268 @@ +using System.Collections.Generic; +using UnityEngine; + +[DisallowMultipleComponent] +public sealed class StatusEffectController : MonoBehaviour +{ + private sealed class StatusEffectInstance + { + public StatusEffectType Type; + public Entity Source; + public float RemainingDuration; + public float DamagePerSecond; + public float TargetMaxHpDamageRatePerSecond; + public float IncomingDamageBonusRate; + public bool PreventsAction; + public EntityStatModifierHandle ModifierHandle; + } + + private readonly List effects = new(); + private Entity owner; + + public static StatusEffectController GetOrAdd(Entity entity) + { + if (entity == null) + return null; + + if (!entity.TryGetComponent(out StatusEffectController controller)) + controller = entity.gameObject.AddComponent(); + + return controller; + } + + public bool HasEffect(StatusEffectType type) + { + return GetStackCount(type) > 0; + } + + public int GetStackCount(StatusEffectType type) + { + int count = 0; + for (int i = 0; i < effects.Count; i++) + { + if (effects[i].Type == type) + count++; + } + + return count; + } + + public bool PreventsAction + { + get + { + for (int i = 0; i < effects.Count; i++) + { + if (effects[i].PreventsAction) + return true; + } + + return false; + } + } + + private void Awake() + { + owner = GetComponent(); + if (owner != null) + owner.OnBeforeDamageTaken += HandleBeforeDamageTaken; + } + + private void Update() + { + float deltaTime = Time.deltaTime; + for (int i = effects.Count - 1; i >= 0; i--) + { + StatusEffectInstance effect = effects[i]; + if (owner != null && owner.Hp > 0f) + { + float damagePerSecond = + effect.DamagePerSecond + owner.MaxHp * effect.TargetMaxHpDamageRatePerSecond; + if (damagePerSecond > 0f) + owner.TakeDamage(damagePerSecond * deltaTime, effect.Source); + } + + effect.RemainingDuration -= deltaTime; + if (effect.RemainingDuration <= 0f) + RemoveAt(i); + } + } + + public void ApplyBurn( + Entity source, + float damagePerSecond, + float targetMaxHpDamageRatePerSecond, + float duration, + bool canStack, + int maxStacks + ) + { + if ((damagePerSecond <= 0f && targetMaxHpDamageRatePerSecond <= 0f) || duration <= 0f) + return; + + ApplyEffect( + StatusEffectType.Burn, + source, + duration, + damagePerSecond, + targetMaxHpDamageRatePerSecond, + EntityStatModifier.Zero, + 0f, + false, + canStack, + maxStacks + ); + } + + public void ApplyModifierEffect( + StatusEffectType type, + Entity source, + EntityStatModifier modifier, + float duration, + bool canStack, + int maxStacks + ) + { + if (duration <= 0f) + return; + + ApplyEffect(type, source, duration, 0f, 0f, modifier, 0f, false, canStack, maxStacks); + } + + public void ApplyIncomingDamageUp( + Entity source, + float incomingDamageBonusRate, + float duration, + bool canStack, + int maxStacks + ) + { + if (incomingDamageBonusRate <= 0f || duration <= 0f) + return; + + ApplyEffect( + StatusEffectType.IncomingDamageUp, + source, + duration, + 0f, + 0f, + EntityStatModifier.Zero, + incomingDamageBonusRate, + false, + canStack, + maxStacks + ); + } + + public void ApplyActionBlock(StatusEffectType type, Entity source, float duration) + { + if (duration <= 0f) + return; + + EntityStatModifier modifier = EntityStatModifier.Zero; + modifier.BonusMoveSpeedRate = -1f; + + ApplyEffect(type, source, duration, 0f, 0f, modifier, 0f, true, false, 1); + } + + public void RemoveAll(StatusEffectType type) + { + for (int i = effects.Count - 1; i >= 0; i--) + { + if (effects[i].Type == type) + RemoveAt(i); + } + } + + private void ApplyEffect( + StatusEffectType type, + Entity source, + float duration, + float damagePerSecond, + float targetMaxHpDamageRatePerSecond, + EntityStatModifier modifier, + float incomingDamageBonusRate, + bool preventsAction, + bool canStack, + int maxStacks + ) + { + if (owner == null) + return; + + if (!canStack) + RemoveAll(type); + else if (maxStacks > 0) + TrimOldestStacks(type, maxStacks - 1); + + EntityStatModifierHandle handle = default; + if (!EntityStatModifierUtility.IsZero(modifier)) + handle = owner.ApplyStatModifier(modifier); + + effects.Add( + new StatusEffectInstance + { + Type = type, + Source = source, + RemainingDuration = duration, + DamagePerSecond = damagePerSecond, + TargetMaxHpDamageRatePerSecond = targetMaxHpDamageRatePerSecond, + IncomingDamageBonusRate = incomingDamageBonusRate, + PreventsAction = preventsAction, + ModifierHandle = handle, + } + ); + } + + private void HandleBeforeDamageTaken(IncomingDamageContext damageContext) + { + if (damageContext == null || damageContext.IsCancelled) + return; + + float incomingDamageBonusRate = 0f; + for (int i = 0; i < effects.Count; i++) + incomingDamageBonusRate += effects[i].IncomingDamageBonusRate; + + if (incomingDamageBonusRate > 0f) + damageContext.Damage *= 1f + incomingDamageBonusRate; + } + + private void TrimOldestStacks(StatusEffectType type, int targetStackCount) + { + while (GetStackCount(type) > targetStackCount) + { + int oldestIndex = -1; + for (int i = 0; i < effects.Count; i++) + { + if (effects[i].Type == type) + { + oldestIndex = i; + break; + } + } + + if (oldestIndex < 0) + return; + + RemoveAt(oldestIndex); + } + } + + private void RemoveAt(int index) + { + StatusEffectInstance effect = effects[index]; + if (owner != null && effect.ModifierHandle.IsValid) + owner.RemoveStatModifier(effect.ModifierHandle); + + effects.RemoveAt(index); + } + + private void OnDestroy() + { + if (owner != null) + owner.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + + for (int i = effects.Count - 1; i >= 0; i--) + RemoveAt(i); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectController.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectController.cs.meta new file mode 100644 index 00000000..7618cf24 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f81d2a00c7dc4334ba13d7ce21045674 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectType.cs b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectType.cs new file mode 100644 index 00000000..868f7385 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectType.cs @@ -0,0 +1,11 @@ +public enum StatusEffectType +{ + Burn, + Frost, + Freeze, + Airborne, + Stun, + AttackPowerDown, + IncomingDamageUp, + MoveSpeedDown, +} diff --git a/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectType.cs.meta b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectType.cs.meta new file mode 100644 index 00000000..d9bf6ae9 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Battle/StatusEffects/StatusEffectType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 88aeb7ad6a1f455f84e975322e9b9e83 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs b/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs index 28f49567..a047bbf1 100644 --- a/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs +++ b/Fantasy-Grower/Assets/Scripts/Battle/WaveController.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using UnityEngine; /// @@ -12,9 +11,33 @@ public class WaveController : MonoBehaviour /// 현재 웨이브의 모든 적이 사망했을 때 발화된다. public static event Action OnAllEnemiesDead; + /// 현재 웨이브에 등록된 적 하나가 사망했을 때 발화된다. + public static event Action OnEnemyDiedGlobal; + + /// 새 웨이브 스폰이 시작되었을 때 발화된다. + public static event Action OnWaveStartedGlobal; + + /// 현재 웨이브의 생존 적 수가 변경되었을 때 발화된다. + public static event Action OnEnemyCountChangedGlobal; + + public static int CurrentActiveEnemyCount { get; private set; } + + private static readonly List activeControllers = new(); + private int _aliveCount; private readonly HashSet _activeEnemies = new(); + private void OnEnable() + { + if (!activeControllers.Contains(this)) + activeControllers.Add(this); + } + + private void OnDisable() + { + activeControllers.Remove(this); + } + private void OnDestroy() { foreach (Enemy e in _activeEnemies) @@ -22,6 +45,9 @@ private void OnDestroy() if (e != null) e.OnDied -= OnEnemyDied; } + + CurrentActiveEnemyCount = 0; + OnEnemyCountChangedGlobal?.Invoke(CurrentActiveEnemyCount); } /// @@ -37,6 +63,8 @@ public void SpawnWave(WaveData waveData, Transform spawnPoint, float spawnInterv } _activeEnemies.Clear(); + OnWaveStartedGlobal?.Invoke(); + float curOffset = 0f; // 추후 오브젝트 풀링 작동 방식으로 변환할 것. @@ -63,6 +91,8 @@ public void SpawnWave(WaveData waveData, Transform spawnPoint, float spawnInterv } _aliveCount = _activeEnemies.Count; + CurrentActiveEnemyCount = _aliveCount; + OnEnemyCountChangedGlobal?.Invoke(CurrentActiveEnemyCount); if (_aliveCount == 0) { @@ -72,10 +102,45 @@ public void SpawnWave(WaveData waveData, Transform spawnPoint, float spawnInterv } /// 현재 살아있는 첫 번째 적을 반환한다. 없으면 null. - public Enemy GetFirstAliveEnemy() => _activeEnemies.FirstOrDefault(e => e != null && e.Hp > 0); + public Enemy GetFirstAliveEnemy() + { + foreach (Enemy enemy in _activeEnemies) + { + if (enemy != null && enemy.Hp > 0) + return enemy; + } + + return null; + } public IReadOnlyCollection ActiveEnemies => _activeEnemies; + public static bool TryCollectActiveEnemies(List results) + { + if (results == null) + return false; + + results.Clear(); + + for (int i = 0; i < activeControllers.Count; i++) + { + WaveController waveController = activeControllers[i]; + if (waveController != null) + waveController.CollectAliveEnemies(results); + } + + return results.Count > 0; + } + + private void CollectAliveEnemies(List results) + { + foreach (Enemy enemy in _activeEnemies) + { + if (enemy != null && enemy.Hp > 0f) + results.Add(enemy); + } + } + /// 살아있는 적을 모두 파괴하고 상태를 초기화한다. 재시도 또는 씬 정리 시 사용. public void Clear() { @@ -89,6 +154,8 @@ public void Clear() } _activeEnemies.Clear(); _aliveCount = 0; + CurrentActiveEnemyCount = 0; + OnEnemyCountChangedGlobal?.Invoke(CurrentActiveEnemyCount); } private void OnEnemyDied(Entity entity) @@ -99,6 +166,9 @@ private void OnEnemyDied(Entity entity) entity.OnDied -= OnEnemyDied; _activeEnemies.Remove(enemy); _aliveCount = Mathf.Max(0, _aliveCount - 1); + CurrentActiveEnemyCount = _aliveCount; + OnEnemyCountChangedGlobal?.Invoke(CurrentActiveEnemyCount); + OnEnemyDiedGlobal?.Invoke(enemy); if (_aliveCount == 0) OnAllEnemiesDead?.Invoke(); diff --git a/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs b/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs index 19cc2d35..278cbd0f 100644 --- a/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs +++ b/Fantasy-Grower/Assets/Scripts/Core/DamageCalculator.cs @@ -16,12 +16,13 @@ public static class DamageCalculator public static (float damage, bool isCritical) Calculate( float rawAttackPower, float targetDamageReduction, - float attackerCriticalPercentage + float attackerCriticalPercentage, + float criticalDamageMultiplier = 2f ) { float reduced = Mathf.Max(1, rawAttackPower * (1f - targetDamageReduction)); bool isCritical = Random.value * 100f < attackerCriticalPercentage; - float finalDamage = isCritical ? reduced * 2 : reduced; + float finalDamage = isCritical ? reduced * criticalDamageMultiplier : reduced; return (finalDamage, isCritical); } } diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs b/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs index 6d7de954..e6d2483c 100644 --- a/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatData.cs @@ -9,4 +9,5 @@ public class EntityStatData : ScriptableObject public float AttackPower; public float AttackSpeed; public float CriticalPercentage; + public float AttackRange; } diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs index 81e7b9cb..9ddc12e4 100644 --- a/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifier.cs @@ -1,4 +1,4 @@ -[System.Serializable] +[System.Serializable] public readonly struct EntityStatModifierHandle : System.IEquatable { internal EntityStatModifierHandle(int id) @@ -33,6 +33,10 @@ public struct EntityStatModifier public float BonusAttackPower; public float BonusAttackSpeed; public float BonusCriticalPercentage; + public float BonusCriticalDamageRate; + public float BonusAttackRangeRate; + public float BonusMoveSpeedRate; + public float BonusOutgoingDamageRate; [UnityEngine.Header("비율 보정 (0.2 = 20%)")] public float BonusHpRate; @@ -42,6 +46,9 @@ public struct EntityStatModifier public float BonusAttackSpeedRate; public float BonusCriticalPercentageRate; + [UnityEngine.Header("기타 보정")] + public int BonusBasicAttackTargetCount; + public static EntityStatModifier operator +(EntityStatModifier a, EntityStatModifier b) { return new EntityStatModifier @@ -52,6 +59,10 @@ public struct EntityStatModifier BonusAttackPower = a.BonusAttackPower + b.BonusAttackPower, BonusAttackSpeed = a.BonusAttackSpeed + b.BonusAttackSpeed, BonusCriticalPercentage = a.BonusCriticalPercentage + b.BonusCriticalPercentage, + BonusCriticalDamageRate = a.BonusCriticalDamageRate + b.BonusCriticalDamageRate, + BonusAttackRangeRate = a.BonusAttackRangeRate + b.BonusAttackRangeRate, + BonusMoveSpeedRate = a.BonusMoveSpeedRate + b.BonusMoveSpeedRate, + BonusOutgoingDamageRate = a.BonusOutgoingDamageRate + b.BonusOutgoingDamageRate, BonusHpRate = a.BonusHpRate + b.BonusHpRate, BonusHpRecoveryRate = a.BonusHpRecoveryRate + b.BonusHpRecoveryRate, BonusDamageReductionRate = a.BonusDamageReductionRate + b.BonusDamageReductionRate, @@ -59,6 +70,8 @@ public struct EntityStatModifier BonusAttackSpeedRate = a.BonusAttackSpeedRate + b.BonusAttackSpeedRate, BonusCriticalPercentageRate = a.BonusCriticalPercentageRate + b.BonusCriticalPercentageRate, + BonusBasicAttackTargetCount = + a.BonusBasicAttackTargetCount + b.BonusBasicAttackTargetCount, }; } diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifierUtility.cs b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifierUtility.cs new file mode 100644 index 00000000..adeaf587 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifierUtility.cs @@ -0,0 +1,49 @@ +public static class EntityStatModifierUtility +{ + public static bool IsZero(EntityStatModifier modifier) + { + return modifier.BonusHp == 0f + && modifier.BonusHpRecovery == 0f + && modifier.BonusDamageReduction == 0f + && modifier.BonusAttackPower == 0f + && modifier.BonusAttackSpeed == 0f + && modifier.BonusCriticalPercentage == 0f + && modifier.BonusCriticalDamageRate == 0f + && modifier.BonusAttackRangeRate == 0f + && modifier.BonusMoveSpeedRate == 0f + && modifier.BonusOutgoingDamageRate == 0f + && modifier.BonusHpRate == 0f + && modifier.BonusHpRecoveryRate == 0f + && modifier.BonusDamageReductionRate == 0f + && modifier.BonusAttackPowerRate == 0f + && modifier.BonusAttackSpeedRate == 0f + && modifier.BonusCriticalPercentageRate == 0f + && modifier.BonusBasicAttackTargetCount == 0; + } + + public static EntityStatModifier Scale(EntityStatModifier modifier, float multiplier) + { + return new EntityStatModifier + { + BonusHp = modifier.BonusHp * multiplier, + BonusHpRecovery = modifier.BonusHpRecovery * multiplier, + BonusDamageReduction = modifier.BonusDamageReduction * multiplier, + BonusAttackPower = modifier.BonusAttackPower * multiplier, + BonusAttackSpeed = modifier.BonusAttackSpeed * multiplier, + BonusCriticalPercentage = modifier.BonusCriticalPercentage * multiplier, + BonusCriticalDamageRate = modifier.BonusCriticalDamageRate * multiplier, + BonusAttackRangeRate = modifier.BonusAttackRangeRate * multiplier, + BonusMoveSpeedRate = modifier.BonusMoveSpeedRate * multiplier, + BonusOutgoingDamageRate = modifier.BonusOutgoingDamageRate * multiplier, + BonusHpRate = modifier.BonusHpRate * multiplier, + BonusHpRecoveryRate = modifier.BonusHpRecoveryRate * multiplier, + BonusDamageReductionRate = modifier.BonusDamageReductionRate * multiplier, + BonusAttackPowerRate = modifier.BonusAttackPowerRate * multiplier, + BonusAttackSpeedRate = modifier.BonusAttackSpeedRate * multiplier, + BonusCriticalPercentageRate = modifier.BonusCriticalPercentageRate * multiplier, + BonusBasicAttackTargetCount = UnityEngine.Mathf.RoundToInt( + modifier.BonusBasicAttackTargetCount * multiplier + ), + }; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifierUtility.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifierUtility.cs.meta new file mode 100644 index 00000000..1530f797 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/EntityStatModifierUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c31e4f83f7de4693b12c4dbde9be3d92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/IncomingDamageContext.cs b/Fantasy-Grower/Assets/Scripts/Core/IncomingDamageContext.cs new file mode 100644 index 00000000..99519891 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/IncomingDamageContext.cs @@ -0,0 +1,20 @@ +public sealed class IncomingDamageContext +{ + public IncomingDamageContext(Entity target, Entity attacker, float damage) + { + Target = target; + Attacker = attacker; + Damage = damage; + } + + public Entity Target { get; } + public Entity Attacker { get; } + public float Damage { get; set; } + public bool IsCancelled { get; private set; } + + public void Cancel() + { + IsCancelled = true; + Damage = 0f; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/IncomingDamageContext.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/IncomingDamageContext.cs.meta new file mode 100644 index 00000000..63e4413f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/IncomingDamageContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ac21940f7f14d1d890d95dc8b253605 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillContext.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillContext.cs new file mode 100644 index 00000000..c503b9b3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillContext.cs @@ -0,0 +1,61 @@ +/// +/// 액티브 스킬이 실행될 때 필요한 런타임 정보를 전달합니다. +/// ScriptableObject인 스킬 데이터가 Entity 핸들 같은 런타임 상태를 직접 보관하지 않도록 분리합니다. +/// +public readonly struct ActiveSkillContext +{ + public ActiveSkillContext( + ActiveSkillExecutor executor, + SkillTreeComponent skillTreeComponent, + Entity caster, + ActiveSkillData skill, + int slotIndex + ) + { + Executor = executor; + SkillTreeComponent = skillTreeComponent; + Caster = caster; + Skill = skill; + SlotIndex = slotIndex; + } + + public ActiveSkillExecutor Executor { get; } + public SkillTreeComponent SkillTreeComponent { get; } + public Entity Caster { get; } + public ActiveSkillData Skill { get; } + public int SlotIndex { get; } + + public bool TryCollectTargets( + ActiveSkillTargetMode targetMode, + int maxTargets, + System.Collections.Generic.List results, + float extensionRange = 0f + ) + { + if (Executor == null) + return false; + + return Executor.TryCollectTargets(targetMode, maxTargets, results, extensionRange); + } + + public System.Collections.Generic.IReadOnlyList CollectTargets( + ActiveSkillTargetMode targetMode, + int maxTargets, + float extensionRange = 0f + ) + { + if (Executor == null) + return System.Array.Empty(); + + return Executor.CollectTargets(targetMode, maxTargets, extensionRange); + } + + public float GetModifiedDamage(float baseDamage) + { + if (Executor == null) + return baseDamage; + + float multiplier = Caster != null ? Caster.OutgoingDamageMultiplier : 1f; + return baseDamage * multiplier * Executor.GetActiveSkillDamageMultiplier(Skill); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillContext.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillContext.cs.meta new file mode 100644 index 00000000..636aa8d5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillContext.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a0bbc9ade6e947329282fb1ee34afbbe diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillDamageCategory.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillDamageCategory.cs new file mode 100644 index 00000000..35dbbcc4 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillDamageCategory.cs @@ -0,0 +1,6 @@ +public enum ActiveSkillDamageCategory +{ + None, + SingleTarget, + Area, +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillDamageCategory.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillDamageCategory.cs.meta new file mode 100644 index 00000000..e8532d39 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillDamageCategory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3d836167211e4ff08d8b9aa64087be3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs index dc8827f0..3972605e 100644 --- a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillData.cs @@ -1,7 +1,50 @@ -using UnityEngine; +using UnityEngine; /// -/// 액티브 스킬 데이터의 추상 기반 클래스. -/// UseSkill()을 구현하여 스킬별 발동 로직을 정의한다. +/// 액티브 스킬 데이터의 추상 기반 클래스입니다. +/// 실제 런타임 효과는 사용자, 슬롯, 실행기 정보를 담은 ActiveSkillContext를 통해 실행합니다. /// -public abstract class ActiveSkillData : SkillData { } +public abstract class ActiveSkillData : SkillData +{ + [SerializeField] + private ActiveSkillDamageCategory damageCategory = ActiveSkillDamageCategory.None; + + [SerializeField] + private bool usableOncePerDungeon; + + [ + SerializeField, + Min(0f), + Tooltip("인식 사거리 너머 추가 타격 반경 (0이면 인식 사거리 내만 공격)") + ] + private float extensionRange; + + public ActiveSkillDamageCategory DamageCategory => damageCategory; + public virtual bool UsableOncePerDungeon => usableOncePerDungeon; + public float ExtensionRange => extensionRange; + + /// + /// 기존 테스트/에디터 호출 호환용 메서드입니다. + /// 실제 전투에서는 TryUseSkill(ActiveSkillContext)를 사용합니다. + /// + public override void UseSkill() { } + + public bool TryUseSkill(ActiveSkillContext context) + { + if (!CanUseSkill(context)) + return false; + + UseSkill(context); + return true; + } + + protected virtual bool CanUseSkill(ActiveSkillContext context) + { + return context.Caster != null && context.Executor != null; + } + + protected virtual void UseSkill(ActiveSkillContext context) + { + UseSkill(); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillExecutor.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillExecutor.cs new file mode 100644 index 00000000..b6e7a641 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillExecutor.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +/// +/// 플레이어에게 장착된 액티브 스킬을 실행하고 쿨다운과 일시적 스탯 보정을 관리합니다. +/// +[DisallowMultipleComponent] +[RequireComponent(typeof(Player))] +[RequireComponent(typeof(SkillTreeComponent))] +public sealed class ActiveSkillExecutor : MonoBehaviour +{ + [Header("입력(컴퓨터 환경 전용)")] + [SerializeField] + private bool enableKeyboardInput = true; + + [SerializeField] + private KeyCode[] slotKeys = + { + KeyCode.Alpha1, + KeyCode.Alpha2, + KeyCode.Alpha3, + KeyCode.Alpha4, + KeyCode.Alpha5, + }; + + private readonly List temporaryModifierHandles = new(); + private readonly List runtimeCleanups = new(); + private readonly List targetBuffer = new(); + private readonly List waveTargetBuffer = new(); + private readonly HashSet usedOncePerDungeonSlots = new(); + + private Player player; + private SkillTreeComponent skillTreeComponent; + private AttackTargetsSensing attackTargetsSensing; + private float[] cooldownEndTimes; + + private void Awake() + { + player = GetComponent(); + skillTreeComponent = GetComponent(); + attackTargetsSensing = GetComponentInChildren(); + } + + private void Start() + { + EnsureCooldownBuffer(); + } + + private void OnEnable() + { + AddressableStageFeatureBase.OnAnyStageChanged += HandleStageChanged; + ResetOncePerDungeonUsages(); + } + + private void Update() + { + if (!enableKeyboardInput) + return; + + int slotCount = Mathf.Min(slotKeys.Length, GetActiveSlotCount()); + for (int i = 0; i < slotCount; i++) + { + if (Input.GetKeyDown(slotKeys[i])) + TryUseSkill(i); + } + } + + private void OnDisable() + { + AddressableStageFeatureBase.OnAnyStageChanged -= HandleStageChanged; + ClearRuntimeCleanups(); + ClearTemporaryModifiers(); + } + + public bool TryUseSkill(int slotIndex) + { + ActiveSkillData skill = GetEquippedSkill(slotIndex); + if (skill == null) + return false; + + EnsureCooldownBuffer(); + if (slotIndex < 0 || slotIndex >= cooldownEndTimes.Length) + return false; + + if (GetCooldownRemaining(slotIndex) > 0f) + return false; + + if (skill.UsableOncePerDungeon && usedOncePerDungeonSlots.Contains(slotIndex)) + return false; + + var context = new ActiveSkillContext(this, skillTreeComponent, player, skill, slotIndex); + + if (!skill.TryUseSkill(context)) + return false; + + if (skill.UsableOncePerDungeon) + usedOncePerDungeonSlots.Add(slotIndex); + + float cooldown = GetModifiedCooldown(skill); + if (cooldown > 0f) + cooldownEndTimes[slotIndex] = Time.time + cooldown; + + return true; + } + + public float GetCooldownRemaining(int slotIndex) + { + if (cooldownEndTimes == null || slotIndex < 0 || slotIndex >= cooldownEndTimes.Length) + return 0f; + + return Mathf.Max(0f, cooldownEndTimes[slotIndex] - Time.time); + } + + public ActiveSkillData GetEquippedSkill(int slotIndex) + { + if (skillTreeComponent == null) + return null; + + return skillTreeComponent.GetEquippedActive(slotIndex); + } + + public float GetCooldownRatio(int slotIndex) + { + ActiveSkillData skill = GetEquippedSkill(slotIndex); + float cooldown = GetModifiedCooldown(skill); + if (skill == null || cooldown <= 0f) + return 0f; + + return Mathf.Clamp01(GetCooldownRemaining(slotIndex) / cooldown); + } + + public bool IsOncePerDungeonSkillUsed(int slotIndex) + { + return usedOncePerDungeonSlots.Contains(slotIndex); + } + + public void ReduceAllCooldowns(float seconds) + { + if (seconds <= 0f || cooldownEndTimes == null) + return; + + for (int i = 0; i < cooldownEndTimes.Length; i++) + ReduceCooldown(i, seconds); + } + + public void ReduceAllCooldownsPercent(float percent) + { + if (percent <= 0f || cooldownEndTimes == null) + return; + + for (int i = 0; i < cooldownEndTimes.Length; i++) + { + ActiveSkillData skill = GetEquippedSkill(i); + if (skill != null) + { + float totalCooldown = GetModifiedCooldown(skill); + ReduceCooldown(i, totalCooldown * percent); + } + } + } + + public void ResetCooldownsExcept(int excludedSlotIndex) + { + if (cooldownEndTimes == null) + return; + + for (int i = 0; i < cooldownEndTimes.Length; i++) + { + if (i != excludedSlotIndex) + cooldownEndTimes[i] = Time.time; + } + } + + public void ResetOncePerDungeonUsages() + { + usedOncePerDungeonSlots.Clear(); + } + + private void HandleStageChanged(int stageIndex) + { + ResetOncePerDungeonUsages(); + } + + public bool TryCollectTargets( + ActiveSkillTargetMode targetMode, + int maxTargets, + List results, + float extensionRange = 0f + ) + { + if (results == null) + return false; + + results.Clear(); + + switch (targetMode) + { + case ActiveSkillTargetMode.Self: + if (player != null) + results.Add(player); + break; + case ActiveSkillTargetMode.AllEnemies: + WaveController.TryCollectActiveEnemies(waveTargetBuffer); + AddTargets(waveTargetBuffer, maxTargets, results); + break; + case ActiveSkillTargetMode.DetectedTargets: + default: + if (extensionRange > 0f && player != null) + { + // 인식 사거리 + 연장 사거리 범위 내에서 가장 가까운 K마리를 O(N*K)로 수집 + float attackAreaMultiplier = + skillTreeComponent != null + ? skillTreeComponent.GetAttackAreaMultiplier() + : 1f; + float totalRange = + (player.AttackRange > 0f ? player.AttackRange : 0f) + + extensionRange * attackAreaMultiplier; + WaveController.TryCollectActiveEnemies(waveTargetBuffer); + CollectNearestInRange( + waveTargetBuffer, + player.transform.position, + totalRange, + maxTargets, + results + ); + } + else if (attackTargetsSensing != null) + { + AddTargets(attackTargetsSensing.GetTargets(), maxTargets, results); + } + break; + } + + return results.Count > 0; + } + + public IReadOnlyList CollectTargets( + ActiveSkillTargetMode targetMode, + int maxTargets, + float extensionRange = 0f + ) + { + TryCollectTargets(targetMode, maxTargets, targetBuffer, extensionRange); + return targetBuffer; + } + + public void ReduceCooldown(int slotIndex, float seconds) + { + if ( + seconds <= 0f + || cooldownEndTimes == null + || slotIndex < 0 + || slotIndex >= cooldownEndTimes.Length + ) + { + return; + } + + cooldownEndTimes[slotIndex] = Mathf.Max(Time.time, cooldownEndTimes[slotIndex] - seconds); + } + + public float GetActiveSkillDamageMultiplier() + { + return GetActiveSkillDamageMultiplier(null); + } + + public float GetActiveSkillDamageMultiplier(ActiveSkillData skill) + { + return skillTreeComponent != null + ? skillTreeComponent.GetActiveSkillDamageMultiplier(skill) + : 1f; + } + + private float GetModifiedCooldown(ActiveSkillData skill) + { + if (skill == null || skill.Cooldown <= 0f) + return 0f; + + float multiplier = + skillTreeComponent != null ? skillTreeComponent.GetActiveSkillCooldownMultiplier() : 1f; + return skill.Cooldown * multiplier; + } + + public EntityStatModifierHandle ApplyTemporaryModifier( + EntityStatModifier modifier, + float duration + ) + { + if (player == null) + return default; + + EntityStatModifierHandle handle = player.ApplyStatModifier(modifier); + temporaryModifierHandles.Add(handle); + + if (duration > 0f) + StartCoroutine(RemoveTemporaryModifierAfterDelay(handle, duration)); + + return handle; + } + + public void RegisterRuntimeCleanup(Action cleanup) + { + if (cleanup == null || runtimeCleanups.Contains(cleanup)) + return; + + runtimeCleanups.Add(cleanup); + } + + public void UnregisterRuntimeCleanup(Action cleanup) + { + if (cleanup == null) + return; + + runtimeCleanups.Remove(cleanup); + } + + private IEnumerator RemoveTemporaryModifierAfterDelay( + EntityStatModifierHandle handle, + float duration + ) + { + yield return YieldInstructionCache.WaitForSeconds(duration); + RemoveTemporaryModifier(handle); + } + + private void RemoveTemporaryModifier(EntityStatModifierHandle handle) + { + if (!handle.IsValid) + return; + + if (player != null) + player.RemoveStatModifier(handle); + + temporaryModifierHandles.Remove(handle); + } + + private void ClearTemporaryModifiers() + { + StopAllCoroutines(); + + if (player != null) + { + foreach (EntityStatModifierHandle handle in temporaryModifierHandles) + player.RemoveStatModifier(handle); + } + + temporaryModifierHandles.Clear(); + } + + private void ClearRuntimeCleanups() + { + for (int i = runtimeCleanups.Count - 1; i >= 0; i--) + runtimeCleanups[i].Invoke(); + + runtimeCleanups.Clear(); + } + + private void EnsureCooldownBuffer() + { + int slotCount = GetActiveSlotCount(); + if (cooldownEndTimes != null && cooldownEndTimes.Length == slotCount) + return; + + float[] newCooldownEndTimes = new float[slotCount]; + if (cooldownEndTimes != null) + { + int copyLength = Mathf.Min(cooldownEndTimes.Length, slotCount); + Array.Copy(cooldownEndTimes, newCooldownEndTimes, copyLength); + } + + cooldownEndTimes = newCooldownEndTimes; + } + + private int GetActiveSlotCount() + { + return skillTreeComponent != null ? skillTreeComponent.EquippedActiveCount : 0; + } + + private static void AddTargets( + IReadOnlyList source, + int maxTargets, + List results + ) + { + if (source == null) + return; + + int targetLimit = Mathf.Max(0, maxTargets); + for (int i = 0; i < source.Count && results.Count < targetLimit; i++) + { + Entity target = source[i]; + if (target != null && target.Hp > 0f) + results.Add(target); + } + } + + /// + /// 지정한 원점으로부터 totalRange 이내의 생존 적 중 가장 가까운 K마리를 수집합니다. + /// O(N * K) 삽입 정렬 방식: 람다 캡처·Sort·Sqrt 호출 없이 제곱 거리(sqrMagnitude)만 비교합니다. + /// + private static void CollectNearestInRange( + List candidates, + Vector3 origin, + float totalRange, + int maxTargets, + List results + ) + { + float rangeSqr = totalRange * totalRange; + int k = Mathf.Max(0, maxTargets); + + // results는 호출 전 Clear된 상태이므로 재활용됨 + for (int i = 0; i < candidates.Count; i++) + { + Entity candidate = candidates[i]; + if (candidate == null || candidate.Hp <= 0f) + continue; + + float sqrDist = (candidate.transform.position - origin).sqrMagnitude; + if (sqrDist > rangeSqr) + continue; + + // results에 삽입할 위치를 찾는다 (가까운 순서 유지) + int insertIndex = results.Count; + for (int j = results.Count - 1; j >= 0; j--) + { + float existingSqrDist = (results[j].transform.position - origin).sqrMagnitude; + if (sqrDist < existingSqrDist) + insertIndex = j; + else + break; + } + + if (insertIndex < k) + { + results.Insert(insertIndex, candidate); + + if (results.Count > k) + results.RemoveAt(results.Count - 1); + } + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillExecutor.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillExecutor.cs.meta new file mode 100644 index 00000000..27c76544 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillExecutor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0733f8d1a3f642ca9bcfa8813c6cb14b diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillTargetMode.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillTargetMode.cs new file mode 100644 index 00000000..112cba82 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillTargetMode.cs @@ -0,0 +1,6 @@ +public enum ActiveSkillTargetMode +{ + DetectedTargets, + AllEnemies, + Self, +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillTargetMode.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillTargetMode.cs.meta new file mode 100644 index 00000000..cc27a04e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ActiveSkillTargetMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f3ae10a938114fbc94a5a699ec2b7ac9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillCooldownModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillCooldownModifier.cs new file mode 100644 index 00000000..45fa0326 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillCooldownModifier.cs @@ -0,0 +1,5 @@ +public interface IActiveSkillCooldownModifier +{ + /// 0.2 = 액티브 스킬 쿨다운 20% 감소. + float CooldownReductionRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillCooldownModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillCooldownModifier.cs.meta new file mode 100644 index 00000000..da20270c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillCooldownModifier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 05559a5f4b4d4deeb747249cd4eaf123 diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillDamageModifier.cs new file mode 100644 index 00000000..b2f3ec66 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillDamageModifier.cs @@ -0,0 +1,4 @@ +public interface IActiveSkillDamageModifier +{ + float ActiveSkillDamageBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillDamageModifier.cs.meta new file mode 100644 index 00000000..1f463e9e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IActiveSkillDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 688c0943a5884d949a762af13e44bb45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAreaActiveSkillDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAreaActiveSkillDamageModifier.cs new file mode 100644 index 00000000..4c76032a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAreaActiveSkillDamageModifier.cs @@ -0,0 +1,4 @@ +public interface IAreaActiveSkillDamageModifier +{ + float AreaActiveSkillDamageBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAreaActiveSkillDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAreaActiveSkillDamageModifier.cs.meta new file mode 100644 index 00000000..f0b9afcd --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAreaActiveSkillDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 091a3d1c2afa427a8effbfcc0b5d05d7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAttackAreaModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAttackAreaModifier.cs new file mode 100644 index 00000000..92822ad1 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAttackAreaModifier.cs @@ -0,0 +1,4 @@ +public interface IAttackAreaModifier +{ + float AttackAreaBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAttackAreaModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAttackAreaModifier.cs.meta new file mode 100644 index 00000000..3e320f0a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IAttackAreaModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 05619e9179254ec4a9ce751dbab7a952 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBasicAttackDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBasicAttackDamageModifier.cs new file mode 100644 index 00000000..ee28c42e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBasicAttackDamageModifier.cs @@ -0,0 +1,4 @@ +public interface IBasicAttackDamageModifier +{ + float BasicAttackDamageMultiplier { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBasicAttackDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBasicAttackDamageModifier.cs.meta new file mode 100644 index 00000000..3847fa72 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBasicAttackDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6df9392c5153423fac8c471701447912 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBurnStackingModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBurnStackingModifier.cs new file mode 100644 index 00000000..17d776c5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBurnStackingModifier.cs @@ -0,0 +1,4 @@ +public interface IBurnStackingModifier +{ + bool AllowsBurnStacking { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBurnStackingModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBurnStackingModifier.cs.meta new file mode 100644 index 00000000..0d515791 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IBurnStackingModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a56ff16526b649be999eaee735b1f41e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IEliteDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IEliteDamageModifier.cs new file mode 100644 index 00000000..ef246696 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IEliteDamageModifier.cs @@ -0,0 +1,4 @@ +public interface IEliteDamageModifier +{ + float EliteDamageBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IEliteDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IEliteDamageModifier.cs.meta new file mode 100644 index 00000000..75681a54 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IEliteDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2fb881dbb61d4087a5d6b487145cf58f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostEffectBonusModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostEffectBonusModifier.cs new file mode 100644 index 00000000..9660c894 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostEffectBonusModifier.cs @@ -0,0 +1,4 @@ +public interface IFrostEffectBonusModifier +{ + float FrostEffectBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostEffectBonusModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostEffectBonusModifier.cs.meta new file mode 100644 index 00000000..532080c8 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostEffectBonusModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a616b5dd2d5241789b301637f3bd4b0a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostFreezeModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostFreezeModifier.cs new file mode 100644 index 00000000..b4742e0f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostFreezeModifier.cs @@ -0,0 +1,6 @@ +public interface IFrostFreezeModifier +{ + int RequiredFrostStacks { get; } + float FreezeDuration { get; } + EntityStatModifier FreezeModifier { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostFreezeModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostFreezeModifier.cs.meta new file mode 100644 index 00000000..3a088d32 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IFrostFreezeModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ea42acee67cc4ea794313919b824b181 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IOutgoingDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IOutgoingDamageModifier.cs new file mode 100644 index 00000000..c6a79312 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IOutgoingDamageModifier.cs @@ -0,0 +1,4 @@ +public interface IOutgoingDamageModifier +{ + float OutgoingDamageBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IOutgoingDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IOutgoingDamageModifier.cs.meta new file mode 100644 index 00000000..11639a5e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/IOutgoingDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c89d28790da4ca7bc9da3aafac6fc6a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyAreaSkillDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyAreaSkillDamageModifier.cs new file mode 100644 index 00000000..442865b3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyAreaSkillDamageModifier.cs @@ -0,0 +1,4 @@ +public interface ISingleEnemyAreaSkillDamageModifier +{ + float SingleEnemyAreaSkillDamageBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyAreaSkillDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyAreaSkillDamageModifier.cs.meta new file mode 100644 index 00000000..07ccd4fe --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyAreaSkillDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e438d1a32b9a4f3e89007da5c21445b9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyDamageModifier.cs new file mode 100644 index 00000000..3ae57227 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyDamageModifier.cs @@ -0,0 +1,4 @@ +public interface ISingleEnemyDamageModifier +{ + float SingleEnemyDamageBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyDamageModifier.cs.meta new file mode 100644 index 00000000..4c828e57 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleEnemyDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e79811817094b9fb9eb975bc28fac42 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleTargetActiveSkillDamageModifier.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleTargetActiveSkillDamageModifier.cs new file mode 100644 index 00000000..84b4232e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleTargetActiveSkillDamageModifier.cs @@ -0,0 +1,4 @@ +public interface ISingleTargetActiveSkillDamageModifier +{ + float SingleTargetActiveSkillDamageBonusRate { get; } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleTargetActiveSkillDamageModifier.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleTargetActiveSkillDamageModifier.cs.meta new file mode 100644 index 00000000..43bd5ef7 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/ISingleTargetActiveSkillDamageModifier.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d6d70e95b2f4c07a1395f62ac45c85a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs index caddb82f..cad37fb4 100644 --- a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillData.cs @@ -14,4 +14,10 @@ public override void UseSkill() { } /// SkillTreeComponent.RecalculatePassives()에서 일괄 호출된다. /// public abstract void ApplyPassive(ref EntityStatModifier modifier); + + /// + /// 장착 중에만 유지되어야 하는 런타임 패시브 효과를 생성한다. + /// 단순 스탯 패시브는 null을 반환하면 된다. + /// + public virtual PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) => null; } diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillRuntime.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillRuntime.cs new file mode 100644 index 00000000..54f1b9ca --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillRuntime.cs @@ -0,0 +1,23 @@ +public readonly struct PassiveSkillRuntimeContext +{ + public PassiveSkillRuntimeContext(SkillTreeComponent skillTreeComponent, Entity owner) + { + SkillTreeComponent = skillTreeComponent; + Owner = owner; + } + + public SkillTreeComponent SkillTreeComponent { get; } + public Entity Owner { get; } +} + +public abstract class PassiveSkillRuntime +{ + protected PassiveSkillRuntime(PassiveSkillRuntimeContext context) + { + Context = context; + } + + protected PassiveSkillRuntimeContext Context { get; } + + public abstract void Dispose(); +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillRuntime.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillRuntime.cs.meta new file mode 100644 index 00000000..ff7f0658 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveSkillRuntime.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 054ebed1684b47539fa2f1ce5469bc95 diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveTriggerType.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveTriggerType.cs new file mode 100644 index 00000000..75ea0aad --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveTriggerType.cs @@ -0,0 +1,4 @@ +public enum PassiveTriggerType +{ + WindScar, +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveTriggerType.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveTriggerType.cs.meta new file mode 100644 index 00000000..87523783 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/PassiveTriggerType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b6d43d62f6a4b74b8ce9a7fd5e18e5e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs index de44de99..def66599 100644 --- a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/SkillTreeComponent.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using UnityEngine; /// @@ -23,10 +22,15 @@ public class SkillTreeComponent : MonoBehaviour private List equippedActives; private List equippedPassives; private readonly List passiveModifierHandles = new(); + private readonly List passiveRuntimes = new(); private ISkillTreeStrategy strategy; private Entity entity; + public event System.Action OnPassiveTriggered; + + public int EquippedActiveCount => equippedActives != null ? equippedActives.Count : 0; + private void Awake() { entity = GetComponent(); @@ -63,6 +67,11 @@ private void Awake() RecalculatePassives(); } + private void OnDestroy() + { + ClearPassiveEffects(); + } + /// /// 트리 데이터를 결정한다. GameManager가 존재하고 선택된 직업의 트리가 /// 등록되어 있으면 그것을 사용하고, 아니면 직렬화된 treeData를 유지한다. @@ -90,13 +99,15 @@ public bool TryUnlockNode(SkillNodeData node) if (IsUnlocked(node)) { - Debug.Log($"[SkillTree] {node.Skill?.SkillName}은 이미 해금되어 있습니다."); + string skillName = node.Skill != null ? node.Skill.SkillName : string.Empty; + Debug.Log($"[SkillTree] {skillName}은 이미 해금되어 있습니다."); return false; } if (!CanUnlock(node)) { - Debug.Log($"[SkillTree] {node.Skill?.SkillName} 해금 조건 미충족."); + string skillName = node.Skill != null ? node.Skill.SkillName : string.Empty; + Debug.Log($"[SkillTree] {skillName} 해금 조건 미충족."); return false; } @@ -106,7 +117,11 @@ public bool TryUnlockNode(SkillNodeData node) unlockedState[node] = true; strategy.OnNodeUnlocked(node, unlockedState); - Debug.Log($"[SkillTree] {node.Skill?.SkillName} 해금 완료."); + if (node.TierIndex == 0) + RecalculatePassives(); + + string unlockedSkillName = node.Skill != null ? node.Skill.SkillName : string.Empty; + Debug.Log($"[SkillTree] {unlockedSkillName} 해금 완료."); return true; } @@ -116,9 +131,7 @@ public bool TryUnlockNode(SkillNodeData node) /// private void RecalculatePassives() { - foreach (EntityStatModifierHandle handle in passiveModifierHandles) - entity.RemoveStatModifier(handle); - passiveModifierHandles.Clear(); + ClearPassiveEffects(); foreach (PassiveSkillData passive in equippedPassives) { @@ -128,7 +141,193 @@ private void RecalculatePassives() EntityStatModifier modifier = EntityStatModifier.Zero; passive.ApplyPassive(ref modifier); passiveModifierHandles.Add(entity.ApplyStatModifier(modifier)); + + PassiveSkillRuntime runtime = passive.CreateRuntime( + new PassiveSkillRuntimeContext(this, entity) + ); + if (runtime != null) + passiveRuntimes.Add(runtime); + } + + BasicAttackSkillData basicAttack = GetUnlockedBasicAttack(); + if (basicAttack != null && basicAttack.BonusAttackSpeed != 0f) + { + EntityStatModifier modifier = EntityStatModifier.Zero; + modifier.BonusAttackSpeed = basicAttack.BonusAttackSpeed; + passiveModifierHandles.Add(entity.ApplyStatModifier(modifier)); + } + } + + public float GetActiveSkillCooldownMultiplier() + { + if (equippedPassives == null) + return 1f; + + float reduction = 0f; + foreach (PassiveSkillData passive in equippedPassives) + { + if (passive is IActiveSkillCooldownModifier cooldownModifier) + reduction += cooldownModifier.CooldownReductionRate; } + + return Mathf.Max(0f, 1f - reduction); + } + + public float GetOutgoingDamageMultiplier() + { + float multiplier = + 1f + SumPassiveBonus(m => m.OutgoingDamageBonusRate); + + if (WaveController.CurrentActiveEnemyCount == 1) + { + multiplier *= + 1f + SumPassiveBonus(m => m.SingleEnemyDamageBonusRate); + } + + return multiplier; + } + + public float GetActiveSkillDamageMultiplier(ActiveSkillData skill) + { + float multiplier = GetOutgoingDamageMultiplier(); + multiplier *= + 1f + SumPassiveBonus(m => m.ActiveSkillDamageBonusRate); + + ActiveSkillDamageCategory damageCategory = + skill != null ? skill.DamageCategory : ActiveSkillDamageCategory.None; + + if (damageCategory == ActiveSkillDamageCategory.SingleTarget) + { + multiplier *= + 1f + + SumPassiveBonus(m => + m.SingleTargetActiveSkillDamageBonusRate + ); + } + else if (damageCategory == ActiveSkillDamageCategory.Area) + { + multiplier *= + 1f + + SumPassiveBonus(m => + m.AreaActiveSkillDamageBonusRate + ); + + if (WaveController.CurrentActiveEnemyCount == 1) + { + multiplier *= + 1f + + SumPassiveBonus(m => + m.SingleEnemyAreaSkillDamageBonusRate + ); + } + } + + return multiplier; + } + + public float GetBasicAttackDamageMultiplier() + { + if (equippedPassives == null) + return 1f; + + float multiplier = 1f; + foreach (PassiveSkillData passive in equippedPassives) + { + if (passive is IBasicAttackDamageModifier damageModifier) + multiplier *= Mathf.Max(0f, damageModifier.BasicAttackDamageMultiplier); + } + + return multiplier; + } + + public float GetAttackAreaMultiplier() + { + return 1f + SumPassiveBonus(m => m.AttackAreaBonusRate); + } + + public float GetEliteDamageMultiplier() + { + return 1f + SumPassiveBonus(m => m.EliteDamageBonusRate); + } + + public void NotifyPassiveTriggered(PassiveTriggerType triggerType) + { + OnPassiveTriggered?.Invoke(triggerType); + } + + public bool AllowsBurnStacking() + { + if (equippedPassives == null) + return false; + + foreach (PassiveSkillData passive in equippedPassives) + { + if (passive is IBurnStackingModifier { AllowsBurnStacking: true }) + return true; + } + + return false; + } + + public float GetFrostEffectMultiplier() + { + return 1f + SumPassiveBonus(m => m.FrostEffectBonusRate); + } + + public bool TryGetFrostFreezeRule( + out int requiredStacks, + out float duration, + out EntityStatModifier modifier + ) + { + requiredStacks = 0; + duration = 0f; + modifier = EntityStatModifier.Zero; + + if (equippedPassives == null) + return false; + + foreach (PassiveSkillData passive in equippedPassives) + { + if (passive is not IFrostFreezeModifier freezeModifier) + continue; + + requiredStacks = freezeModifier.RequiredFrostStacks; + duration = freezeModifier.FreezeDuration; + modifier = freezeModifier.FreezeModifier; + return requiredStacks > 0 && duration > 0f; + } + + return false; + } + + private void ClearPassiveEffects() + { + foreach (PassiveSkillRuntime runtime in passiveRuntimes) + runtime.Dispose(); + passiveRuntimes.Clear(); + + foreach (EntityStatModifierHandle handle in passiveModifierHandles) + { + if (entity != null) + entity.RemoveStatModifier(handle); + } + passiveModifierHandles.Clear(); + } + + private float SumPassiveBonus(System.Func selector) + { + if (equippedPassives == null) + return 0f; + + float bonus = 0f; + foreach (PassiveSkillData passive in equippedPassives) + { + if (passive is T modifier) + bonus += selector(modifier); + } + + return bonus; } // ─── 액티브 스킬 장착 ───────────────────────────────────────── @@ -229,6 +428,30 @@ public bool CanUnlock(SkillNodeData node) private SkillNodeData FindNodeBySkill(SkillData skill) { - return treeData?.AllNodes?.FirstOrDefault(n => n.Skill == skill); + if (treeData == null || treeData.AllNodes == null) + return null; + + foreach (SkillNodeData node in treeData.AllNodes) + { + if (node != null && node.Skill == skill) + return node; + } + + return null; + } + + public BasicAttackSkillData GetUnlockedBasicAttack() + { + if (unlockedState == null) + return null; + + foreach (var kvp in unlockedState) + { + if (kvp.Value && kvp.Key != null && kvp.Key.TierIndex == 0) + { + return kvp.Key.Skill as BasicAttackSkillData; + } + } + return null; } } diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills.meta new file mode 100644 index 00000000..5a39a40e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4e2142a09f936014cb9b17c83161d2fd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills.meta new file mode 100644 index 00000000..7702eb0c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0eb34d056c057584b94709ffaa926bc9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherContinuousShootActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherContinuousShootActiveSkill.cs new file mode 100644 index 00000000..1bae20cd --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherContinuousShootActiveSkill.cs @@ -0,0 +1,72 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +[CreateAssetMenu( + fileName = "ArcherContinuousShootActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Archer Continuous Shoot" +)] +public sealed class ArcherContinuousShootActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f), Tooltip("화살 한 발당 데미지 배율")] + private float damageRate = 1.0f; + + [SerializeField, Min(1), Tooltip("발사할 화살 개수")] + private int arrowCount = 3; + + [SerializeField, Min(0f), Tooltip("화살 간 발사 간격 (초)")] + private float shotInterval = 0.1f; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) + && context + .CollectTargets(ActiveSkillTargetMode.DetectedTargets, 1, ExtensionRange) + .Count > 0; + } + + protected override void UseSkill(ActiveSkillContext context) + { + var targets = context.CollectTargets( + ActiveSkillTargetMode.DetectedTargets, + 1, + ExtensionRange + ); + if (targets.Count == 0) + return; + + Entity target = targets[0]; + if (target != null && target.Hp > 0f) + { + context.Executor.StartCoroutine(ShootRoutine(context, target)); + } + } + + private IEnumerator ShootRoutine(ActiveSkillContext context, Entity target) + { + int shotsFired = 0; + while ( + shotsFired < arrowCount && target != null && target.Hp > 0f && context.Caster != null + ) + { + float rawDamage = context.Caster.AttackPower * damageRate; + rawDamage = context.GetModifiedDamage(rawDamage); + + (float damage, _) = DamageCalculator.Calculate( + rawDamage, + target.DamageReduction, + context.Caster.CriticalPercentage, + context.Caster.CriticalDamageMultiplier + ); + + float actualDamage = target.TakeDamage(damage, context.Caster); + context.Caster.NotifyDamageDealt(target, actualDamage); + + shotsFired++; + if (shotsFired < arrowCount) + { + yield return YieldInstructionCache.WaitForSeconds(shotInterval); + } + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherContinuousShootActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherContinuousShootActiveSkill.cs.meta new file mode 100644 index 00000000..9884bb73 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherContinuousShootActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2e0069c3058e42b4ba42ea73d7d05d15 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherEvadeActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherEvadeActiveSkill.cs new file mode 100644 index 00000000..091e39be --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherEvadeActiveSkill.cs @@ -0,0 +1,105 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ArcherEvadeActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Archer Evade Buff" +)] +public sealed class ArcherEvadeActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f), Tooltip("회피 버프 지속 시간")] + private float duration = 10f; + + [SerializeField, Min(0f), Tooltip("회피 성공 시 반격 데미지 배율 (예: 1.2 = 120%)")] + private float counterDamageRate = 1.2f; + + protected override void UseSkill(ActiveSkillContext context) + { + if (context.Caster == null) + return; + + EvadeTracker tracker = context.Caster.GetComponent(); + if (tracker == null) + { + tracker = context.Caster.gameObject.AddComponent(); + tracker.Initialize(context.Caster, duration, counterDamageRate); + } + else + { + tracker.Refresh(duration, counterDamageRate); + } + } + + private sealed class EvadeTracker : MonoBehaviour + { + private Entity caster; + private float counterDamageRate; + private bool isCleanedUp; + + public void Initialize(Entity caster, float duration, float counterDamageRate) + { + this.caster = caster; + this.counterDamageRate = counterDamageRate; + + caster.OnBeforeDamageTaken += HandleBeforeDamageTaken; + Invoke(nameof(Cleanup), duration); + } + + public void Refresh(float duration, float counterDamageRate) + { + this.counterDamageRate = counterDamageRate; + CancelInvoke(nameof(Cleanup)); + Invoke(nameof(Cleanup), duration); + } + + private void HandleBeforeDamageTaken(IncomingDamageContext damageContext) + { + if (damageContext == null || damageContext.IsCancelled) + return; + + damageContext.Cancel(); + Debug.Log("[추적] 피해 회피! 반격 시작"); + + if (damageContext.Attacker != null && damageContext.Attacker.Hp > 0f && caster != null) + { + float rawDamage = caster.AttackPower * counterDamageRate; + if (caster.TryGetComponent(out SkillTreeComponent skillTree)) + { + rawDamage *= skillTree.GetOutgoingDamageMultiplier(); + } + rawDamage *= caster.OutgoingDamageMultiplier; + + (float damage, _) = DamageCalculator.Calculate( + rawDamage, + damageContext.Attacker.DamageReduction, + caster.CriticalPercentage, + caster.CriticalDamageMultiplier + ); + + float actualDamage = damageContext.Attacker.TakeDamage(damage, caster); + caster.NotifyDamageDealt(damageContext.Attacker, actualDamage); + } + } + + private void Cleanup() + { + if (isCleanedUp) + return; + + isCleanedUp = true; + if (caster != null) + { + caster.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + } + Destroy(this); + } + + private void OnDestroy() + { + CancelInvoke(); + if (!isCleanedUp && caster != null) + { + caster.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + } + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherEvadeActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherEvadeActiveSkill.cs.meta new file mode 100644 index 00000000..150b35ed --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherEvadeActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fa06e7b2449485d419fc089d7d55a35c \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherFocusActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherFocusActiveSkill.cs new file mode 100644 index 00000000..6bc88d7b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherFocusActiveSkill.cs @@ -0,0 +1,69 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ArcherFocusActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Archer Focus" +)] +public sealed class ArcherFocusActiveSkill : ActiveSkillData +{ + [SerializeField, Tooltip("다음 공격 데미지 배율 (예: 1.5 = 1.5배)")] + private float damageMultiplier = 1.5f; + + protected override void UseSkill(ActiveSkillContext context) + { + if (context.Caster == null) + return; + + // 공격력 배율 증가 버프 적용 + EntityStatModifier modifier = EntityStatModifier.Zero; + modifier.BonusOutgoingDamageRate = damageMultiplier - 1f; + + EntityStatModifierHandle handle = context.Caster.ApplyStatModifier(modifier); + + // 다음 공격 성공 시 버프를 해제하는 헬퍼 컴포넌트 부착 + FocusTracker tracker = context.Caster.gameObject.AddComponent(); + tracker.Initialize(context.Caster, handle); + } + + private sealed class FocusTracker : MonoBehaviour + { + private Entity caster; + private EntityStatModifierHandle modifierHandle; + private bool isTriggered; + + public void Initialize(Entity caster, EntityStatModifierHandle modifierHandle) + { + this.caster = caster; + this.modifierHandle = modifierHandle; + caster.OnDamageDealt += HandleDamageDealt; + } + + private void HandleDamageDealt(Entity target, float damage) + { + if (isTriggered) + return; + + isTriggered = true; + Cleanup(); + Destroy(this); + } + + private void Cleanup() + { + if (caster != null) + { + caster.OnDamageDealt -= HandleDamageDealt; + if (modifierHandle.IsValid) + { + caster.RemoveStatModifier(modifierHandle); + modifierHandle = default; + } + } + } + + private void OnDestroy() + { + Cleanup(); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherFocusActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherFocusActiveSkill.cs.meta new file mode 100644 index 00000000..38463e97 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherFocusActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 027d0163e15e38f41abcb0f6f52c157a \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherHeadshotActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherHeadshotActiveSkill.cs new file mode 100644 index 00000000..5eb82098 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherHeadshotActiveSkill.cs @@ -0,0 +1,48 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ArcherHeadshotActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Archer Headshot" +)] +public sealed class ArcherHeadshotActiveSkill : ActiveSkillData +{ + [SerializeField, Range(0f, 1f), Tooltip("타겟의 최대 체력 대비 피해량 비율 (예: 0.1 = 10%)")] + private float targetMaxHpDamageRate = 0.1f; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) + && context + .CollectTargets(ActiveSkillTargetMode.DetectedTargets, 1, ExtensionRange) + .Count > 0; + } + + protected override void UseSkill(ActiveSkillContext context) + { + var targets = context.CollectTargets( + ActiveSkillTargetMode.DetectedTargets, + 1, + ExtensionRange + ); + if (targets.Count == 0) + return; + + Entity target = targets[0]; + if (target == null || target.Hp <= 0f) + return; + + // 최대 체력 비례 데미지 계산 + float rawDamage = target.MaxHp * targetMaxHpDamageRate; + rawDamage = context.GetModifiedDamage(rawDamage); + + (float damage, _) = DamageCalculator.Calculate( + rawDamage, + target.DamageReduction, + context.Caster.CriticalPercentage, + context.Caster.CriticalDamageMultiplier + ); + + float actualDamage = target.TakeDamage(damage, context.Caster); + context.Caster.NotifyDamageDealt(target, actualDamage); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherHeadshotActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherHeadshotActiveSkill.cs.meta new file mode 100644 index 00000000..6272840e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherHeadshotActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e6c16a09d9c6adf4282199253da8eeed \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherToggleActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherToggleActiveSkill.cs new file mode 100644 index 00000000..ea89f57d --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherToggleActiveSkill.cs @@ -0,0 +1,93 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ArcherToggleActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Archer Toggle Buff" +)] +public sealed class ArcherToggleActiveSkill : ActiveSkillData +{ + [SerializeField, Tooltip("버프 활성화 시 보너스 공격 속도")] + private float bonusAttackSpeed = 1.0f; + + [SerializeField, Tooltip("버프 활성화 시 일반 공격 추가 타겟 수")] + private int bonusTargetCount = 99; + + [SerializeField, Tooltip("버프 활성화 시 받는 피해 증가 배율 (0.5 = 50% 증가)")] + private float incomingDamageIncreaseRate = 0.5f; + + protected override void UseSkill(ActiveSkillContext context) + { + if (context.Caster == null) + return; + + ToggleStateTracker tracker = context.Caster.GetComponent(); + if (tracker == null) + { + // Toggle ON + tracker = context.Caster.gameObject.AddComponent(); + tracker.Initialize( + context.Caster, + bonusAttackSpeed, + bonusTargetCount, + incomingDamageIncreaseRate + ); + Debug.Log( + $"[부동자세] 활성화 (공속 +{bonusAttackSpeed}, 타겟 +{bonusTargetCount}, 받는피해 +{incomingDamageIncreaseRate * 100}%)" + ); + } + else + { + // Toggle OFF + Destroy(tracker); + Debug.Log("[부동자세] 비활성화"); + } + } + + private sealed class ToggleStateTracker : MonoBehaviour + { + private Entity caster; + private EntityStatModifierHandle modifierHandle; + private float incomingDamageIncreaseRate; + + public void Initialize( + Entity caster, + float bonusAttackSpeed, + int bonusTargetCount, + float incomingDamageIncreaseRate + ) + { + this.caster = caster; + this.incomingDamageIncreaseRate = incomingDamageIncreaseRate; + + // 스탯 버프 적용 (공속 및 기본공격 타겟 수) + EntityStatModifier modifier = EntityStatModifier.Zero; + modifier.BonusAttackSpeed = bonusAttackSpeed; + modifier.BonusBasicAttackTargetCount = bonusTargetCount; + + modifierHandle = caster.ApplyStatModifier(modifier); + + // 받는 피해 증가 이벤트 구독 + caster.OnBeforeDamageTaken += HandleBeforeDamageTaken; + } + + private void HandleBeforeDamageTaken(IncomingDamageContext damageContext) + { + if (damageContext == null || damageContext.IsCancelled) + return; + + damageContext.Damage *= 1f + incomingDamageIncreaseRate; + } + + private void OnDestroy() + { + if (caster != null) + { + caster.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + if (modifierHandle.IsValid) + { + caster.RemoveStatModifier(modifierHandle); + } + } + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherToggleActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherToggleActiveSkill.cs.meta new file mode 100644 index 00000000..5d0e8711 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArcherToggleActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cfc3196a34b79c446aea0b228fa4cd90 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArrowRainActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArrowRainActiveSkill.cs new file mode 100644 index 00000000..98cfc63a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArrowRainActiveSkill.cs @@ -0,0 +1,128 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +[CreateAssetMenu( + fileName = "ArrowRainActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Arrow Rain" +)] +public sealed class ArrowRainActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f), Tooltip("지속 시간 동안 입힐 총 기본 피해량")] + private float totalBaseDamage = 50f; + + [SerializeField, Min(0f), Tooltip("지속 시간 동안 입힐 총 공격력 계수")] + private float totalAttackPowerRate = 2.0f; + + [SerializeField, Min(0.1f), Tooltip("장판 지속 시간")] + private float duration = 3.0f; + + [SerializeField, Min(0.1f), Tooltip("피해 틱 주기 (초)")] + private float tickInterval = 0.5f; + + [SerializeField, Range(0f, 1f), Tooltip("이동 속도 감소 비율 (0.5 = 50% 감소)")] + private float slowRate = 0.5f; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) + && context + .CollectTargets(ActiveSkillTargetMode.DetectedTargets, 1, ExtensionRange) + .Count > 0; + } + + protected override void UseSkill(ActiveSkillContext context) + { + var targets = context.CollectTargets( + ActiveSkillTargetMode.DetectedTargets, + 1, + ExtensionRange + ); + if (targets.Count == 0) + return; + + Vector3 targetPosition = targets[0].transform.position; + context.Executor.StartCoroutine(HazardRoutine(context, targetPosition)); + } + + private IEnumerator HazardRoutine(ActiveSkillContext context, Vector3 centerPosition) + { + int tickCount = Mathf.Max(1, Mathf.RoundToInt(duration / tickInterval)); + float baseDamagePerTick = totalBaseDamage / tickCount; + float attackPowerRatePerTick = totalAttackPowerRate / tickCount; + + float elapsed = 0f; + List targetsBuffer = new List(); + List allEnemies = new List(); + + // 이동 속도 감소 수치 정의 + EntityStatModifier slowModifier = EntityStatModifier.Zero; + slowModifier.BonusMoveSpeedRate = -slowRate; + + while (elapsed < duration && context.Caster != null) + { + // 전체 적 중 장판 범위 내에 있는지 판정 + WaveController.TryCollectActiveEnemies(allEnemies); + + float attackAreaMultiplier = + context.SkillTreeComponent != null + ? context.SkillTreeComponent.GetAttackAreaMultiplier() + : 1f; + float searchRange = ExtensionRange * attackAreaMultiplier; + float searchRangeSqr = searchRange * searchRange; + + targetsBuffer.Clear(); + for (int i = 0; i < allEnemies.Count; i++) + { + Entity enemy = allEnemies[i]; + if (enemy == null || enemy.Hp <= 0f) + continue; + + float sqrDist = (enemy.transform.position - centerPosition).sqrMagnitude; + if (sqrDist <= searchRangeSqr) + { + targetsBuffer.Add(enemy); + } + } + + float rawDamage = + baseDamagePerTick + context.Caster.AttackPower * attackPowerRatePerTick; + rawDamage = context.GetModifiedDamage(rawDamage); + + for (int i = 0; i < targetsBuffer.Count; i++) + { + Entity target = targetsBuffer[i]; + if (target == null || target.Hp <= 0f) + continue; + + // 슬로우 디버프 적용 (다음 틱 전에 풀리지 않도록 1.5배의 지속시간 부여) + StatusEffectController controller = StatusEffectController.GetOrAdd(target); + if (controller != null) + { + controller.ApplyModifierEffect( + StatusEffectType.MoveSpeedDown, + context.Caster, + slowModifier, + tickInterval * 1.5f, + false, + 1 + ); + } + + // 피해 판정 + (float damage, _) = DamageCalculator.Calculate( + rawDamage, + target.DamageReduction, + context.Caster.CriticalPercentage, + context.Caster.CriticalDamageMultiplier + ); + + float actualDamage = target.TakeDamage(damage, context.Caster); + context.Caster.NotifyDamageDealt(target, actualDamage); + } + + yield return YieldInstructionCache.WaitForSeconds(tickInterval); + elapsed += tickInterval; + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArrowRainActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArrowRainActiveSkill.cs.meta new file mode 100644 index 00000000..3dde7d3f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ArrowRainActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4415e54686e93be41b9f8b1beab02d17 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/AwakeningActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/AwakeningActiveSkill.cs new file mode 100644 index 00000000..65c8fecd --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/AwakeningActiveSkill.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "AwakeningActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Warrior Awakening" +)] +public sealed class AwakeningActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f)] + private float duration = 20f; + + [SerializeField] + private EntityStatModifier awakeningModifier = new() + { + BonusOutgoingDamageRate = 0.2f, + BonusCriticalPercentage = 50f, + BonusAttackSpeed = 0.5f, + BonusDamageReduction = 0.1f, + }; + + public override bool UsableOncePerDungeon => true; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) && duration > 0f; + } + + protected override void UseSkill(ActiveSkillContext context) + { + context.Executor.ApplyTemporaryModifier(awakeningModifier, duration); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/AwakeningActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/AwakeningActiveSkill.cs.meta new file mode 100644 index 00000000..f059fd45 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/AwakeningActiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ddf55e731a924b0ab887d60fa540b096 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CloneDamageActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CloneDamageActiveSkill.cs new file mode 100644 index 00000000..3ce13b71 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CloneDamageActiveSkill.cs @@ -0,0 +1,75 @@ +using System.Collections; +using UnityEngine; + +[CreateAssetMenu( + fileName = "CloneDamageActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Warrior Clone Damage" +)] +public sealed class CloneDamageActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f)] + private float duration = 10f; + + [SerializeField, Min(0f)] + private float copiedDamageRate = 0.5f; + + private sealed class Runtime + { + private readonly ActiveSkillContext context; + private readonly float duration; + private readonly float copiedDamageRate; + private bool isDisposed; + + public Runtime(ActiveSkillContext context, float duration, float copiedDamageRate) + { + this.context = context; + this.duration = duration; + this.copiedDamageRate = copiedDamageRate; + } + + public void Start() + { + context.Caster.OnDamageDealt += HandleDamageDealt; + context.Executor.RegisterRuntimeCleanup(Dispose); + context.Executor.StartCoroutine(DisposeAfterDuration()); + } + + private void HandleDamageDealt(Entity target, float damage) + { + if (target == null || target.Hp <= 0f || damage <= 0f) + return; + + target.TakeDamage(damage * copiedDamageRate, context.Caster); + } + + private IEnumerator DisposeAfterDuration() + { + yield return YieldInstructionCache.WaitForSeconds(duration); + + Dispose(); + } + + private void Dispose() + { + if (isDisposed) + return; + + isDisposed = true; + if (context.Caster != null) + context.Caster.OnDamageDealt -= HandleDamageDealt; + + if (context.Executor != null) + context.Executor.UnregisterRuntimeCleanup(Dispose); + } + } + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) && duration > 0f && copiedDamageRate > 0f; + } + + protected override void UseSkill(ActiveSkillContext context) + { + new Runtime(context, duration, copiedDamageRate).Start(); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CloneDamageActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CloneDamageActiveSkill.cs.meta new file mode 100644 index 00000000..bdf1cbb8 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CloneDamageActiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 84f90799265e4d7cbf4a3b17ceded4fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CooldownResetActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CooldownResetActiveSkill.cs new file mode 100644 index 00000000..5b1838fe --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CooldownResetActiveSkill.cs @@ -0,0 +1,13 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "CooldownResetActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Cooldown Reset" +)] +public sealed class CooldownResetActiveSkill : ActiveSkillData +{ + protected override void UseSkill(ActiveSkillContext context) + { + context.Executor.ResetCooldownsExcept(context.SlotIndex); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CooldownResetActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CooldownResetActiveSkill.cs.meta new file mode 100644 index 00000000..c8036404 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/CooldownResetActiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3310de53e63f43348e6a0a749c4bcd55 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/DamageReflectionActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/DamageReflectionActiveSkill.cs new file mode 100644 index 00000000..75ddba40 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/DamageReflectionActiveSkill.cs @@ -0,0 +1,91 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +[CreateAssetMenu( + fileName = "DamageReflectionActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Warrior Damage Reflection" +)] +public sealed class DamageReflectionActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f)] + private float duration = 3f; + + [SerializeField, Min(0f)] + private float reflectedDamageMultiplier = 2f; + + private sealed class Runtime + { + private readonly ActiveSkillContext context; + private readonly float duration; + private readonly float reflectedDamageMultiplier; + private readonly List targets = new(); + private float accumulatedDamage; + private bool isDisposed; + + public Runtime(ActiveSkillContext context, float duration, float reflectedDamageMultiplier) + { + this.context = context; + this.duration = duration; + this.reflectedDamageMultiplier = reflectedDamageMultiplier; + } + + public void Start() + { + context.Caster.OnDamageTaken += HandleDamageTaken; + context.Executor.RegisterRuntimeCleanup(Dispose); + context.Executor.StartCoroutine(ReflectAfterDuration()); + } + + private void HandleDamageTaken(float previousHp, float currentHp) + { + accumulatedDamage += Mathf.Max(0f, previousHp - currentHp); + } + + private IEnumerator ReflectAfterDuration() + { + yield return YieldInstructionCache.WaitForSeconds(duration); + + Dispose(); + if (context.Caster == null || accumulatedDamage <= 0f) + yield break; + + if (!WaveController.TryCollectActiveEnemies(targets)) + yield break; + + float damage = context.GetModifiedDamage(accumulatedDamage * reflectedDamageMultiplier); + for (int i = 0; i < targets.Count; i++) + { + Entity target = targets[i]; + if (target == null || target.Hp <= 0f) + continue; + + float actualDamage = target.TakeDamage(damage, context.Caster); + context.Caster.NotifyDamageDealt(target, actualDamage); + } + } + + private void Dispose() + { + if (isDisposed) + return; + + isDisposed = true; + if (context.Caster != null) + context.Caster.OnDamageTaken -= HandleDamageTaken; + + if (context.Executor != null) + context.Executor.UnregisterRuntimeCleanup(Dispose); + } + } + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) && duration > 0f; + } + + protected override void UseSkill(ActiveSkillContext context) + { + new Runtime(context, duration, reflectedDamageMultiplier).Start(); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/DamageReflectionActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/DamageReflectionActiveSkill.cs.meta new file mode 100644 index 00000000..4bab7736 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/DamageReflectionActiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d7b8e5695644c62a413e3ff350a1fc1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/EndureActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/EndureActiveSkill.cs new file mode 100644 index 00000000..17f3da8c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/EndureActiveSkill.cs @@ -0,0 +1,27 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "EndureActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Warrior Endure" +)] +public sealed class EndureActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f)] + private float damageReductionPerEnemy = 0.3f; + + [SerializeField, Min(0f)] + private float duration = 5f; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) && duration > 0f; + } + + protected override void UseSkill(ActiveSkillContext context) + { + EntityStatModifier modifier = EntityStatModifier.Zero; + modifier.BonusDamageReduction = + WaveController.CurrentActiveEnemyCount * damageReductionPerEnemy; + context.Executor.ApplyTemporaryModifier(modifier, duration); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/EndureActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/EndureActiveSkill.cs.meta new file mode 100644 index 00000000..5a6796ab --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/EndureActiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c5f4ee6e7c5497ab2ad794df2e0bbd2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ExecuteActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ExecuteActiveSkill.cs new file mode 100644 index 00000000..9c105e7a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ExecuteActiveSkill.cs @@ -0,0 +1,49 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ExecuteActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Warrior Execute" +)] +public sealed class ExecuteActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f)] + private float baseDamage = 200f; + + [SerializeField, Min(0f)] + private float missingHpDamageRate = 0.2f; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) + && context + .CollectTargets(ActiveSkillTargetMode.DetectedTargets, 1, ExtensionRange) + .Count > 0; + } + + protected override void UseSkill(ActiveSkillContext context) + { + var targets = context.CollectTargets( + ActiveSkillTargetMode.DetectedTargets, + 1, + ExtensionRange + ); + if (targets.Count == 0) + return; + + Entity target = targets[0]; + if (target == null || target.Hp <= 0f) + return; + + float missingHp = Mathf.Max(0f, target.MaxHp - target.Hp); + float rawDamage = context.GetModifiedDamage(baseDamage + missingHp * missingHpDamageRate); + (float damage, _) = DamageCalculator.Calculate( + rawDamage, + target.DamageReduction, + context.Caster.CriticalPercentage, + context.Caster.CriticalDamageMultiplier + ); + + float actualDamage = target.TakeDamage(damage, context.Caster); + context.Caster.NotifyDamageDealt(target, actualDamage); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ExecuteActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ExecuteActiveSkill.cs.meta new file mode 100644 index 00000000..88c7d88a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/ExecuteActiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4504379b98eb4f05bb32dd52d02beb48 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/StatBuffActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/StatBuffActiveSkill.cs new file mode 100644 index 00000000..26082c06 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/StatBuffActiveSkill.cs @@ -0,0 +1,24 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "StatBuffActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Stat Buff" +)] +public sealed class StatBuffActiveSkill : ActiveSkillData +{ + [SerializeField] + private EntityStatModifier modifier; + + [SerializeField, Min(0f)] + private float duration = 5f; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) && duration > 0f; + } + + protected override void UseSkill(ActiveSkillContext context) + { + context.Executor.ApplyTemporaryModifier(modifier, duration); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/StatBuffActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/StatBuffActiveSkill.cs.meta new file mode 100644 index 00000000..658f49d3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/StatBuffActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 040e06596a3f4feb9fc3b889fdf7d6cc diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/TargetedDamageActiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/TargetedDamageActiveSkill.cs new file mode 100644 index 00000000..45144fbf --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/TargetedDamageActiveSkill.cs @@ -0,0 +1,116 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "TargetedDamageActiveSkill", + menuName = "ScriptableObjects/SkillTree/Active/Targeted Damage" +)] +public sealed class TargetedDamageActiveSkill : ActiveSkillData +{ + [SerializeField, Min(0f)] + private float baseDamage; + + [SerializeField, Min(0f)] + private float attackPowerRate; + + [SerializeField, Min(1)] + private int maxTargets = 1; + + [SerializeField] + private ActiveSkillTargetMode targetMode = ActiveSkillTargetMode.DetectedTargets; + + [SerializeField] + private bool guaranteedCritical; + + [SerializeField, Range(0f, 1f)] + private float lifeStealRate; + + [SerializeField] + private bool applyStatusEffect; + + [SerializeField] + private StatusEffectType statusEffectType; + + [SerializeField, Min(0f)] + private float statusEffectDuration = 5f; + + [SerializeField] + private EntityStatModifier statusModifier; + + [SerializeField, Min(0f)] + private float incomingDamageBonusRate; + + protected override bool CanUseSkill(ActiveSkillContext context) + { + return base.CanUseSkill(context) + && context.CollectTargets(targetMode, maxTargets, ExtensionRange).Count > 0; + } + + protected override void UseSkill(ActiveSkillContext context) + { + var targets = context.CollectTargets(targetMode, maxTargets, ExtensionRange); + float rawDamage = baseDamage + context.Caster.AttackPower * attackPowerRate; + rawDamage = context.GetModifiedDamage(rawDamage); + + float totalActualDamage = 0f; + for (int i = 0; i < targets.Count; i++) + { + Entity target = targets[i]; + if (target == null || target.Hp <= 0f) + continue; + + float criticalChance = guaranteedCritical ? 100f : context.Caster.CriticalPercentage; + (float damage, _) = DamageCalculator.Calculate( + rawDamage, + target.DamageReduction, + criticalChance, + context.Caster.CriticalDamageMultiplier + ); + + float actualDamage = target.TakeDamage(damage, context.Caster); + totalActualDamage += actualDamage; + context.Caster.NotifyDamageDealt(target, actualDamage); + + ApplyStatusEffect(context.Caster, target); + } + + if (lifeStealRate > 0f && totalActualDamage > 0f) + context.Caster.Heal(totalActualDamage * lifeStealRate); + } + + private void ApplyStatusEffect(Entity source, Entity target) + { + if (!applyStatusEffect || target == null || statusEffectDuration <= 0f) + return; + + StatusEffectController controller = StatusEffectController.GetOrAdd(target); + if (controller == null) + return; + + if (statusEffectType == StatusEffectType.Stun) + { + controller.ApplyActionBlock(StatusEffectType.Stun, source, statusEffectDuration); + return; + } + + if (statusEffectType == StatusEffectType.IncomingDamageUp) + { + controller.ApplyIncomingDamageUp( + source, + incomingDamageBonusRate, + statusEffectDuration, + false, + 1 + ); + return; + } + + controller.ApplyModifierEffect( + statusEffectType, + source, + statusModifier, + statusEffectDuration, + false, + 1 + ); + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/TargetedDamageActiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/TargetedDamageActiveSkill.cs.meta new file mode 100644 index 00000000..ff353596 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/ActiveSkills/TargetedDamageActiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cdbce97dd4097ce42a08397728ce416b \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/BasicAttackSkillData.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/BasicAttackSkillData.cs new file mode 100644 index 00000000..7ad18383 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/BasicAttackSkillData.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +/// +/// 스킬 트리에서 기본 평타(Tier 0)를 정의하는 데이터 클래스. +/// +[CreateAssetMenu( + fileName = "BasicAttackSkillData", + menuName = "ScriptableObjects/SkillTree/BasicAttack" +)] +public class BasicAttackSkillData : SkillData +{ + [SerializeField, Min(0f), Tooltip("기본 공격력 배율 (예: 1.0 = 100%)")] + private float damageRate = 1f; + public float DamageRate => damageRate; + + [SerializeField, Min(1), Tooltip("공격 가능한 최대 타겟 수")] + private int maxTargets = 1; + public int MaxTargets => maxTargets; + + [SerializeField, Tooltip("장착(해금) 시 영구적으로 증가하는 기본 공격 속도")] + private float bonusAttackSpeed = 0f; + public float BonusAttackSpeed => bonusAttackSpeed; + + [ + SerializeField, + Min(0f), + Tooltip("인식 사거리 너머 추가 타격 반경 (0이면 인식 사거리 내만 공격)") + ] + private float extensionRange = 0f; + public float ExtensionRange => extensionRange; + + public override void UseSkill() + { + // 평타는 AutoAttackController 및 EntityAttackBehaviour에 의해 구동되므로 직접 UseSkill을 호출하지 않습니다. + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/BasicAttackSkillData.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/BasicAttackSkillData.cs.meta new file mode 100644 index 00000000..86c70b37 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/BasicAttackSkillData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1e78177b50544ff4d9c909f44451d436 \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills.meta new file mode 100644 index 00000000..ca69710f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 34d01395847d39844aab4c4434d499a3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ActiveSkillDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ActiveSkillDamagePassiveSkill.cs new file mode 100644 index 00000000..0af7c597 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ActiveSkillDamagePassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ActiveSkillDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Active Skill Damage" +)] +public sealed class ActiveSkillDamagePassiveSkill : PassiveSkillData, IActiveSkillDamageModifier +{ + [SerializeField, Min(0f)] + private float activeSkillDamageBonusRate; + + public float ActiveSkillDamageBonusRate => activeSkillDamageBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ActiveSkillDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ActiveSkillDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..dabc3d87 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ActiveSkillDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 132d26f865664db6995d12e02027f9eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AreaActiveSkillDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AreaActiveSkillDamagePassiveSkill.cs new file mode 100644 index 00000000..e0fa6125 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AreaActiveSkillDamagePassiveSkill.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "AreaActiveSkillDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Area Active Skill Damage" +)] +public sealed class AreaActiveSkillDamagePassiveSkill + : PassiveSkillData, + IAreaActiveSkillDamageModifier +{ + [SerializeField, Min(0f)] + private float areaActiveSkillDamageBonusRate; + + public float AreaActiveSkillDamageBonusRate => areaActiveSkillDamageBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AreaActiveSkillDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AreaActiveSkillDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..3af26d5c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AreaActiveSkillDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d4c570914bd14d29a48ae81baf07c38c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AttackAreaPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AttackAreaPassiveSkill.cs new file mode 100644 index 00000000..c3414420 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AttackAreaPassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "AttackAreaPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Attack Area" +)] +public sealed class AttackAreaPassiveSkill : PassiveSkillData, IAttackAreaModifier +{ + [SerializeField, Min(0f)] + private float attackAreaBonusRate; + + public float AttackAreaBonusRate => attackAreaBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AttackAreaPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AttackAreaPassiveSkill.cs.meta new file mode 100644 index 00000000..8160f543 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/AttackAreaPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3670337be01450c9c5da2c163395f52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BasicAttackDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BasicAttackDamagePassiveSkill.cs new file mode 100644 index 00000000..90bc33c8 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BasicAttackDamagePassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "BasicAttackDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Basic Attack Damage" +)] +public sealed class BasicAttackDamagePassiveSkill : PassiveSkillData, IBasicAttackDamageModifier +{ + [SerializeField, Min(0f)] + private float basicAttackDamageMultiplier = 1f; + + public float BasicAttackDamageMultiplier => basicAttackDamageMultiplier; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BasicAttackDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BasicAttackDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..0cbe711a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BasicAttackDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 53cbcfb3cd4f4714924cbfbdc40f4646 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnOnHitPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnOnHitPassiveSkill.cs new file mode 100644 index 00000000..c968a36f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnOnHitPassiveSkill.cs @@ -0,0 +1,104 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "BurnOnHitPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Burn On Hit" +)] +public sealed class BurnOnHitPassiveSkill : PassiveSkillData +{ + [SerializeField, Min(0f)] + private float damagePerSecond; + + [SerializeField, Min(0f)] + private float attackPowerDamageRatePerSecond; + + [SerializeField, Min(0f)] + private float targetMaxHpDamageRatePerSecond; + + [SerializeField, Min(0f)] + private float duration = 3f; + + [SerializeField, Min(1)] + private int maxStacks = 5; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime( + context, + damagePerSecond, + attackPowerDamageRatePerSecond, + targetMaxHpDamageRatePerSecond, + duration, + maxStacks + ); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float damagePerSecond; + private readonly float attackPowerDamageRatePerSecond; + private readonly float targetMaxHpDamageRatePerSecond; + private readonly float duration; + private readonly int maxStacks; + + public Runtime( + PassiveSkillRuntimeContext context, + float damagePerSecond, + float attackPowerDamageRatePerSecond, + float targetMaxHpDamageRatePerSecond, + float duration, + int maxStacks + ) + : base(context) + { + this.damagePerSecond = damagePerSecond; + this.attackPowerDamageRatePerSecond = attackPowerDamageRatePerSecond; + this.targetMaxHpDamageRatePerSecond = targetMaxHpDamageRatePerSecond; + this.duration = duration; + this.maxStacks = maxStacks; + + if (Context.Owner != null) + Context.Owner.OnDamageDealt += HandleDamageDealt; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnDamageDealt -= HandleDamageDealt; + } + + private void HandleDamageDealt(Entity target, float _) + { + if (target == null || Context.Owner == null) + return; + + float finalDamagePerSecond = + damagePerSecond + Context.Owner.AttackPower * attackPowerDamageRatePerSecond; + float finalTargetMaxHpDamageRatePerSecond = targetMaxHpDamageRatePerSecond; + if (Context.SkillTreeComponent != null) + { + finalDamagePerSecond *= Context.SkillTreeComponent.GetOutgoingDamageMultiplier(); + finalTargetMaxHpDamageRatePerSecond *= + Context.SkillTreeComponent.GetOutgoingDamageMultiplier(); + } + + StatusEffectController controller = StatusEffectController.GetOrAdd(target); + if (controller == null) + return; + + bool allowsBurnStacking = + Context.SkillTreeComponent != null + && Context.SkillTreeComponent.AllowsBurnStacking(); + controller.ApplyBurn( + Context.Owner, + finalDamagePerSecond, + finalTargetMaxHpDamageRatePerSecond, + duration, + allowsBurnStacking, + maxStacks + ); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnOnHitPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnOnHitPassiveSkill.cs.meta new file mode 100644 index 00000000..4e8ba426 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnOnHitPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c8f3e12d3df4c098d4d0e4b37e33b38 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnStackingPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnStackingPassiveSkill.cs new file mode 100644 index 00000000..4f7bd408 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnStackingPassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "BurnStackingPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Burn Stacking" +)] +public sealed class BurnStackingPassiveSkill : PassiveSkillData, IBurnStackingModifier +{ + [SerializeField] + private bool allowsBurnStacking = true; + + public bool AllowsBurnStacking => allowsBurnStacking; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnStackingPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnStackingPassiveSkill.cs.meta new file mode 100644 index 00000000..40584f16 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnStackingPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 46374c47f4f14d1eb8fd89ecc42d63e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnTargetBonusDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnTargetBonusDamagePassiveSkill.cs new file mode 100644 index 00000000..c5c72c98 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnTargetBonusDamagePassiveSkill.cs @@ -0,0 +1,52 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "BurnTargetBonusDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Burn Target Bonus Damage" +)] +public sealed class BurnTargetBonusDamagePassiveSkill : PassiveSkillData +{ + [SerializeField, Min(0f)] + private float bonusDamageRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, bonusDamageRate); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float bonusDamageRate; + + public Runtime(PassiveSkillRuntimeContext context, float bonusDamageRate) + : base(context) + { + this.bonusDamageRate = bonusDamageRate; + + if (Context.Owner != null) + Context.Owner.OnDamageDealt += HandleDamageDealt; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnDamageDealt -= HandleDamageDealt; + } + + private void HandleDamageDealt(Entity target, float damage) + { + if (target == null || damage <= 0f) + return; + + if ( + target.TryGetComponent(out StatusEffectController controller) + && controller.HasEffect(StatusEffectType.Burn) + ) + { + target.TakeDamage(damage * bonusDamageRate, Context.Owner); + } + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnTargetBonusDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnTargetBonusDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..516ecc9a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/BurnTargetBonusDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e116b81fb15a44fdb6ae9a3dd1f1b7eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ChanceDamageIgnorePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ChanceDamageIgnorePassiveSkill.cs new file mode 100644 index 00000000..1c57cd8e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ChanceDamageIgnorePassiveSkill.cs @@ -0,0 +1,47 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ChanceDamageIgnorePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Chance Damage Ignore" +)] +public sealed class ChanceDamageIgnorePassiveSkill : PassiveSkillData +{ + [SerializeField, Range(0f, 1f)] + private float ignoreChance; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, ignoreChance); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float ignoreChance; + + public Runtime(PassiveSkillRuntimeContext context, float ignoreChance) + : base(context) + { + this.ignoreChance = ignoreChance; + + if (Context.Owner != null) + Context.Owner.OnBeforeDamageTaken += HandleBeforeDamageTaken; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + } + + private void HandleBeforeDamageTaken(IncomingDamageContext damageContext) + { + if (damageContext == null || damageContext.IsCancelled) + return; + + if (Random.value < ignoreChance) + damageContext.Cancel(); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ChanceDamageIgnorePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ChanceDamageIgnorePassiveSkill.cs.meta new file mode 100644 index 00000000..6322336d --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ChanceDamageIgnorePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b116667572ea43a795b6f0f0a55f28ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/CooldownReductionPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/CooldownReductionPassiveSkill.cs new file mode 100644 index 00000000..13d2d943 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/CooldownReductionPassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "CooldownReductionPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Cooldown Reduction" +)] +public sealed class CooldownReductionPassiveSkill : PassiveSkillData, IActiveSkillCooldownModifier +{ + [SerializeField, Range(0f, 1f)] + private float cooldownReductionRate; + + public float CooldownReductionRate => cooldownReductionRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/CooldownReductionPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/CooldownReductionPassiveSkill.cs.meta new file mode 100644 index 00000000..cfa7b59c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/CooldownReductionPassiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0aff9f200441458aa7efb6ef8fd87091 diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/EliteDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/EliteDamagePassiveSkill.cs new file mode 100644 index 00000000..834eb226 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/EliteDamagePassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "EliteDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Elite Damage" +)] +public sealed class EliteDamagePassiveSkill : PassiveSkillData, IEliteDamageModifier +{ + [SerializeField, Min(0f)] + private float eliteDamageBonusRate; + + public float EliteDamageBonusRate => eliteDamageBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/EliteDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/EliteDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..9300883e --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/EliteDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d22709fc9b91408093b36082baf6289b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ExtraHitPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ExtraHitPassiveSkill.cs new file mode 100644 index 00000000..2b158af9 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ExtraHitPassiveSkill.cs @@ -0,0 +1,54 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "ExtraHitPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Extra Hit" +)] +public sealed class ExtraHitPassiveSkill : PassiveSkillData +{ + [SerializeField, Range(0f, 1f)] + private float triggerChance = 1f; + + [SerializeField, Min(0f)] + private float damageRate = 1f; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, triggerChance, damageRate); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float triggerChance; + private readonly float damageRate; + + public Runtime(PassiveSkillRuntimeContext context, float triggerChance, float damageRate) + : base(context) + { + this.triggerChance = triggerChance; + this.damageRate = damageRate; + + if (Context.Owner != null) + Context.Owner.OnDamageDealt += HandleDamageDealt; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnDamageDealt -= HandleDamageDealt; + } + + private void HandleDamageDealt(Entity target, float damage) + { + if (target == null || target.Hp <= 0f || damage <= 0f) + return; + + if (Random.value > triggerChance) + return; + + target.TakeDamage(damage * damageRate, Context.Owner); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ExtraHitPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ExtraHitPassiveSkill.cs.meta new file mode 100644 index 00000000..9db13e3b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/ExtraHitPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57fac38fae2a4f7290f9dc04af758928 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostEffectBonusPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostEffectBonusPassiveSkill.cs new file mode 100644 index 00000000..622dbb2f --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostEffectBonusPassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "FrostEffectBonusPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Frost Effect Bonus" +)] +public sealed class FrostEffectBonusPassiveSkill : PassiveSkillData, IFrostEffectBonusModifier +{ + [SerializeField, Min(0f)] + private float frostEffectBonusRate; + + public float FrostEffectBonusRate => frostEffectBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostEffectBonusPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostEffectBonusPassiveSkill.cs.meta new file mode 100644 index 00000000..7fa2c1c5 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostEffectBonusPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d1180d444934dffb006270db4204c7d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostFreezePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostFreezePassiveSkill.cs new file mode 100644 index 00000000..fbbe8845 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostFreezePassiveSkill.cs @@ -0,0 +1,23 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "FrostFreezePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Frost Freeze" +)] +public sealed class FrostFreezePassiveSkill : PassiveSkillData, IFrostFreezeModifier +{ + [SerializeField, Min(1)] + private int requiredFrostStacks = 3; + + [SerializeField, Min(0f)] + private float freezeDuration = 1f; + + [SerializeField] + private EntityStatModifier freezeModifier; + + public int RequiredFrostStacks => requiredFrostStacks; + public float FreezeDuration => freezeDuration; + public EntityStatModifier FreezeModifier => freezeModifier; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostFreezePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostFreezePassiveSkill.cs.meta new file mode 100644 index 00000000..ca5bf077 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostFreezePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb0e751247bb4f1c8e40674a6de9ce01 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostOnHitPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostOnHitPassiveSkill.cs new file mode 100644 index 00000000..bd29b160 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostOnHitPassiveSkill.cs @@ -0,0 +1,101 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "FrostOnHitPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Frost On Hit" +)] +public sealed class FrostOnHitPassiveSkill : PassiveSkillData +{ + [SerializeField] + private EntityStatModifier frostModifier; + + [SerializeField, Min(0f)] + private float duration = 3f; + + [SerializeField, Min(1)] + private int maxStacks = 5; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, frostModifier, duration, maxStacks); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly EntityStatModifier frostModifier; + private readonly float duration; + private readonly int maxStacks; + + public Runtime( + PassiveSkillRuntimeContext context, + EntityStatModifier frostModifier, + float duration, + int maxStacks + ) + : base(context) + { + this.frostModifier = frostModifier; + this.duration = duration; + this.maxStacks = maxStacks; + + if (Context.Owner != null) + Context.Owner.OnDamageDealt += HandleDamageDealt; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnDamageDealt -= HandleDamageDealt; + } + + private void HandleDamageDealt(Entity target, float _) + { + if (target == null) + return; + + StatusEffectController controller = StatusEffectController.GetOrAdd(target); + if (controller == null) + return; + + float frostEffectMultiplier = + Context.SkillTreeComponent != null + ? Context.SkillTreeComponent.GetFrostEffectMultiplier() + : 1f; + EntityStatModifier scaledModifier = EntityStatModifierUtility.Scale( + frostModifier, + frostEffectMultiplier + ); + controller.ApplyModifierEffect( + StatusEffectType.Frost, + Context.Owner, + scaledModifier, + duration, + true, + maxStacks + ); + + if ( + Context.SkillTreeComponent != null + && Context.SkillTreeComponent.TryGetFrostFreezeRule( + out int requiredStacks, + out float freezeDuration, + out EntityStatModifier freezeModifier + ) + && controller.GetStackCount(StatusEffectType.Frost) >= requiredStacks + ) + { + controller.RemoveAll(StatusEffectType.Frost); + controller.ApplyModifierEffect( + StatusEffectType.Freeze, + Context.Owner, + freezeModifier, + freezeDuration, + false, + 1 + ); + } + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostOnHitPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostOnHitPassiveSkill.cs.meta new file mode 100644 index 00000000..b98af5a4 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/FrostOnHitPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cb0d540c28948fa87261b1de4b61409 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/KillStackAttackPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/KillStackAttackPassiveSkill.cs new file mode 100644 index 00000000..857a2f9d --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/KillStackAttackPassiveSkill.cs @@ -0,0 +1,81 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "KillStackAttackPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Kill Stack Attack" +)] +public sealed class KillStackAttackPassiveSkill : PassiveSkillData +{ + [SerializeField] + private EntityStatModifier modifierPerStack; + + [SerializeField, Min(0)] + private int maxStack; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, modifierPerStack, maxStack); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly EntityStatModifier modifierPerStack; + private readonly int maxStack; + private EntityStatModifierHandle modifierHandle; + private int currentStack; + + public Runtime( + PassiveSkillRuntimeContext context, + EntityStatModifier modifierPerStack, + int maxStack + ) + : base(context) + { + this.modifierPerStack = modifierPerStack; + this.maxStack = maxStack; + WaveController.OnEnemyDiedGlobal += HandleEnemyDied; + } + + public override void Dispose() + { + WaveController.OnEnemyDiedGlobal -= HandleEnemyDied; + + if (Context.Owner != null && modifierHandle.IsValid) + Context.Owner.RemoveStatModifier(modifierHandle); + + modifierHandle = default; + } + + private void HandleEnemyDied(Enemy _) + { + if (Context.Owner == null || Context.Owner.Hp <= 0f) + return; + + if (maxStack > 0 && currentStack >= maxStack) + return; + + currentStack++; + EntityStatModifier modifier = ScaleModifier(modifierPerStack, currentStack); + + if (modifierHandle.IsValid) + { + if (!Context.Owner.UpdateStatModifier(modifierHandle, modifier)) + { + Context.Owner.RemoveStatModifier(modifierHandle); + modifierHandle = Context.Owner.ApplyStatModifier(modifier); + } + } + else + { + modifierHandle = Context.Owner.ApplyStatModifier(modifier); + } + } + + private static EntityStatModifier ScaleModifier(EntityStatModifier modifier, int stack) + { + return EntityStatModifierUtility.Scale(modifier, stack); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/KillStackAttackPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/KillStackAttackPassiveSkill.cs.meta new file mode 100644 index 00000000..4e6aedb6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/KillStackAttackPassiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 049d4e3d2f4a48dabfd4dc22a15854be diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthDamageHealPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthDamageHealPassiveSkill.cs new file mode 100644 index 00000000..351f775c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthDamageHealPassiveSkill.cs @@ -0,0 +1,56 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "LowHealthDamageHealPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Low Health Damage Heal" +)] +public sealed class LowHealthDamageHealPassiveSkill : PassiveSkillData +{ + [SerializeField, Range(0f, 1f)] + private float healthThreshold = 0.5f; + + [SerializeField, Range(0f, 1f)] + private float damageHealRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, healthThreshold, damageHealRate); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float healthThreshold; + private readonly float damageHealRate; + + public Runtime( + PassiveSkillRuntimeContext context, + float healthThreshold, + float damageHealRate + ) + : base(context) + { + this.healthThreshold = healthThreshold; + this.damageHealRate = damageHealRate; + Context.Owner.OnDamageDealt += HandleDamageDealt; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnDamageDealt -= HandleDamageDealt; + } + + private void HandleDamageDealt(Entity _, float damage) + { + if (Context.Owner == null || Context.Owner.MaxHp <= 0f) + return; + + if (Context.Owner.Hp / Context.Owner.MaxHp > healthThreshold) + return; + + Context.Owner.Heal(damage * damageHealRate); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthDamageHealPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthDamageHealPassiveSkill.cs.meta new file mode 100644 index 00000000..07a3a3d8 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthDamageHealPassiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: be420bc1b73d40d2a8cf9843d088fbcf diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthInvincibilityPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthInvincibilityPassiveSkill.cs new file mode 100644 index 00000000..50e40ddf --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthInvincibilityPassiveSkill.cs @@ -0,0 +1,121 @@ +using System.Collections; +using UnityEngine; + +[CreateAssetMenu( + fileName = "LowHealthInvincibilityPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Low Health Invincibility" +)] +public sealed class LowHealthInvincibilityPassiveSkill : PassiveSkillData +{ + [SerializeField, Range(0f, 1f)] + private float healthThreshold = 0.01f; + + [SerializeField, Min(0f)] + private float duration = 10f; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, healthThreshold, duration); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float healthThreshold; + private readonly float duration; + private bool isInvincible; + private bool canTrigger = true; + private Coroutine removeCoroutine; + + public Runtime(PassiveSkillRuntimeContext context, float healthThreshold, float duration) + : base(context) + { + this.healthThreshold = healthThreshold; + this.duration = duration; + + if (Context.Owner != null) + { + Context.Owner.OnUpdated += Refresh; + Context.Owner.OnBeforeDamageTaken += HandleBeforeDamageTaken; + } + } + + public override void Dispose() + { + if (Context.Owner != null) + { + Context.Owner.OnUpdated -= Refresh; + Context.Owner.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + } + + StopRemoveCoroutine(); + isInvincible = false; + } + + private void Refresh() + { + if (Context.Owner == null || Context.Owner.MaxHp <= 0f) + return; + + float healthRatio = Context.Owner.Hp / Context.Owner.MaxHp; + if (healthRatio > healthThreshold) + canTrigger = true; + + if (!canTrigger || isInvincible || healthRatio > healthThreshold) + return; + + Activate(); + } + + private void HandleBeforeDamageTaken(IncomingDamageContext damageContext) + { + if (damageContext == null || damageContext.IsCancelled) + return; + + if (isInvincible) + { + damageContext.Cancel(); + return; + } + + if (!canTrigger || Context.Owner == null || Context.Owner.MaxHp <= 0f) + return; + + float thresholdHp = Context.Owner.MaxHp * healthThreshold; + float predictedHp = Context.Owner.Hp - damageContext.Damage; + if (predictedHp > thresholdHp) + return; + + Activate(); + if (isInvincible) + damageContext.Damage = Mathf.Max(0f, Context.Owner.Hp - thresholdHp); + } + + private void Activate() + { + if (duration <= 0f || Context.SkillTreeComponent == null) + return; + + canTrigger = false; + isInvincible = true; + removeCoroutine = Context.SkillTreeComponent.StartCoroutine(RemoveAfterDuration()); + } + + private IEnumerator RemoveAfterDuration() + { + yield return YieldInstructionCache.WaitForSeconds(duration); + removeCoroutine = null; + isInvincible = false; + } + + private void StopRemoveCoroutine() + { + if (removeCoroutine == null || Context.SkillTreeComponent == null) + return; + + Context.SkillTreeComponent.StopCoroutine(removeCoroutine); + removeCoroutine = null; + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthInvincibilityPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthInvincibilityPassiveSkill.cs.meta new file mode 100644 index 00000000..ae4e3c26 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthInvincibilityPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1d5fbf3fb09e40c3b7754287bb5a410d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthRecoveryPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthRecoveryPassiveSkill.cs new file mode 100644 index 00000000..e8dd5d70 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthRecoveryPassiveSkill.cs @@ -0,0 +1,74 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "LowHealthRecoveryPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Low Health Recovery" +)] +public sealed class LowHealthRecoveryPassiveSkill : PassiveSkillData +{ + [SerializeField, Range(0f, 1f)] + private float healthThreshold = 0.5f; + + [SerializeField] + private EntityStatModifier recoveryModifier; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, healthThreshold, recoveryModifier); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float healthThreshold; + private readonly EntityStatModifier recoveryModifier; + private EntityStatModifierHandle modifierHandle; + private bool isApplied; + + public Runtime( + PassiveSkillRuntimeContext context, + float healthThreshold, + EntityStatModifier recoveryModifier + ) + : base(context) + { + this.healthThreshold = healthThreshold; + this.recoveryModifier = recoveryModifier; + Context.Owner.OnUpdated += Refresh; + Refresh(); + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnUpdated -= Refresh; + + if (Context.Owner != null && modifierHandle.IsValid) + Context.Owner.RemoveStatModifier(modifierHandle); + + modifierHandle = default; + } + + private void Refresh() + { + if (Context.Owner == null || Context.Owner.MaxHp <= 0f) + return; + + bool shouldApply = Context.Owner.Hp / Context.Owner.MaxHp <= healthThreshold; + if (shouldApply == isApplied) + return; + + isApplied = shouldApply; + if (isApplied) + { + modifierHandle = Context.Owner.ApplyStatModifier(recoveryModifier); + } + else if (modifierHandle.IsValid) + { + Context.Owner.RemoveStatModifier(modifierHandle); + modifierHandle = default; + } + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthRecoveryPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthRecoveryPassiveSkill.cs.meta new file mode 100644 index 00000000..44de13c3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/LowHealthRecoveryPassiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e7bf5697f6dd45a38c1e56c160fa8cf9 diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/MultishotPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/MultishotPassiveSkill.cs new file mode 100644 index 00000000..fe3c3613 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/MultishotPassiveSkill.cs @@ -0,0 +1,21 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "MultishotPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Multishot" +)] +public sealed class MultishotPassiveSkill : PassiveSkillData, IBasicAttackDamageModifier +{ + [SerializeField, Min(0f), Tooltip("일반 공격의 개별 화살 데미지 배율 (예: 0.75 = 75%)")] + private float basicAttackDamageMultiplier = 0.75f; + + [SerializeField, Min(1), Tooltip("추가되는 일반 공격 대상 수")] + private int bonusTargets = 1; + + public float BasicAttackDamageMultiplier => basicAttackDamageMultiplier; + + public override void ApplyPassive(ref EntityStatModifier modifier) + { + modifier.BonusBasicAttackTargetCount += bonusTargets; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/MultishotPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/MultishotPassiveSkill.cs.meta new file mode 100644 index 00000000..606d1b68 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/MultishotPassiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: afbe1788d0c38c34aae58ba76b01ed9d \ No newline at end of file diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/RiskRewardPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/RiskRewardPassiveSkill.cs new file mode 100644 index 00000000..ab7503f3 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/RiskRewardPassiveSkill.cs @@ -0,0 +1,51 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "RiskRewardPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Risk Reward" +)] +public sealed class RiskRewardPassiveSkill : PassiveSkillData, IOutgoingDamageModifier +{ + [SerializeField, Min(0f)] + private float outgoingDamageBonusRate = 0.5f; + + [SerializeField, Min(0f)] + private float incomingDamageBonusRate = 0.3f; + + public float OutgoingDamageBonusRate => outgoingDamageBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, incomingDamageBonusRate); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float incomingDamageBonusRate; + + public Runtime(PassiveSkillRuntimeContext context, float incomingDamageBonusRate) + : base(context) + { + this.incomingDamageBonusRate = incomingDamageBonusRate; + + if (Context.Owner != null) + Context.Owner.OnBeforeDamageTaken += HandleBeforeDamageTaken; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + } + + private void HandleBeforeDamageTaken(IncomingDamageContext damageContext) + { + if (damageContext == null || damageContext.IsCancelled) + return; + + damageContext.Damage *= 1f + incomingDamageBonusRate; + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/RiskRewardPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/RiskRewardPassiveSkill.cs.meta new file mode 100644 index 00000000..3f38fc62 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/RiskRewardPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e40e79c5790440adbd1801f074e6781d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyAreaSkillDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyAreaSkillDamagePassiveSkill.cs new file mode 100644 index 00000000..b98b0a48 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyAreaSkillDamagePassiveSkill.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "SingleEnemyAreaSkillDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Single Enemy Area Skill Damage" +)] +public sealed class SingleEnemyAreaSkillDamagePassiveSkill + : PassiveSkillData, + ISingleEnemyAreaSkillDamageModifier +{ + [SerializeField, Min(0f)] + private float singleEnemyAreaSkillDamageBonusRate; + + public float SingleEnemyAreaSkillDamageBonusRate => singleEnemyAreaSkillDamageBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyAreaSkillDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyAreaSkillDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..273eea69 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyAreaSkillDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1f58f6e0a70c4d3db755e0bd9f4f1b7f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyDamagePassiveSkill.cs new file mode 100644 index 00000000..3aa79ea0 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyDamagePassiveSkill.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "SingleEnemyDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Single Enemy Damage" +)] +public sealed class SingleEnemyDamagePassiveSkill : PassiveSkillData, ISingleEnemyDamageModifier +{ + [SerializeField, Min(0f)] + private float singleEnemyDamageBonusRate; + + public float SingleEnemyDamageBonusRate => singleEnemyDamageBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..24891365 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleEnemyDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23647314e6eb47e48a6e991c063837b7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleTargetActiveSkillDamagePassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleTargetActiveSkillDamagePassiveSkill.cs new file mode 100644 index 00000000..2f0e8ab6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleTargetActiveSkillDamagePassiveSkill.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "SingleTargetActiveSkillDamagePassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Single Target Active Skill Damage" +)] +public sealed class SingleTargetActiveSkillDamagePassiveSkill + : PassiveSkillData, + ISingleTargetActiveSkillDamageModifier +{ + [SerializeField, Min(0f)] + private float singleTargetActiveSkillDamageBonusRate; + + public float SingleTargetActiveSkillDamageBonusRate => singleTargetActiveSkillDamageBonusRate; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleTargetActiveSkillDamagePassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleTargetActiveSkillDamagePassiveSkill.cs.meta new file mode 100644 index 00000000..1d03b1c2 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/SingleTargetActiveSkillDamagePassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ed9f7aeb00e4e798dc9dfb2d82c0173 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/StatModifierPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/StatModifierPassiveSkill.cs new file mode 100644 index 00000000..0ac597c6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/StatModifierPassiveSkill.cs @@ -0,0 +1,16 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "StatModifierPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Stat Modifier" +)] +public sealed class StatModifierPassiveSkill : PassiveSkillData +{ + [SerializeField] + private EntityStatModifier modifier; + + public override void ApplyPassive(ref EntityStatModifier targetModifier) + { + targetModifier += modifier; + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/StatModifierPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/StatModifierPassiveSkill.cs.meta new file mode 100644 index 00000000..0d9aaa2b --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/StatModifierPassiveSkill.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b7ec831eb812409db2fe187c6081ead2 diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WaveOnceDamageReductionPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WaveOnceDamageReductionPassiveSkill.cs new file mode 100644 index 00000000..de440dc7 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WaveOnceDamageReductionPassiveSkill.cs @@ -0,0 +1,104 @@ +using System.Collections; +using UnityEngine; + +[CreateAssetMenu( + fileName = "WaveOnceDamageReductionPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Wave Once Damage Reduction" +)] +public sealed class WaveOnceDamageReductionPassiveSkill : PassiveSkillData +{ + [SerializeField] + private EntityStatModifier damageReductionModifier; + + [SerializeField, Min(0f)] + private float duration = 10f; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, damageReductionModifier, duration); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly EntityStatModifier damageReductionModifier; + private readonly float duration; + private EntityStatModifierHandle handle; + private Coroutine removeCoroutine; + private bool usedInCurrentWave; + + public Runtime( + PassiveSkillRuntimeContext context, + EntityStatModifier damageReductionModifier, + float duration + ) + : base(context) + { + this.damageReductionModifier = damageReductionModifier; + this.duration = duration; + + if (Context.Owner != null) + Context.Owner.OnBeforeDamageTaken += HandleBeforeDamageTaken; + + WaveController.OnWaveStartedGlobal += ResetWaveUsage; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnBeforeDamageTaken -= HandleBeforeDamageTaken; + + WaveController.OnWaveStartedGlobal -= ResetWaveUsage; + RemoveModifier(); + } + + private void HandleBeforeDamageTaken(IncomingDamageContext damageContext) + { + if ( + usedInCurrentWave + || damageContext == null + || damageContext.IsCancelled + || Context.Owner == null + ) + { + return; + } + + usedInCurrentWave = true; + if (handle.IsValid) + return; + + handle = Context.Owner.ApplyStatModifier(damageReductionModifier); + + if (duration > 0f) + removeCoroutine = Context.SkillTreeComponent.StartCoroutine(RemoveAfterDuration()); + } + + private IEnumerator RemoveAfterDuration() + { + yield return YieldInstructionCache.WaitForSeconds(duration); + removeCoroutine = null; + RemoveModifier(); + } + + private void ResetWaveUsage() + { + usedInCurrentWave = false; + } + + private void RemoveModifier() + { + if (removeCoroutine != null && Context.SkillTreeComponent != null) + { + Context.SkillTreeComponent.StopCoroutine(removeCoroutine); + removeCoroutine = null; + } + + if (Context.Owner != null && handle.IsValid) + Context.Owner.RemoveStatModifier(handle); + + handle = default; + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WaveOnceDamageReductionPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WaveOnceDamageReductionPassiveSkill.cs.meta new file mode 100644 index 00000000..8c4f674a --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WaveOnceDamageReductionPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fdffde80a01f4e5d9422ab0b983b8ee4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarCooldownReductionPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarCooldownReductionPassiveSkill.cs new file mode 100644 index 00000000..ed6b6d61 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarCooldownReductionPassiveSkill.cs @@ -0,0 +1,54 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "WindScarCooldownReductionPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Wind Scar Cooldown Reduction" +)] +public sealed class WindScarCooldownReductionPassiveSkill : PassiveSkillData +{ + [ + SerializeField, + Range(0f, 1f), + Tooltip("칼바람 발동 시 모든 스킬 총 쿨타임 감소 비율 (예: 0.333 = 1/3 감소)") + ] + private float cooldownReductionRate = 0.333f; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, cooldownReductionRate); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly float cooldownReductionRate; + private readonly ActiveSkillExecutor executor; + + public Runtime(PassiveSkillRuntimeContext context, float cooldownReductionRate) + : base(context) + { + this.cooldownReductionRate = cooldownReductionRate; + executor = + context.Owner != null ? context.Owner.GetComponent() : null; + + if (Context.SkillTreeComponent != null) + Context.SkillTreeComponent.OnPassiveTriggered += HandlePassiveTriggered; + } + + public override void Dispose() + { + if (Context.SkillTreeComponent != null) + Context.SkillTreeComponent.OnPassiveTriggered -= HandlePassiveTriggered; + } + + private void HandlePassiveTriggered(PassiveTriggerType triggerType) + { + if (triggerType != PassiveTriggerType.WindScar || cooldownReductionRate <= 0f) + return; + + if (executor != null) + executor.ReduceAllCooldownsPercent(cooldownReductionRate); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarCooldownReductionPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarCooldownReductionPassiveSkill.cs.meta new file mode 100644 index 00000000..86294f5c --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarCooldownReductionPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 393884dc9cf44566b16f6b9aa8fb7067 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarPassiveSkill.cs b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarPassiveSkill.cs new file mode 100644 index 00000000..ff0e7822 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarPassiveSkill.cs @@ -0,0 +1,64 @@ +using UnityEngine; + +[CreateAssetMenu( + fileName = "WindScarPassiveSkill", + menuName = "ScriptableObjects/SkillTree/Passive/Wind Scar" +)] +public sealed class WindScarPassiveSkill : PassiveSkillData +{ + [SerializeField, Min(1)] + private int requiredHits = 5; + + [SerializeField, Min(0f)] + private float damageRate = 1f; + + public override void ApplyPassive(ref EntityStatModifier modifier) { } + + public override PassiveSkillRuntime CreateRuntime(PassiveSkillRuntimeContext context) + { + return new Runtime(context, requiredHits, damageRate); + } + + private sealed class Runtime : PassiveSkillRuntime + { + private readonly int requiredHits; + private readonly float damageRate; + private int hitCount; + + public Runtime(PassiveSkillRuntimeContext context, int requiredHits, float damageRate) + : base(context) + { + this.requiredHits = Mathf.Max(1, requiredHits); + this.damageRate = damageRate; + + if (Context.Owner != null) + Context.Owner.OnDamageDealt += HandleDamageDealt; + } + + public override void Dispose() + { + if (Context.Owner != null) + Context.Owner.OnDamageDealt -= HandleDamageDealt; + } + + private void HandleDamageDealt(Entity target, float _) + { + if (target == null || Context.Owner == null) + return; + + hitCount++; + if (hitCount < requiredHits) + return; + + hitCount = 0; + float damage = Context.Owner.AttackPower * damageRate; + if (Context.SkillTreeComponent != null) + damage *= Context.SkillTreeComponent.GetOutgoingDamageMultiplier(); + + target.TakeDamage(damage, Context.Owner); + + if (Context.SkillTreeComponent != null) + Context.SkillTreeComponent.NotifyPassiveTriggered(PassiveTriggerType.WindScar); + } + } +} diff --git a/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarPassiveSkill.cs.meta b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarPassiveSkill.cs.meta new file mode 100644 index 00000000..73b5d8dd --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/Core/SkillTree/Skills/PassiveSkills/WindScarPassiveSkill.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 091e67121f3b480aaa9679a8edca0e0c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset deleted file mode 100644 index a9adaba5..00000000 --- a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/BasicSkill.asset +++ /dev/null @@ -1,19 +0,0 @@ -%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: 0c074805eb6fdc84eb032f40ad30f1e5, type: 3} - m_Name: BasicSkill - m_EditorClassIdentifier: - SkillName: - SkillIcon: {fileID: 0} - SkillDescription: - Cooldown: 0 - Damage: 0 diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill.meta new file mode 100644 index 00000000..59110e36 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fd3f310ad5634a0429e2021d53cba984 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Pierce.asset b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Pierce.asset similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Pierce.asset rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Pierce.asset diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Pierce.asset.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Pierce.asset.meta similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Pierce.asset.meta rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Pierce.asset.meta diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/QuickStone.asset b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/QuickStone.asset similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/QuickStone.asset rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/QuickStone.asset diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/QuickStone.asset.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/QuickStone.asset.meta similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/QuickStone.asset.meta rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/QuickStone.asset.meta diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Slash.asset b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Slash.asset similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Slash.asset rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Slash.asset diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Slash.asset.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Slash.asset.meta similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Slash.asset.meta rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/Slash.asset.meta diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/SpinSlash.asset b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/SpinSlash.asset similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/SpinSlash.asset rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/SpinSlash.asset diff --git a/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/SpinSlash.asset.meta b/Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/SpinSlash.asset.meta similarity index 100% rename from Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/SpinSlash.asset.meta rename to Fantasy-Grower/Assets/Scripts/SO/SkillData/Warrior/Skill/SpinSlash.asset.meta diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_1Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_1Stat.asset index d4f90c08..753f89e2 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_1Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_1Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Goblin_1Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 20 HpRecovery: 0 DamageReduction: 0 AttackPower: 2 AttackSpeed: 0.8 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_2Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_2Stat.asset index 20a7c2bb..33b217cc 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_2Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_2Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Goblin_2Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 24 HpRecovery: 0 DamageReduction: 0 AttackPower: 2.5 AttackSpeed: 0.9 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_3Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_3Stat.asset index d474bce1..bacc06a7 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_3Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Goblin_3Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Goblin_3Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 30 HpRecovery: 0 DamageReduction: 0.01 AttackPower: 3 AttackSpeed: 1 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/KingStat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/KingStat.asset index 63c47e91..a6fccda7 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/KingStat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/KingStat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: KingStat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 300 HpRecovery: 0 DamageReduction: 0.15 AttackPower: 30 AttackSpeed: 0.8 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_1Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_1Stat.asset index 57148580..8f40b297 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_1Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_1Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Orc_1Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 38 HpRecovery: 0 DamageReduction: 0.02 AttackPower: 4 AttackSpeed: 0.65 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_2Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_2Stat.asset index 6b0e9bb3..67185209 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_2Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_2Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Orc_2Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 46 HpRecovery: 0 DamageReduction: 0.03 AttackPower: 5 AttackSpeed: 0.7 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_3Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_3Stat.asset index a48c0037..b781117b 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_3Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Orc_3Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Orc_3Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 56 HpRecovery: 0 DamageReduction: 0.05 AttackPower: 6 AttackSpeed: 0.75 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_1Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_1Stat.asset index c670e937..437de987 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_1Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_1Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Skeleton_1Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 70 HpRecovery: 0 DamageReduction: 0.06 AttackPower: 7 AttackSpeed: 0.85 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_2Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_2Stat.asset index 065c5a6a..79ea8782 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_2Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_2Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Skeleton_2Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 85 HpRecovery: 0 DamageReduction: 0.08 AttackPower: 8.5 AttackSpeed: 0.95 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_3Stat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_3Stat.asset index a0c9468b..f09ef50c 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_3Stat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/Skeleton_3Stat.asset @@ -11,10 +11,11 @@ MonoBehaviour: m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 5df7739d4b9038a4d9cb6566170314b6, type: 3} m_Name: Skeleton_3Stat - m_EditorClassIdentifier: + m_EditorClassIdentifier: Hp: 105 HpRecovery: 0 DamageReduction: 0.1 AttackPower: 10 AttackSpeed: 1.05 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset index df8dc660..0a2551b4 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Enemy/TestEnemyStat.asset @@ -13,9 +13,9 @@ MonoBehaviour: m_Name: TestEnemyStat m_EditorClassIdentifier: Hp: 150 + HpRecovery: 0 DamageReduction: 0 AttackPower: 10 AttackSpeed: 1 - AttackRange: 2.5 CriticalPercentage: 0 - EntityType: 1 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/ArcherStat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/ArcherStat.asset index 82b19cbd..e50da6d7 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/ArcherStat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/ArcherStat.asset @@ -13,8 +13,9 @@ MonoBehaviour: m_Name: ArcherStat m_EditorClassIdentifier: Hp: 30 - HpRecovery: 15 + HpRecovery: 5 DamageReduction: 0.15 AttackPower: 8 AttackSpeed: 2.3 CriticalPercentage: 0 + AttackRange: 3 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset index 2f60a720..83d9bb44 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WarriorStat.asset @@ -13,8 +13,9 @@ MonoBehaviour: m_Name: WarriorStat m_EditorClassIdentifier: Hp: 50 - HpRecovery: 20 + HpRecovery: 7 DamageReduction: 0.25 AttackPower: 5 AttackSpeed: 1.5 CriticalPercentage: 0 + AttackRange: 1 diff --git a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WizardStat.asset b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WizardStat.asset index 2c28d844..6b9dfe78 100644 --- a/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WizardStat.asset +++ b/Fantasy-Grower/Assets/Scripts/SO/StatData/Player/WizardStat.asset @@ -13,8 +13,9 @@ MonoBehaviour: m_Name: WizardStat m_EditorClassIdentifier: Hp: 20 - HpRecovery: 18 + HpRecovery: 6 DamageReduction: 0.08 AttackPower: 10 AttackSpeed: 1 CriticalPercentage: 0 + AttackRange: 2.5 diff --git a/Fantasy-Grower/Assets/Scripts/UI/InDungeon/ActiveSkillButtonUI.cs b/Fantasy-Grower/Assets/Scripts/UI/InDungeon/ActiveSkillButtonUI.cs new file mode 100644 index 00000000..f9df69b6 --- /dev/null +++ b/Fantasy-Grower/Assets/Scripts/UI/InDungeon/ActiveSkillButtonUI.cs @@ -0,0 +1,135 @@ +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +/// +/// 모바일 던전 UI에서 액티브 스킬 슬롯 하나를 실행하는 버튼입니다. +/// +[DisallowMultipleComponent] +[RequireComponent(typeof(Button))] +public sealed class ActiveSkillButtonUI : MonoBehaviour +{ + [SerializeField, Min(0)] + private int slotIndex; + + [Header("표시")] + [SerializeField] + private Image iconImage; + + [SerializeField] + private Image cooldownFillImage; + + [SerializeField] + private TMP_Text cooldownText; + + [SerializeField] + private GameObject emptySlotObject; + + private ActiveSkillExecutor cachedExecutor; + + private Button button; + private ActiveSkillData currentSkill; + + private void Awake() + { + button = GetComponent