2026-04-08 스킬시스템 진행중, 폴더구조 변경등
This commit is contained in:
@@ -1 +1 @@
|
||||
public enum PlayerState { Idle, Walk, Run, Dodge, Jump, Fall, Attack, Charge, Hit, Dead, Inertia, Action, Trans, None }
|
||||
public enum PlayerState { Idle, Walk, Run, Dodge, Jump, Fall, Attack, Charge, Cast, Channel, Hit, Dead, Inertia, Action, Trans, None }
|
||||
@@ -57,7 +57,7 @@ public void SetMaxJumpCount(int maxCount)
|
||||
|
||||
#region 상태확인용 헬퍼함수들
|
||||
//지상 이동이 가능한가?
|
||||
public bool CanMove() => IsGrounded && !IsMoveCut && (CurrentState == PlayerState.Idle || CurrentState == PlayerState.Walk || CurrentState == PlayerState.Run) && (CurrentState != PlayerState.Inertia && CurrentState != PlayerState.Action && CurrentState != PlayerState.Trans);
|
||||
public bool CanMove() => IsGrounded && !IsMoveCut && (CurrentState == PlayerState.Idle || CurrentState == PlayerState.Walk || CurrentState == PlayerState.Run) && (CurrentState != PlayerState.Inertia && CurrentState != PlayerState.Action && CurrentState != PlayerState.Trans && CurrentState != PlayerState.Cast && CurrentState != PlayerState.Channel);
|
||||
//점프가 가능한 상태인가?
|
||||
public bool CanJump()
|
||||
{
|
||||
@@ -81,6 +81,7 @@ public bool CanDodge()
|
||||
if (CurrentState == PlayerState.Inertia) return false;
|
||||
if (CurrentState == PlayerState.Action) return false;
|
||||
if (CurrentState == PlayerState.Trans) return false;
|
||||
if (CurrentState == PlayerState.Cast || CurrentState == PlayerState.Channel) return false;
|
||||
|
||||
// 스태미나 시스템이 있다면 체크
|
||||
// if (CurrentStamina < DodgeCost) return false;
|
||||
@@ -92,8 +93,9 @@ public bool CanDodge()
|
||||
//공격이 가능한 상태인가?
|
||||
public bool CanAttack()
|
||||
{
|
||||
//이미 공격 중이거나 차징 중이면 공격 불가 (연속기가 있다면 바뀔수 있음)
|
||||
if (CurrentState == PlayerState.Attack || CurrentState == PlayerState.Charge)
|
||||
//이미 공격 중이거나 차징/캐스팅/채널링 중이면 공격 불가
|
||||
if (CurrentState == PlayerState.Attack || CurrentState == PlayerState.Charge
|
||||
|| CurrentState == PlayerState.Cast || CurrentState == PlayerState.Channel)
|
||||
return false;
|
||||
|
||||
//피격(Hit) 중이거나 죽었다면(Dead) 공격 불가
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class PlayerHealth : Health
|
||||
public class PlayerHealth : Health, IDamageable
|
||||
{
|
||||
[SerializeField] PlayerStat _pstat;
|
||||
|
||||
@@ -33,6 +33,13 @@ private void Update()
|
||||
|
||||
public void TakeDamage(int damage)
|
||||
{
|
||||
ChangeHP(currentHp - Mathf.Clamp(damage,0,currentHp));
|
||||
ChangeHP(currentHp - Mathf.Clamp(damage, 0, currentHp));
|
||||
}
|
||||
|
||||
public void TakeDamage(int damage, Transform source)
|
||||
{
|
||||
TakeDamage(damage);
|
||||
}
|
||||
|
||||
public Transform GetTransform() => transform;
|
||||
}
|
||||
|
||||
30
Assets/02_Scripts/Skill/Data/DebuffData.cs
Normal file
30
Assets/02_Scripts/Skill/Data/DebuffData.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using UnityEngine;
|
||||
|
||||
public enum DebuffType
|
||||
{
|
||||
DamageOverTime,
|
||||
Slow,
|
||||
StatReduction,
|
||||
Stun
|
||||
}
|
||||
|
||||
[CreateAssetMenu(menuName = "Skill/DebuffData")]
|
||||
public class DebuffData : ScriptableObject
|
||||
{
|
||||
[Header("기본 정보")]
|
||||
public string DebuffName;
|
||||
public Sprite Icon;
|
||||
|
||||
[Header("디버프 설정")]
|
||||
public DebuffType DebuffType;
|
||||
public float Duration;
|
||||
public float TickInterval;
|
||||
public float Value;
|
||||
|
||||
[Header("스택")]
|
||||
public bool Stackable;
|
||||
public int MaxStacks;
|
||||
|
||||
[Header("이펙트")]
|
||||
public GameObject EffectPrefab;
|
||||
}
|
||||
2
Assets/02_Scripts/Skill/Data/DebuffData.cs.meta
Normal file
2
Assets/02_Scripts/Skill/Data/DebuffData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 71747801caf54d74cafaf0cabbc6bed7
|
||||
@@ -2,11 +2,39 @@
|
||||
|
||||
/*
|
||||
사용법:
|
||||
Project에서 Create → Skill/SkillData로 스킬 에셋 생성, 레벨별 수치 입력
|
||||
Create → Skill/WeaponSkillSet로 무기별 스킬 묶음 생성
|
||||
플레이어에 SkillManager + Effect 컴포넌트들 부착
|
||||
무기 변경 시 LoadWeaponSkills(skillSet) 호출
|
||||
입력 시 SkillInput(slotIndex, inputState) 호출
|
||||
1. Project에서 Create → Skill/SkillData로 스킬 에셋 생성, 레벨별 수치 입력
|
||||
2. Create → Skill/WeaponSkillSet로 무기별 스킬 묶음 생성
|
||||
3. Create → Skill/DebuffData로 디버프 에셋 생성 (필요시)
|
||||
4. 플레이어에 SkillModule + 필요한 Effect 컴포넌트들 부착
|
||||
(DamageEffect, AreaEffect, ZoneEffect, BuffEffect, ProjectileEffect)
|
||||
5. 디버프 받는 대상에 StatusEffectReceiver + IDamageable 구현 부착
|
||||
6. 무기 변경 시 LoadWeaponSkills(skillSet) 호출
|
||||
7. 입력 시 SkillInput(slotIndex, inputState) 호출
|
||||
8. Remote 스킬 범위 확인 시 AreaConfirmInput(inputState) 호출
|
||||
|
||||
ActivationType (발동 방식):
|
||||
Instant — 즉시 발동
|
||||
Charge — 누르고 있으면 차징, 놓으면 발동
|
||||
Cast — 시전 시간 후 자동 발동 (피격/회피 시 취소)
|
||||
Channel — 시전 동안 지속적으로 효과 반복 (피격/회피 시 취소)
|
||||
|
||||
CastMethod (발동 위치):
|
||||
Self — 자기 중심 발동 (키 입력 즉시 ActivationType 흐름 시작)
|
||||
Remote — 원격 발동 (마우스로 위치 선택 → 확인 후 ActivationType 흐름 시작)
|
||||
|
||||
TargetType (효과 방식):
|
||||
Single → DamageEffect
|
||||
Area → AreaEffect
|
||||
Self → BuffEffect
|
||||
Projectile → ProjectileEffect
|
||||
Zone → ZoneEffect (설치형)
|
||||
|
||||
조합 예시:
|
||||
Instant + Self → 즉시 자기 주변 발동
|
||||
Instant + Remote → 위치 선택 후 즉시 발동
|
||||
Cast + Remote → 위치 선택 → 캐스팅 → 완료 시 해당 위치에 발동
|
||||
Channel + Remote → 위치 선택 → 해당 위치에서 채널링
|
||||
Charge + Remote → 위치 선택 → 차징 → 릴리스 시 해당 위치에 발동
|
||||
*/
|
||||
|
||||
[CreateAssetMenu(menuName = "Skill/SkillData")]
|
||||
@@ -20,6 +48,7 @@ public class SkillData : ScriptableObject
|
||||
[Header("스킬 분류")]
|
||||
public SkillType SkillType;
|
||||
public ActivationType ActivationType;
|
||||
public CastMethod CastMethod;
|
||||
public TargetType TargetType;
|
||||
|
||||
[Header("애니메이션")]
|
||||
@@ -28,9 +57,15 @@ public class SkillData : ScriptableObject
|
||||
[Header("이펙트")]
|
||||
public GameObject EffectPrefab;
|
||||
|
||||
[Header("범위 지정 (AreaSelect용)")]
|
||||
[Header("범위 지정 (Remote용)")]
|
||||
public GameObject AreaIndicatorPrefab;
|
||||
|
||||
[Header("설치형 (Zone)")]
|
||||
public GameObject ZonePrefab;
|
||||
|
||||
[Header("디버프")]
|
||||
public DebuffData[] AppliedDebuffs;
|
||||
|
||||
[Header("레벨별 수치")]
|
||||
public SkillLevelData[] Levels;
|
||||
|
||||
@@ -50,8 +85,13 @@ public class SkillLevelData
|
||||
public float ManaCost;
|
||||
public float Duration;
|
||||
public float ChargeTimeMax;
|
||||
public float CastTime;
|
||||
public float ChannelDuration;
|
||||
public float TickInterval;
|
||||
public float TickDamage;
|
||||
}
|
||||
|
||||
public enum SkillType { Active, Passive }
|
||||
public enum ActivationType { Instant, Charge, AreaSelect }
|
||||
public enum TargetType { Self, Single, Area, Projectile }
|
||||
public enum ActivationType { Instant, Charge, Cast, Channel }
|
||||
public enum CastMethod { Self, Remote }
|
||||
public enum TargetType { Self, Single, Area, Projectile, Zone }
|
||||
|
||||
@@ -4,10 +4,19 @@ public class AreaEffect : MonoBehaviour, ISkillEffect
|
||||
{
|
||||
public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
|
||||
{
|
||||
SkillLevelData levelData = skill.CurrentLevelData;
|
||||
float finalDamage = levelData.Damage * chargeRatio;
|
||||
Vector3 center = caster.position + caster.forward * skill.CurrentLevelData.Range;
|
||||
ApplyArea(skill, caster, center, chargeRatio);
|
||||
}
|
||||
|
||||
Vector3 center = caster.position + caster.forward * levelData.Range;
|
||||
public void ExecuteAtPosition(SkillInstance skill, Transform caster, Vector3 targetPos, float chargeRatio)
|
||||
{
|
||||
ApplyArea(skill, caster, targetPos, chargeRatio);
|
||||
}
|
||||
|
||||
private void ApplyArea(SkillInstance skill, Transform caster, Vector3 center, float chargeRatio)
|
||||
{
|
||||
SkillLevelData levelData = skill.CurrentLevelData;
|
||||
int damage = Mathf.RoundToInt(levelData.Damage * chargeRatio);
|
||||
|
||||
if (skill.Data.EffectPrefab != null)
|
||||
{
|
||||
@@ -19,25 +28,21 @@ public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
|
||||
{
|
||||
if (hit.transform == caster) continue;
|
||||
|
||||
Debug.Log($"[범위] {hit.name}에게 {finalDamage} 데미지");
|
||||
}
|
||||
}
|
||||
IDamageable target = hit.GetComponent<IDamageable>();
|
||||
if (target != null)
|
||||
{
|
||||
target.TakeDamage(damage, caster);
|
||||
|
||||
public void ExecuteAtPosition(SkillInstance skill, Transform caster, Vector3 targetPos)
|
||||
{
|
||||
SkillLevelData levelData = skill.CurrentLevelData;
|
||||
|
||||
if (skill.Data.EffectPrefab != null)
|
||||
{
|
||||
Instantiate(skill.Data.EffectPrefab, targetPos, Quaternion.identity);
|
||||
}
|
||||
|
||||
Collider[] hits = Physics.OverlapSphere(targetPos, levelData.Range);
|
||||
foreach (Collider hit in hits)
|
||||
{
|
||||
if (hit.transform == caster) continue;
|
||||
|
||||
Debug.Log($"[범위지정] {hit.name}에게 {levelData.Damage} 데미지");
|
||||
if (skill.Data.AppliedDebuffs != null)
|
||||
{
|
||||
StatusEffectReceiver receiver = hit.GetComponent<StatusEffectReceiver>();
|
||||
if (receiver != null)
|
||||
{
|
||||
foreach (var debuff in skill.Data.AppliedDebuffs)
|
||||
receiver.ApplyDebuff(debuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,10 @@ public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
|
||||
|
||||
Debug.Log($"버프 적용: {skill.Data.SkillName}, 지속시간 {levelData.Duration}초");
|
||||
}
|
||||
|
||||
public void ExecuteAtPosition(SkillInstance skill, Transform caster, Vector3 targetPos, float chargeRatio)
|
||||
{
|
||||
// 버프는 항상 시전자 대상이므로 Execute와 동일
|
||||
Execute(skill, caster, chargeRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,40 @@ public class DamageEffect : MonoBehaviour, ISkillEffect
|
||||
{
|
||||
public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
|
||||
{
|
||||
SkillLevelData levelData = skill.CurrentLevelData;
|
||||
float finalDamage = levelData.Damage * chargeRatio;
|
||||
float range = levelData.Range;
|
||||
Vector3 center = caster.position + caster.forward * skill.CurrentLevelData.Range * 0.5f;
|
||||
ApplyDamage(skill, caster, center, chargeRatio);
|
||||
}
|
||||
|
||||
Collider[] hits = Physics.OverlapSphere(caster.position + caster.forward * range * 0.5f, range);
|
||||
public void ExecuteAtPosition(SkillInstance skill, Transform caster, Vector3 targetPos, float chargeRatio)
|
||||
{
|
||||
ApplyDamage(skill, caster, targetPos, chargeRatio);
|
||||
}
|
||||
|
||||
private void ApplyDamage(SkillInstance skill, Transform caster, Vector3 center, float chargeRatio)
|
||||
{
|
||||
SkillLevelData levelData = skill.CurrentLevelData;
|
||||
int damage = Mathf.RoundToInt(levelData.Damage * chargeRatio);
|
||||
|
||||
Collider[] hits = Physics.OverlapSphere(center, levelData.Range);
|
||||
foreach (Collider hit in hits)
|
||||
{
|
||||
if (hit.transform == caster) continue;
|
||||
|
||||
// IDamageable 등 인터페이스가 있으면 여기서 적용
|
||||
Debug.Log($"{hit.name}에게 {finalDamage} 데미지");
|
||||
IDamageable target = hit.GetComponent<IDamageable>();
|
||||
if (target != null)
|
||||
{
|
||||
target.TakeDamage(damage, caster);
|
||||
|
||||
if (skill.Data.AppliedDebuffs != null)
|
||||
{
|
||||
StatusEffectReceiver receiver = hit.GetComponent<StatusEffectReceiver>();
|
||||
if (receiver != null)
|
||||
{
|
||||
foreach (var debuff in skill.Data.AppliedDebuffs)
|
||||
receiver.ApplyDebuff(debuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
public interface ISkillEffect
|
||||
{
|
||||
void Execute(SkillInstance skill, Transform caster, float chargeRatio);
|
||||
void ExecuteAtPosition(SkillInstance skill, Transform caster, Vector3 targetPos, float chargeRatio);
|
||||
}
|
||||
|
||||
@@ -12,4 +12,17 @@ public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
|
||||
// 투사체에 데미지 정보 전달
|
||||
// proj.GetComponent<Projectile>()?.Init(skill.CurrentLevelData.Damage * chargeRatio);
|
||||
}
|
||||
|
||||
public void ExecuteAtPosition(SkillInstance skill, Transform caster, Vector3 targetPos, float chargeRatio)
|
||||
{
|
||||
if (skill.Data.EffectPrefab == null) return;
|
||||
|
||||
Vector3 spawnPos = caster.position + Vector3.up;
|
||||
Vector3 direction = (targetPos - spawnPos).normalized;
|
||||
Quaternion rotation = Quaternion.LookRotation(direction);
|
||||
GameObject proj = Instantiate(skill.Data.EffectPrefab, spawnPos, rotation);
|
||||
|
||||
// 투사체에 데미지 정보 전달
|
||||
// proj.GetComponent<Projectile>()?.Init(skill.CurrentLevelData.Damage * chargeRatio);
|
||||
}
|
||||
}
|
||||
|
||||
29
Assets/02_Scripts/Skill/Effects/ZoneEffect.cs
Normal file
29
Assets/02_Scripts/Skill/Effects/ZoneEffect.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ZoneEffect : MonoBehaviour, ISkillEffect
|
||||
{
|
||||
public void Execute(SkillInstance skill, Transform caster, float chargeRatio)
|
||||
{
|
||||
Vector3 center = caster.position + caster.forward * skill.CurrentLevelData.Range;
|
||||
SpawnZone(skill, center);
|
||||
}
|
||||
|
||||
public void ExecuteAtPosition(SkillInstance skill, Transform caster, Vector3 targetPos, float chargeRatio)
|
||||
{
|
||||
SpawnZone(skill, targetPos);
|
||||
}
|
||||
|
||||
private void SpawnZone(SkillInstance skill, Vector3 position)
|
||||
{
|
||||
if (skill.Data.ZonePrefab == null) return;
|
||||
|
||||
GameObject zoneObj = Instantiate(skill.Data.ZonePrefab, position, Quaternion.identity);
|
||||
ZoneEntity entity = zoneObj.GetComponent<ZoneEntity>();
|
||||
if (entity != null)
|
||||
{
|
||||
SkillLevelData data = skill.CurrentLevelData;
|
||||
float tickDmg = data.TickDamage > 0 ? data.TickDamage : data.Damage;
|
||||
entity.Init(tickDmg, data.Range, data.Duration, data.TickInterval, skill.Data.AppliedDebuffs);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Skill/Effects/ZoneEffect.cs.meta
Normal file
2
Assets/02_Scripts/Skill/Effects/ZoneEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 681ba972155e69d4e903ca9f06ade300
|
||||
61
Assets/02_Scripts/Skill/Effects/ZoneEntity.cs
Normal file
61
Assets/02_Scripts/Skill/Effects/ZoneEntity.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ZoneEntity : MonoBehaviour
|
||||
{
|
||||
private float _damage;
|
||||
private float _radius;
|
||||
private float _duration;
|
||||
private float _tickInterval;
|
||||
private DebuffData[] _debuffs;
|
||||
|
||||
private float _lifeTimer;
|
||||
private float _tickAccumulator;
|
||||
|
||||
public void Init(float damage, float radius, float duration, float tickInterval, DebuffData[] debuffs)
|
||||
{
|
||||
_damage = damage;
|
||||
_radius = radius;
|
||||
_duration = duration;
|
||||
_tickInterval = tickInterval > 0 ? tickInterval : 1f;
|
||||
_debuffs = debuffs;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_lifeTimer += Time.deltaTime;
|
||||
_tickAccumulator += Time.deltaTime;
|
||||
|
||||
if (_tickAccumulator >= _tickInterval)
|
||||
{
|
||||
_tickAccumulator -= _tickInterval;
|
||||
ApplyTickDamage();
|
||||
}
|
||||
|
||||
if (_lifeTimer >= _duration)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTickDamage()
|
||||
{
|
||||
Collider[] hits = Physics.OverlapSphere(transform.position, _radius);
|
||||
foreach (Collider col in hits)
|
||||
{
|
||||
IDamageable target = col.GetComponent<IDamageable>();
|
||||
if (target == null) continue;
|
||||
|
||||
target.TakeDamage(Mathf.RoundToInt(_damage), transform);
|
||||
|
||||
if (_debuffs != null)
|
||||
{
|
||||
StatusEffectReceiver receiver = col.GetComponent<StatusEffectReceiver>();
|
||||
if (receiver != null)
|
||||
{
|
||||
foreach (var debuff in _debuffs)
|
||||
receiver.ApplyDebuff(debuff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Skill/Effects/ZoneEntity.cs.meta
Normal file
2
Assets/02_Scripts/Skill/Effects/ZoneEntity.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c474ebcf1164e1348b54f06b02241e08
|
||||
@@ -16,11 +16,26 @@ public class SkillModule : MonoBehaviour
|
||||
private int _chargingSlot = -1;
|
||||
private float _chargeTimer = 0f;
|
||||
|
||||
// 범위 지정
|
||||
// 캐스팅
|
||||
private int _castingSlot = -1;
|
||||
private float _castTimer = 0f;
|
||||
|
||||
// 채널링
|
||||
private int _channelingSlot = -1;
|
||||
private float _channelTimer = 0f;
|
||||
private float _channelTickAccumulator = 0f;
|
||||
|
||||
// 범위 지정 (Remote)
|
||||
private int _areaSelectSlot = -1;
|
||||
private GameObject _areaIndicator;
|
||||
|
||||
// Remote 타겟 위치
|
||||
private Vector3 _remoteTargetPos;
|
||||
private bool _hasRemoteTarget;
|
||||
|
||||
public bool IsAreaSelecting => _areaSelectSlot >= 0;
|
||||
public bool IsCasting => _castingSlot >= 0;
|
||||
public bool IsChanneling => _channelingSlot >= 0;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -36,6 +51,8 @@ private void Update()
|
||||
{
|
||||
TickCooldowns();
|
||||
TickCharge();
|
||||
TickCast();
|
||||
TickChannel();
|
||||
TickAreaSelect();
|
||||
}
|
||||
|
||||
@@ -67,6 +84,7 @@ private ISkillEffect ResolveEffect(TargetType targetType)
|
||||
TargetType.Single => GetComponent<DamageEffect>(),
|
||||
TargetType.Area => GetComponent<AreaEffect>(),
|
||||
TargetType.Projectile => GetComponent<ProjectileEffect>(),
|
||||
TargetType.Zone => GetComponent<ZoneEffect>(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
@@ -92,18 +110,16 @@ public void SkillInput(int slotIndex, InputState inputState)
|
||||
|
||||
if (!CanUseSkill(skill)) return;
|
||||
|
||||
if (skill.Data.ActivationType == ActivationType.Instant)
|
||||
{
|
||||
ExecuteSkill(slotIndex);
|
||||
}
|
||||
else if (skill.Data.ActivationType == ActivationType.Charge)
|
||||
{
|
||||
StartCharge(slotIndex);
|
||||
}
|
||||
else if (skill.Data.ActivationType == ActivationType.AreaSelect)
|
||||
// Remote 스킬: 먼저 범위 지정 모드 진입
|
||||
if (skill.Data.CastMethod == CastMethod.Remote)
|
||||
{
|
||||
StartAreaSelect(slotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Self 스킬: 바로 ActivationType 흐름
|
||||
BeginActivation(slotIndex);
|
||||
}
|
||||
}
|
||||
else if (inputState == InputState.Canceled)
|
||||
{
|
||||
@@ -111,6 +127,10 @@ public void SkillInput(int slotIndex, InputState inputState)
|
||||
{
|
||||
ReleaseCharge();
|
||||
}
|
||||
else if (_channelingSlot == slotIndex)
|
||||
{
|
||||
CancelChannel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +148,32 @@ public void AreaConfirmInput(InputState inputState)
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 발동 분기
|
||||
/// <summary>
|
||||
/// ActivationType에 따라 적절한 흐름을 시작
|
||||
/// </summary>
|
||||
private void BeginActivation(int slotIndex)
|
||||
{
|
||||
SkillInstance skill = _equippedSkills[slotIndex];
|
||||
|
||||
switch (skill.Data.ActivationType)
|
||||
{
|
||||
case ActivationType.Instant:
|
||||
ExecuteSkill(slotIndex);
|
||||
break;
|
||||
case ActivationType.Charge:
|
||||
StartCharge(slotIndex);
|
||||
break;
|
||||
case ActivationType.Cast:
|
||||
StartCast(slotIndex);
|
||||
break;
|
||||
case ActivationType.Channel:
|
||||
StartChannel(slotIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 실행
|
||||
private bool CanUseSkill(SkillInstance skill)
|
||||
{
|
||||
@@ -136,7 +182,8 @@ private bool CanUseSkill(SkillInstance skill)
|
||||
PlayerState state = _stateMachine.CurrentState;
|
||||
if (state == PlayerState.Dead || state == PlayerState.Hit
|
||||
|| state == PlayerState.Dodge || state == PlayerState.Trans
|
||||
|| state == PlayerState.Action)
|
||||
|| state == PlayerState.Action || state == PlayerState.Cast
|
||||
|| state == PlayerState.Channel)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@@ -159,12 +206,26 @@ private void ExecuteSkill(int slotIndex)
|
||||
chargeRatio = maxCharge > 0 ? Mathf.Clamp01(_chargeTimer / maxCharge) : 1f;
|
||||
}
|
||||
|
||||
effect?.Execute(skill, _transform, chargeRatio);
|
||||
// CastMethod에 따라 실행 방식 분기
|
||||
if (_hasRemoteTarget)
|
||||
{
|
||||
effect?.ExecuteAtPosition(skill, _transform, _remoteTargetPos, chargeRatio);
|
||||
ClearRemoteTarget();
|
||||
}
|
||||
else
|
||||
{
|
||||
effect?.Execute(skill, _transform, chargeRatio);
|
||||
}
|
||||
|
||||
skill.StartCooldown();
|
||||
_chargeTimer = 0f;
|
||||
_chargingSlot = -1;
|
||||
}
|
||||
|
||||
private void ClearRemoteTarget()
|
||||
{
|
||||
_hasRemoteTarget = false;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 차지
|
||||
@@ -200,7 +261,150 @@ private void TickCharge()
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 범위 지정
|
||||
#region 캐스팅
|
||||
private void StartCast(int slotIndex)
|
||||
{
|
||||
_castingSlot = slotIndex;
|
||||
_castTimer = 0f;
|
||||
|
||||
_stateMachine.ChangeState(PlayerState.Cast);
|
||||
|
||||
SkillInstance skill = _equippedSkills[slotIndex];
|
||||
if (!string.IsNullOrEmpty(skill.Data.AnimTrigger))
|
||||
_anim.SetTrigger(skill.Data.AnimTrigger);
|
||||
|
||||
_stateMachine.OnStateChanged += OnCastInterrupted;
|
||||
}
|
||||
|
||||
private void TickCast()
|
||||
{
|
||||
if (_castingSlot < 0) return;
|
||||
|
||||
_castTimer += Time.deltaTime;
|
||||
|
||||
SkillInstance skill = _equippedSkills[_castingSlot];
|
||||
if (_castTimer >= skill.CurrentLevelData.CastTime)
|
||||
{
|
||||
CompleteCast();
|
||||
}
|
||||
}
|
||||
|
||||
private void CompleteCast()
|
||||
{
|
||||
if (_castingSlot < 0) return;
|
||||
|
||||
_stateMachine.OnStateChanged -= OnCastInterrupted;
|
||||
ExecuteSkill(_castingSlot);
|
||||
_castingSlot = -1;
|
||||
_castTimer = 0f;
|
||||
}
|
||||
|
||||
private void CancelCast()
|
||||
{
|
||||
_stateMachine.OnStateChanged -= OnCastInterrupted;
|
||||
_castingSlot = -1;
|
||||
_castTimer = 0f;
|
||||
ClearRemoteTarget();
|
||||
}
|
||||
|
||||
private void OnCastInterrupted(PlayerState newState)
|
||||
{
|
||||
if (newState == PlayerState.Hit || newState == PlayerState.Dead
|
||||
|| newState == PlayerState.Dodge)
|
||||
{
|
||||
CancelCast();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 채널링
|
||||
private void StartChannel(int slotIndex)
|
||||
{
|
||||
_channelingSlot = slotIndex;
|
||||
_channelTimer = 0f;
|
||||
_channelTickAccumulator = 0f;
|
||||
|
||||
_stateMachine.ChangeState(PlayerState.Channel);
|
||||
|
||||
SkillInstance skill = _equippedSkills[slotIndex];
|
||||
if (!string.IsNullOrEmpty(skill.Data.AnimTrigger))
|
||||
_anim.SetTrigger(skill.Data.AnimTrigger);
|
||||
|
||||
// 첫 틱 즉시 실행
|
||||
ISkillEffect effect = _skillEffects[slotIndex];
|
||||
ExecuteChannelTick(skill, effect);
|
||||
|
||||
// 쿨다운은 채널 시작 시점에 시작
|
||||
skill.StartCooldown();
|
||||
|
||||
_stateMachine.OnStateChanged += OnChannelInterrupted;
|
||||
}
|
||||
|
||||
private void TickChannel()
|
||||
{
|
||||
if (_channelingSlot < 0) return;
|
||||
|
||||
SkillInstance skill = _equippedSkills[_channelingSlot];
|
||||
SkillLevelData data = skill.CurrentLevelData;
|
||||
|
||||
_channelTimer += Time.deltaTime;
|
||||
_channelTickAccumulator += Time.deltaTime;
|
||||
|
||||
if (_channelTickAccumulator >= data.TickInterval)
|
||||
{
|
||||
_channelTickAccumulator -= data.TickInterval;
|
||||
ISkillEffect effect = _skillEffects[_channelingSlot];
|
||||
ExecuteChannelTick(skill, effect);
|
||||
}
|
||||
|
||||
if (_channelTimer >= data.ChannelDuration)
|
||||
{
|
||||
EndChannel();
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteChannelTick(SkillInstance skill, ISkillEffect effect)
|
||||
{
|
||||
if (_hasRemoteTarget)
|
||||
{
|
||||
effect?.ExecuteAtPosition(skill, _transform, _remoteTargetPos, 1f);
|
||||
}
|
||||
else
|
||||
{
|
||||
effect?.Execute(skill, _transform, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void EndChannel()
|
||||
{
|
||||
_stateMachine.OnStateChanged -= OnChannelInterrupted;
|
||||
_stateMachine.ChangeState(PlayerState.Idle);
|
||||
_channelingSlot = -1;
|
||||
_channelTimer = 0f;
|
||||
_channelTickAccumulator = 0f;
|
||||
ClearRemoteTarget();
|
||||
}
|
||||
|
||||
private void CancelChannel()
|
||||
{
|
||||
_stateMachine.OnStateChanged -= OnChannelInterrupted;
|
||||
_channelingSlot = -1;
|
||||
_channelTimer = 0f;
|
||||
_channelTickAccumulator = 0f;
|
||||
ClearRemoteTarget();
|
||||
}
|
||||
|
||||
private void OnChannelInterrupted(PlayerState newState)
|
||||
{
|
||||
if (newState == PlayerState.Hit || newState == PlayerState.Dead
|
||||
|| newState == PlayerState.Dodge)
|
||||
{
|
||||
CancelChannel();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 범위 지정 (Remote)
|
||||
private void StartAreaSelect(int slotIndex)
|
||||
{
|
||||
_areaSelectSlot = slotIndex;
|
||||
@@ -229,26 +433,19 @@ private void ConfirmAreaSelect()
|
||||
{
|
||||
if (_areaSelectSlot < 0) return;
|
||||
|
||||
SkillInstance skill = _equippedSkills[_areaSelectSlot];
|
||||
ISkillEffect effect = _skillEffects[_areaSelectSlot];
|
||||
int slotIndex = _areaSelectSlot;
|
||||
|
||||
_stateMachine.ChangeState(PlayerState.Attack);
|
||||
|
||||
if (!string.IsNullOrEmpty(skill.Data.AnimTrigger))
|
||||
_anim.SetTrigger(skill.Data.AnimTrigger);
|
||||
|
||||
Vector3 targetPos = _areaIndicator.transform.position;
|
||||
|
||||
if (effect is AreaEffect areaEffect)
|
||||
{
|
||||
areaEffect.ExecuteAtPosition(skill, _transform, targetPos);
|
||||
}
|
||||
|
||||
skill.StartCooldown();
|
||||
// 타겟 위치 저장
|
||||
_remoteTargetPos = _areaIndicator.transform.position;
|
||||
_hasRemoteTarget = true;
|
||||
|
||||
// 인디케이터 정리
|
||||
Destroy(_areaIndicator);
|
||||
_areaIndicator = null;
|
||||
_areaSelectSlot = -1;
|
||||
|
||||
// ActivationType에 따라 흐름 시작
|
||||
BeginActivation(slotIndex);
|
||||
}
|
||||
|
||||
public void CancelAreaSelect()
|
||||
|
||||
55
Assets/02_Scripts/_Shared/Status/DebuffInstance.cs
Normal file
55
Assets/02_Scripts/_Shared/Status/DebuffInstance.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class DebuffInstance
|
||||
{
|
||||
public DebuffData Data { get; private set; }
|
||||
public float RemainingTime { get; private set; }
|
||||
public bool IsExpired => RemainingTime <= 0f;
|
||||
|
||||
private StatusEffectReceiver _receiver;
|
||||
private float _tickAccumulator;
|
||||
private GameObject _visualInstance;
|
||||
|
||||
public DebuffInstance(DebuffData data, StatusEffectReceiver receiver)
|
||||
{
|
||||
Data = data;
|
||||
_receiver = receiver;
|
||||
RemainingTime = data.Duration;
|
||||
_tickAccumulator = 0f;
|
||||
}
|
||||
|
||||
public void OnApply()
|
||||
{
|
||||
if (Data.EffectPrefab != null)
|
||||
{
|
||||
_visualInstance = Object.Instantiate(Data.EffectPrefab, _receiver.transform);
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
RemainingTime -= deltaTime;
|
||||
|
||||
if (Data.DebuffType == DebuffType.DamageOverTime && Data.TickInterval > 0)
|
||||
{
|
||||
_tickAccumulator += deltaTime;
|
||||
if (_tickAccumulator >= Data.TickInterval)
|
||||
{
|
||||
_tickAccumulator -= Data.TickInterval;
|
||||
IDamageable damageable = _receiver.GetComponent<IDamageable>();
|
||||
damageable?.TakeDamage(Mathf.RoundToInt(Data.Value), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRemove()
|
||||
{
|
||||
if (_visualInstance != null)
|
||||
Object.Destroy(_visualInstance);
|
||||
}
|
||||
|
||||
public void RefreshDuration()
|
||||
{
|
||||
RemainingTime = Data.Duration;
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/_Shared/Status/DebuffInstance.cs.meta
Normal file
2
Assets/02_Scripts/_Shared/Status/DebuffInstance.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 17e97bf635cef364781fdf5cedc5dfee
|
||||
7
Assets/02_Scripts/_Shared/Status/IDamageable.cs
Normal file
7
Assets/02_Scripts/_Shared/Status/IDamageable.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
public interface IDamageable
|
||||
{
|
||||
void TakeDamage(int damage, Transform source);
|
||||
Transform GetTransform();
|
||||
}
|
||||
2
Assets/02_Scripts/_Shared/Status/IDamageable.cs.meta
Normal file
2
Assets/02_Scripts/_Shared/Status/IDamageable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd29cb0b94cdd2c4388ff287969a9b5e
|
||||
69
Assets/02_Scripts/_Shared/Status/StatusEffectReceiver.cs
Normal file
69
Assets/02_Scripts/_Shared/Status/StatusEffectReceiver.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class StatusEffectReceiver : MonoBehaviour
|
||||
{
|
||||
private List<DebuffInstance> _activeDebuffs = new List<DebuffInstance>();
|
||||
|
||||
public void ApplyDebuff(DebuffData data)
|
||||
{
|
||||
if (data == null) return;
|
||||
|
||||
if (!data.Stackable)
|
||||
{
|
||||
DebuffInstance existing = _activeDebuffs.Find(d => d.Data == data);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.RefreshDuration();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var d in _activeDebuffs)
|
||||
if (d.Data == data) count++;
|
||||
|
||||
if (count >= data.MaxStacks) return;
|
||||
}
|
||||
|
||||
DebuffInstance instance = new DebuffInstance(data, this);
|
||||
_activeDebuffs.Add(instance);
|
||||
instance.OnApply();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
for (int i = _activeDebuffs.Count - 1; i >= 0; i--)
|
||||
{
|
||||
_activeDebuffs[i].Tick(Time.deltaTime);
|
||||
if (_activeDebuffs[i].IsExpired)
|
||||
{
|
||||
_activeDebuffs[i].OnRemove();
|
||||
_activeDebuffs.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasDebuff(DebuffType type)
|
||||
{
|
||||
foreach (var d in _activeDebuffs)
|
||||
if (d.Data.DebuffType == type) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public float GetDebuffValue(DebuffType type)
|
||||
{
|
||||
float total = 0f;
|
||||
foreach (var d in _activeDebuffs)
|
||||
if (d.Data.DebuffType == type) total += d.Data.Value;
|
||||
return total;
|
||||
}
|
||||
|
||||
public void ClearAllDebuffs()
|
||||
{
|
||||
for (int i = _activeDebuffs.Count - 1; i >= 0; i--)
|
||||
_activeDebuffs[i].OnRemove();
|
||||
_activeDebuffs.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dafab9c2111f5d640bf9eb470963b85d
|
||||
Reference in New Issue
Block a user