This commit is contained in:
2026-07-09 21:33:33 +02:00
commit 84a2a365f3
2364 changed files with 950134 additions and 0 deletions
@@ -0,0 +1,260 @@
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
{
/// <summary>
/// When <see cref="PlayerController.activeUnit"/> starts <see cref="CastingAction"/> then player can select
/// targets by <see cref="AbilityAimingType"/> selected in <see cref="BasicAbility"/>. After successfull selection
/// and usage accepting then active unit starts <see cref="AbilityAction"/>.
/// <br/>
/// For each target <see cref="ProjectileVisualizer"/> is created.
/// </summary>
[DefaultExecutionOrder(-1)]
public class PlayerAbilityCasting : MonoBehaviour
{
[SerializeField] private Material _trajectoryMaterial;
[SerializeField] private Material _areaOfEffectMaterial;
private UnitObject _caster;
private BasicAbility _ability;
private readonly List<GameObject> _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();
}
}
/// <summary>
/// Stick current target to position which cursor pointing.
/// </summary>
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);
}
/// <summary>
/// While selecting first target make <see cref="UnitObject"/> look it that direction.
/// </summary>
private void RotateUnitTowardsFirstTarget()
{
if (_targets.Count == 1) _caster.OrderInstantly(new RotateAction(_caster, _targets.Last().transform.position));
}
/// <summary>
/// When player accepting this target's position. Also check if this position is valid!
/// </summary>
private void OnConfirm(InputAction.CallbackContext callbackContext)
{
// On UI allow only selecting unit via its portrait
if (GameUI.current.isCursorOverUI && GameUI.current.PickTopComponentUI<UnitPortraitUI>() == 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();
}
/// <summary>
/// Whole casting completed, proceed to start ability.
/// </summary>
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);
}
/// <summary>
/// When player want to cancel current target or cancel whole casting.
/// </summary>
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();
}
/// <summary>
/// Cancel current target so it can go back to previous target selecting.
/// </summary>
private void UndoTarget()
{
var lastIndex = _targets.Count-1;
Destroy(_targets.Last());
_targets.RemoveAt(lastIndex);
}
/// <summary>
/// Cancel whole casting process.
/// </summary>
private void CancelAbility()
{
EndCasting();
_caster.OrderStop();
}
/// <summary>
/// When player changes <see cref="PlayerController.activeUnit"/>:<br/>
/// 1. Stops casting for previous unit if active<br/>
/// 2. Waiting for new unit casting action
/// </summary>
private void OnActiveUnitChange(UnitObject unit)
{
if (_caster)
{
_caster.unitEvents.onStartCasting.RemoveListener(StartCasting);
EndCasting();
}
_caster = unit;
_caster.unitEvents.onStartCasting.AddListener(StartCasting);
}
/// <summary>
/// When player order <see cref="PlayerController.activeUnit"/> to <see cref="CastingAction"/>.
/// </summary>
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);
}
/// <summary>
/// Prepares next target to be visualized on scene.
/// </summary>
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>();
projectileVisualizer.Init(
_caster,
_ability.projectileSteps,
_trajectoryMaterial,
_areaOfEffectMaterial,
visualizerGameObject.transform
);
_targets.Add(visualizerGameObject);
}
/// <summary>
/// When player order <see cref="PlayerController.activeUnit"/> to stop casting.
/// </summary>
private void EndCasting(CastingEvent castingEvent)
{
EndCasting();
}
/// <summary>
/// Stop whole target selecting and projectile visualizing.
/// </summary>
private void EndCasting()
{
_caster.unitEvents.onEndCasting.RemoveListener(EndCasting);
_targets.ForEach(Destroy);
_targets.Clear();
enabled = false;
CastingInputManager.current.enabled = false;
PlayerInputManager.current.enabled = true;
}
}
}