Files
Genesis_Unity/Assets/02_Scripts/Skill/SkillModule.cs

608 lines
17 KiB
C#

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 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<WeaponSkillSet> _weaponSkills;
[SerializeField] private Weapon _equippedWeapon;
private Dictionary<WeaponType, WeaponSkillSet> _dicSkills = new Dictionary<WeaponType, WeaponSkillSet>();
// 슬롯 이름 → 런타임 매핑 (Dispatcher 테이블)
private Dictionary<string, IUseableRuntime> _skillBindData = new Dictionary<string, IUseableRuntime>();
// 퀵슬롯에 바인딩할 슬롯 이름 목록 (스킬용)
private static readonly string[] SkillSlotNames =
{ "UseSlot_Q", "UseSlot_E", "UseSlot_R", "UseSlot_T" };
private void Awake()
{
_stateMachine = GetComponent<PlayerStateMachine>();
_anim = GetComponent<Animator>();
_transform = transform;
_equippedSkills = new SkillInstance[_maxSlots];
_skillEffects = new ISkillEffect[_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);
RegisterSkillsToQuickslot(out_wss);
}
// 각 슬롯에 dispatcher를 한 번만 바인딩 (이후 내용물만 _skillBindData로 교체)
foreach (string slotName in SkillSlotNames)
{
string captured = slotName;
GameManager.Instance.Level.BindSlotAction(captured, (state) => DispatchSlot(captured, state));
}
}
private void DispatchSlot(string slotName, InputState state)
{
if (!_skillBindData.TryGetValue(slotName, out IUseableRuntime runtime) || runtime == null)
return;
runtime.Execute(new UseContext
{
Caster = gameObject,
Target = null,
UseInputState = state
});
}
private void RegisterSkillsToQuickslot(WeaponSkillSet wss)
{
for (int i = 0; i < SkillSlotNames.Length; i++)
{
if (i < wss.Skills.Count && wss.Skills[i] != null)
_skillBindData[SkillSlotNames[i]] = wss.Skills[i].CreateRuntime();
else
_skillBindData.Remove(SkillSlotNames[i]);
}
}
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<BuffEffect>(),
TargetType.Single => GetComponent<DamageEffect>(),
TargetType.Area => GetComponent<AreaEffect>(),
TargetType.Projectile => GetComponent<ProjectileEffect>(),
TargetType.Zone => GetComponent<ZoneEffect>(),
_ => 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();
}
}
}
/// <summary>
/// 범위 지정 모드 중 마우스 클릭 입력 (InputManager에서 호출)
/// </summary>
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
}