using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; public class SkillModule : MonoBehaviour { [SerializeField] private int _maxSlots = 4; [SerializeField] private LayerMask _groundLayer; // 바닥 레이캐스트용 // 범위 지정 시작/종료 이벤트 (외부에서 RotationMode/UI 등 반응) public event Action OnAreaSelectStarted; public event Action OnAreaSelectEnded; private PlayerStateMachine _stateMachine; private Animator _anim; private Transform _transform; private SkillInstance[] _equippedSkills; private ISkillEffect[] _skillEffects; private SkillData[] _equippedSkillDataCache; // 장착 순서 보존용 // 차지 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; [SerializeField] private List _weaponSkills; [SerializeField] private Weapon _equippedWeapon; private Dictionary _dicSkills = new Dictionary(); private void Awake() { _stateMachine = GetComponent(); _anim = GetComponent(); _transform = transform; _equippedSkills = new SkillInstance[_maxSlots]; _skillEffects = new ISkillEffect[_maxSlots]; _equippedSkillDataCache = new SkillData[_maxSlots]; } private void Start() { // 무기별 스킬셋 테이블 구성 foreach (WeaponSkillSet wss in _weaponSkills) { _dicSkills[wss.WeaponType] = wss; } // 현재 장착 무기의 스킬셋만 "장착 상태"로 로드 (퀵슬롯 자동 바인딩 없음) if (_equippedWeapon != null && _dicSkills.TryGetValue(_equippedWeapon.WType, out WeaponSkillSet out_wss) && out_wss != null) { LoadWeaponSkills(out_wss); } } // 장착된 스킬 목록 반환 (스킬창 UI에서 사용) public IReadOnlyList GetEquippedSkillDatas() => _equippedSkillDataCache; 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); _equippedSkillDataCache[i] = skillSet.Skills[i]; } else { _equippedSkills[i] = null; _skillEffects[i] = null; _equippedSkillDataCache[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 발동 분기 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; // ActionDuration 경과 후 Idle 복귀 float duration = skill.CurrentLevelData.ActionDuration; if (duration > 0f) { _ = Util.RunDelayed(duration, () => { if (this != null && _stateMachine != null && _stateMachine.CurrentState == PlayerState.Attack) _stateMachine.ChangeState(PlayerState.Idle); }, default); } else { _stateMachine.ChangeState(PlayerState.Idle); } } 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, range * 2f, range * 2f); } // 범위 지정 중에는 좌클릭이 확정 용도로 쓰이므로 기본 공격 차단 InputManager.Instance.SuppressNormalAttack = true; // 이벤트 발행 — 구독자(PlayerCharacterController 등)가 회전모드 등 처리 OnAreaSelectStarted?.Invoke(); } private void TickAreaSelect() { if (_areaSelectSlot < 0 || _areaIndicator == null) return; if (Mouse.current == null) return; Vector2 mousePos = Mouse.current.position.ReadValue(); Ray ray = Camera.main.ScreenPointToRay(mousePos); 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; // 범위 지정 종료 → 기본 공격 복구 InputManager.Instance.SuppressNormalAttack = false; OnAreaSelectEnded?.Invoke(); // ActivationType에 따라 흐름 시작 BeginActivation(slotIndex); } public void CancelAreaSelect() { if (_areaIndicator != null) Destroy(_areaIndicator); _areaIndicator = null; _areaSelectSlot = -1; // 범위 지정 종료 → 기본 공격 복구 InputManager.Instance.SuppressNormalAttack = false; OnAreaSelectEnded?.Invoke(); } #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 void SkillInputByData(SkillData data, InputState inputState) { int slotIndex = FindSlotByData(data); if (slotIndex < 0) return; SkillInput(slotIndex, inputState); } private int FindSlotByData(SkillData data) { for (int i = 0; i < _maxSlots; i++) { if (_equippedSkills[i] != null && _equippedSkills[i].Data == data) return i; } return -1; } public int MaxSlots => _maxSlots; #endregion }