using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.InputSystem; using VOID.Generic.Dict.Ability; using VOID.Generic.Objects.Unit; using VOID.Generic.Objects.Unit.Actions; using VOID.Generic.Objects.Unit.Events; using VOID.Generic.Player.Input; using VOID.Generic.Player.Util; using VOID.UserInterface; using VOID.UserInterface.Game.Party; using VOID.Generic.Util; using VOID.ScriptableObjects.Abilities; using PlayerInputManager = VOID.Generic.Player.Input.PlayerInputManager; namespace VOID.Generic.Player { /// /// When starts then player can select /// targets by selected in . After successfull selection /// and usage accepting then active unit starts . ///
/// For each target is created. ///
[DefaultExecutionOrder(-1)] public class PlayerAbilityCasting : MonoBehaviour { [SerializeField] private Material _trajectoryMaterial; [SerializeField] private Material _areaOfEffectMaterial; private UnitObject _caster; private BasicAbility _ability; private readonly List _targets = new(); private bool _isTargetInRange = true; private bool _isValidTarget = true; private void Start() { PlayerController.current.OnActiveUnitChange += OnActiveUnitChange; CastingInputManager.current.confirm.performed += OnConfirm; CastingInputManager.current.cancel.performed += OnCancel; enabled = false; } private void FixedUpdate() { switch (_ability.aimingType) { case AbilityAimingType.Blank: // Blank have zero targets - just do nothing break; case AbilityAimingType.Self: // Always one target - self _targets.First().transform.position = _caster.transform.position; break; case AbilityAimingType.Target: // Default target aiming - player needs to select targets MoveCurrentTargetToCursor(); RotateUnitTowardsFirstTarget(); break; default: throw new ArgumentOutOfRangeException(); } } /// /// Stick current target to position which cursor pointing. /// private void MoveCurrentTargetToCursor() { // Zero targets should not be possible here thanks to validations in BasicAbility if (_targets.Count == 0) { Debug.LogError($"{AbilityAimingType.Target} with exactly ZERO targets defined in {_ability} is invalid!"); return; } // Get position which mouse pointing, if nothing found then player poiting UI or literaly nothing? var select = SelectUtil.Select(); var targetPosition = select.position; var targetObject = select.selectObject; if (!select.hit) return; if (select.onUI && select.unitPortraitUI == null) return; // Directional target correction - forces aim at maximum distance in given direction ignoring height // By default this correction can't aim at objects - it is purely positional if (_ability.aimingCorrectionType is AbilityAimingCorrectionType.Directional) { targetPosition = TargetPositionCorrection_Directional(targetPosition); targetObject = null; } // Target must be allowed by ability _isValidTarget = _ability.projectileSteps.First().validTargets.All(validTarget => validTarget.IsValid(_caster, targetPosition, targetObject)); // Target must be in range! _isTargetInRange = RangeUtil.IsInRange(_caster, targetPosition, _ability.range); // Player can have multiple target, but now we manage only latest one's position var currentTarget = _targets.Last(); currentTarget.SetActive(_isValidTarget && _isTargetInRange); // Current target to cursor's pointing position currentTarget.transform.position = targetPosition; } private Vector3 TargetPositionCorrection_Directional(Vector3 toPosition) { var casterPosition = _caster.transform.position; toPosition = Vector3.Scale(toPosition, new Vector3(1, 0, 1)); var fromPosition = Vector3.Scale(casterPosition, new Vector3(1, 0, 1)); var direction = Vector3.Normalize(toPosition - fromPosition); return casterPosition + direction * (_ability.range + _caster.basicData.radius); } /// /// While selecting first target make look it that direction. /// private void RotateUnitTowardsFirstTarget() { if (_targets.Count == 1) _caster.OrderInstantly(new RotateAction(_caster, _targets.Last().transform.position)); } /// /// When player accepting this target's position. Also check if this position is valid! /// private void OnConfirm(InputAction.CallbackContext callbackContext) { // On UI allow only selecting unit via its portrait if (GameUI.current.isCursorOverUI && GameUI.current.PickTopComponentUI() == null) return; // Not in player's unit range OR ability dont allow that kind of target if (!_isTargetInRange || !_isValidTarget) return; if (_targets.Count == _ability.castUsages) ConfirmAbility(); else if (_targets.Count > 0) InitNextTarget(); } /// /// Whole casting completed, proceed to start ability. /// private void ConfirmAbility() { // OrderStop() also will stop currently running CastingAction which also runs this::EndCasting() // Thats why we need to prepare AbilityAction before that var abilityAction = new AbilityAction(_caster, _ability, _targets.Select(target => target.transform.position)); _caster.OrderStop(); _caster.Order(abilityAction); } /// /// When player want to cancel current target or cancel whole casting. /// private void OnCancel(InputAction.CallbackContext callbackContext) { // Cancel current target if there is any (ability can be targetless and only need confirmation) if (_targets.Count > 0) UndoTarget(); // Stops whole casting if there is no targeting active if (_targets.Count == 0) CancelAbility(); } /// /// Cancel current target so it can go back to previous target selecting. /// private void UndoTarget() { var lastIndex = _targets.Count-1; Destroy(_targets.Last()); _targets.RemoveAt(lastIndex); } /// /// Cancel whole casting process. /// private void CancelAbility() { EndCasting(); _caster.OrderStop(); } /// /// When player changes :
/// 1. Stops casting for previous unit if active
/// 2. Waiting for new unit casting action ///
private void OnActiveUnitChange(UnitObject unit) { if (_caster) { _caster.unitEvents.onStartCasting.RemoveListener(StartCasting); EndCasting(); } _caster = unit; _caster.unitEvents.onStartCasting.AddListener(StartCasting); } /// /// When player order to . /// private void StartCasting(CastingEvent castingEvent) { PlayerInputManager.current.enabled = false; CastingInputManager.current.enabled = true; _ability = castingEvent.ability; _targets.Clear(); enabled = true; InitNextTarget(); _caster.unitEvents.onEndCasting.AddListener(EndCasting); } /// /// Prepares next target to be visualized on scene. /// private void InitNextTarget() { // No more targets to be visualized - usually because ability don't have any targets to select if (_ability.castUsages <= _targets.Count) return; var visualizerGameObject = new GameObject("AbilityTarget-" + _targets.Count); var projectileVisualizer = visualizerGameObject.AddComponent(); projectileVisualizer.Init( _caster, _ability.projectileSteps, _trajectoryMaterial, _areaOfEffectMaterial, visualizerGameObject.transform ); _targets.Add(visualizerGameObject); } /// /// When player order to stop casting. /// private void EndCasting(CastingEvent castingEvent) { EndCasting(); } /// /// Stop whole target selecting and projectile visualizing. /// private void EndCasting() { _caster.unitEvents.onEndCasting.RemoveListener(EndCasting); _targets.ForEach(Destroy); _targets.Clear(); enabled = false; CastingInputManager.current.enabled = false; PlayerInputManager.current.enabled = true; } } }