using UnityEngine; public class SkillModule : MonoBehaviour { [SerializeField] private int _maxSlots = 4; [SerializeField] private LayerMask _groundLayer; // 바닥 레이캐스트용 private PlayerStateMachine _stateMachine; private Animator _anim; private Transform _transform; private SkillInstance[] _equippedSkills; private ISkillEffect[] _skillEffects; // 차지 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() { _stateMachine = GetComponent(); _anim = GetComponent(); _transform = transform; _equippedSkills = new SkillInstance[_maxSlots]; _skillEffects = new ISkillEffect[_maxSlots]; } private void Update() { TickCooldowns(); TickCharge(); TickCast(); TickChannel(); TickAreaSelect(); } #region 스킬 장착 public void LoadWeaponSkills(WeaponSkillSet skillSet) { for (int i = 0; i < _maxSlots; i++) { if (i < skillSet.Skills.Count && skillSet.Skills[i] != null) { _equippedSkills[i] = new SkillInstance(skillSet.Skills[i]); _skillEffects[i] = ResolveEffect(skillSet.Skills[i].TargetType); } else { _equippedSkills[i] = null; _skillEffects[i] = null; } } ApplyPassives(); } private ISkillEffect ResolveEffect(TargetType targetType) { return targetType switch { TargetType.Self => GetComponent(), TargetType.Single => GetComponent(), TargetType.Area => GetComponent(), TargetType.Projectile => GetComponent(), TargetType.Zone => GetComponent(), _ => null }; } #endregion #region 입력 public void SkillInput(int slotIndex, InputState inputState) { if (slotIndex < 0 || slotIndex >= _maxSlots) return; SkillInstance skill = _equippedSkills[slotIndex]; if (skill == null) return; if (skill.Data.SkillType == SkillType.Passive) return; if (inputState == InputState.Started) { // 범위 지정 중에 같은 스킬 키를 다시 누르면 취소 if (_areaSelectSlot == slotIndex) { CancelAreaSelect(); return; } if (!CanUseSkill(skill)) return; // Remote 스킬: 먼저 범위 지정 모드 진입 if (skill.Data.CastMethod == CastMethod.Remote) { StartAreaSelect(slotIndex); } else { // Self 스킬: 바로 ActivationType 흐름 BeginActivation(slotIndex); } } else if (inputState == InputState.Canceled) { if (_chargingSlot == slotIndex) { ReleaseCharge(); } else if (_channelingSlot == slotIndex) { CancelChannel(); } } } /// /// 범위 지정 모드 중 마우스 클릭 입력 (InputManager에서 호출) /// public void AreaConfirmInput(InputState inputState) { if (_areaSelectSlot < 0) return; if (inputState == InputState.Started) { ConfirmAreaSelect(); } } #endregion #region 발동 분기 /// /// ActivationType에 따라 적절한 흐름을 시작 /// 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) { if (skill.IsOnCooldown) return false; PlayerState state = _stateMachine.CurrentState; if (state == PlayerState.Dead || state == PlayerState.Hit || state == PlayerState.Dodge || state == PlayerState.Trans || state == PlayerState.Action || state == PlayerState.Cast || state == PlayerState.Channel) return false; return true; } private void ExecuteSkill(int slotIndex) { SkillInstance skill = _equippedSkills[slotIndex]; ISkillEffect effect = _skillEffects[slotIndex]; _stateMachine.ChangeState(PlayerState.Attack); if (!string.IsNullOrEmpty(skill.Data.AnimTrigger)) _anim.SetTrigger(skill.Data.AnimTrigger); float chargeRatio = 1f; if (skill.Data.ActivationType == ActivationType.Charge) { float maxCharge = skill.CurrentLevelData.ChargeTimeMax; chargeRatio = maxCharge > 0 ? Mathf.Clamp01(_chargeTimer / maxCharge) : 1f; } // 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 차지 private void StartCharge(int slotIndex) { _chargingSlot = slotIndex; _chargeTimer = 0f; _stateMachine.ChangeState(PlayerState.Charge); SkillInstance skill = _equippedSkills[slotIndex]; if (!string.IsNullOrEmpty(skill.Data.AnimTrigger)) _anim.SetTrigger(skill.Data.AnimTrigger); } private void ReleaseCharge() { if (_chargingSlot < 0) return; ExecuteSkill(_chargingSlot); } private void TickCharge() { if (_chargingSlot < 0) return; SkillInstance skill = _equippedSkills[_chargingSlot]; _chargeTimer += Time.deltaTime; if (_chargeTimer >= skill.CurrentLevelData.ChargeTimeMax) { ReleaseCharge(); } } #endregion #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; SkillInstance skill = _equippedSkills[slotIndex]; if (skill.Data.AreaIndicatorPrefab != null) { _areaIndicator = Instantiate(skill.Data.AreaIndicatorPrefab); float range = skill.CurrentLevelData.Range; _areaIndicator.transform.localScale = new Vector3(range * 2f, 1f, range * 2f); } } private void TickAreaSelect() { if (_areaSelectSlot < 0 || _areaIndicator == null) return; Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit, 200f, _groundLayer)) { _areaIndicator.transform.position = hit.point; } } private void ConfirmAreaSelect() { if (_areaSelectSlot < 0) return; int slotIndex = _areaSelectSlot; // 타겟 위치 저장 _remoteTargetPos = _areaIndicator.transform.position; _hasRemoteTarget = true; // 인디케이터 정리 Destroy(_areaIndicator); _areaIndicator = null; _areaSelectSlot = -1; // ActivationType에 따라 흐름 시작 BeginActivation(slotIndex); } public void CancelAreaSelect() { if (_areaIndicator != null) Destroy(_areaIndicator); _areaIndicator = null; _areaSelectSlot = -1; } #endregion #region 쿨다운 private void TickCooldowns() { foreach (var skill in _equippedSkills) { skill?.TickCooldown(Time.deltaTime); } } #endregion #region 패시브 private void ApplyPassives() { foreach (var skill in _equippedSkills) { if (skill != null && skill.Data.SkillType == SkillType.Passive) { // PlayerStat에 스탯 보정 적용 } } } #endregion #region 외부 접근 public SkillInstance GetSkill(int slotIndex) { if (slotIndex < 0 || slotIndex >= _maxSlots) return null; return _equippedSkills[slotIndex]; } public int MaxSlots => _maxSlots; #endregion }