Weapons/MeleeWeaponAttack.cs
using System.Reflection.Emit;
namespace Opium;
public struct WindupSetup
{
[Property] public float WindupTime { get; set; }
public override string ToString()
{
return $"Windup (time: {WindupTime})";
}
}
public struct ArcSetup
{
public ArcSetup()
{
}
public override string ToString()
{
return $"Arc (enabled: {HasArc}) (degrees: {Arc}), (radius: {ArcRadius}), (trace: {ArcTraceRadius})";
}
/// <summary>
/// Does this state have an arc?
/// </summary>
[Property] public bool HasArc { get; set; }
/// <summary>
/// The arc (in degrees) for the melee weapon strike
/// </summary>
[Property] public float Arc { get; set; } = 45f;
/// <summary>
/// The radius of the arc.
/// </summary>
[Property] public float ArcRadius { get; set; } = 10f;
[Property] public bool InvertArc { get; set; } = false;
[Property] public float MinimumTimeForHit { get; set; } = 0.25f;
/// <summary>
/// The trace radius for the arc.
/// </summary>
[Property] public float ArcTraceRadius { get; set; } = 1f;
[Property, Sandbox.Range( -1, 1 )] public float ArcUpAmount { get; set; } = 0.0f;
}
public enum AttackState
{
Inactive,
Windup,
ReadyToAttack,
Swinging,
/// <summary>
/// A heavy attack swing, which differs to a regular swing. This is called when you've wound up all the way to ReadyToAttack and are still holding the attack button down.
/// </summary>
SwingingHeavyAttack,
Hit
}
public struct StateInfo
{
/// <summary>
/// The state.
/// </summary>
[KeyProperty, Category( "General" ), Title( "State Name" )] public AttackState State { get; set; }
[Property, Category( "Animation" )] public string AnimationState { get; set; }
[Property, Category( "Animation" )] public string VariationState { get; set; }
[Property, Category( "Camera Effects" )] public string OnEnterCameraEffect { get; set; }
/// <summary>
/// How quickly can we move while in this state?
/// </summary>
[Property, Category( "General" )] public float MovementSpeed { get; set; }
[Property, Category( "General" )] public float FieldOfViewOffset { get; set; }
[Property, Category( "Stamina" )] public float OnEnterStaminaDrain { get; set; }
[Property, Category( "Stamina" )] public float StaminaDrainAmount { get; set; }
[Property, Category( "Stamina" )] public float StaminaDrainDelay { get; set; }
/// <summary>
/// If defined as non-zero, we will transition to a specified state after a set time.
/// </summary>
[Property, Category( "State Transitions" )] public float StateLength { get; set; }
[Property, Category( "State Transitions" )] public AttackState TransitionState { get; set; }
/// <summary>
/// Any attack state can have an arc.
/// </summary>
[Property, Category( "Setup" )] public ArcSetup Arc { get; set; }
/// <summary>
/// An action that is called every update while we're in this state.
/// </summary>
[Property, Category( "Actions" )] public Action<MeleeWeaponAttack> OnUpdate { get; set; }
/// <summary>
/// An action that is called when we enter this state.
/// </summary>
[Property, Category( "Actions" )] public Action<MeleeWeaponAttack, AttackState> OnEnter { get; set; }
/// <summary>
/// An action that is called when we exit this state.
/// </summary>
[Property, Category( "Actions" )] public Action<MeleeWeaponAttack, AttackState> OnExit { get; set; }
/// <summary>
/// Da sound
/// </summary>
[Property, Category( "Sounds" )] public string OnEnterVoice { get; set; }
}
public partial class MeleeWeaponAttack : Component
{
/// <summary>
/// Accessor for the weapon.
/// </summary>
public MeleeWeapon Weapon => Components.Get<MeleeWeapon>( FindMode.InParent );
/// <summary>
/// The attack resource we'll be using.
/// </summary>
[Property] public MeleeWeaponAttackResource Resource { get; set; }
// Forwarding properties for Resource.
public string AttackTypeName => Resource.AttackTypeName;
public AttackState ActivationState => Resource.ActivationState;
public List<StateInfo> StateInfo => Resource.States;
public bool IsActive => State != AttackState.Inactive;
public TimeSince TimeSinceState { get; protected set; }
public TimeUntil TimeUntilNextState { get; protected set; }
public AttackState NextState { get; protected set; }
public bool HasStateTransition { get; protected set; }
protected void OnStateChanged( AttackState before, AttackState after )
{
foreach ( var controller in Components.GetAll<MeleeStateController>() )
{
StateObjects = new();
if ( controller.IsValid() )
{
controller.OnStateChanged( before, after );
}
}
}
/// <summary>
/// Get a named camera effect from the gameobject we're on
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
protected T GetNamedCameraEffect<T>( string name ) where T : CameraEffect
{
if ( name is null || string.IsNullOrEmpty( name ) ) return null;
var foundGo = Weapon.GameObject.GetAllObjects( false ).FirstOrDefault( x => x.Name.ToLowerInvariant().Equals( name.ToLowerInvariant() ) );
if ( !foundGo.IsValid() ) return null;
var effect = foundGo.Components.Get<T>( FindMode.EverythingInSelfAndDescendants );
if ( effect is not null ) return effect;
return null;
}
private AttackState state;
[Property, ReadOnly, Category( "Data" )] public AttackState State
{
get => state;
set
{
// No re-entrant state.
if ( state == value )
{
//Log.Trace( $"Tried to re-enter state {value}" );
return;
}
var oldState = state;
state = value;
TimeSinceState = 0;
var stateInfo = GetStateInfo( value );
stateInfo.OnEnter?.Invoke( this, oldState );
OnStateChanged( oldState, state );
if ( Weapon.IsValid() && Weapon.Actor.IsValid() )
{
if ( stateInfo.OnEnterVoice is not null )
{
Weapon.Actor.PlayVoice( stateInfo.OnEnterVoice );
}
if ( Weapon.Actor is Opium.PlayerController player && player.IsValid() )
{
var cameraEffect = GetNamedCameraEffect<CameraEffect>( stateInfo.OnEnterCameraEffect );
if ( cameraEffect is not null )
{
cameraEffect.Player = player;
cameraEffect.Enabled = true;
}
if ( stateInfo.OnEnterStaminaDrain != 0f )
{
player.GetMechanic<StaminaMechanic>()?.RemoveStamina( stateInfo.OnEnterStaminaDrain, 2.0f );
}
}
}
//Log.Trace( $"State set to: {value}" );
// Define next state info
NextState = stateInfo.TransitionState;
HasStateTransition = stateInfo.StateLength > 0;
TimeUntilNextState = stateInfo.StateLength;
var oldStateInfo = GetStateInfo( oldState );
oldStateInfo.OnExit?.Invoke( this, state );
}
}
/// <summary>
/// Grab state info.
/// </summary>
/// <param name="state"></param>
/// <returns></returns>
public StateInfo GetStateInfo( AttackState state )
{
return StateInfo.FirstOrDefault( x => x.State == state );
}
public StateInfo CurrentStateInfo => GetStateInfo( State );
/// <summary>
/// An accessor to get the current state via animation.
/// </summary>
public string CurrentAnimatonState
{
get
{
var stateName = CurrentStateInfo.AnimationState;
if ( !string.IsNullOrEmpty( stateName) )
{
return stateName;
}
return "default";
}
}
/// <summary>
/// A simple enough method that handles transitioning to states if our current state has a transition state.
/// </summary>
void UpdateStateTransition()
{
if ( HasStateTransition )
{
if ( TimeUntilNextState <= 0 )
{
State = NextState;
}
}
}
/// <summary>
/// A state controller.
/// </summary>
[Property, Category( "Setup" )] public MeleeStateController StateController { get; set; }
public bool IsPerformingArc { get; set; }
bool ShouldUseHitState( SceneTraceResult tr )
{
if ( tr.GameObject.Components.Get<Actor>( FindMode.EverythingInSelfAndAncestors ) is not null ) return false;
if ( tr.GameObject.Tags.Has( "interact" ) ) return false;
if ( tr.GameObject.Tags.Has( "carry" ) ) return false;
return true;
}
private HashSet<GameObject> StateObjects { get; set; } = new();
[ConVar( "op_dev_arc" )] public static bool IsDebugging { get; set; } = false;
void UpdateArc()
{
var stateInfo = GetStateInfo( State );
var arc = stateInfo.Arc;
var delta = TimeUntilNextState.Fraction;
// Does this state even have an arc?
if ( arc.HasArc )
{
var arcOrigin = Weapon.Actor.CameraObject.Components.Get<WeaponArcOrigin>( FindMode.EverythingInSelfAndDescendants );
var cameraRotation = Weapon.Actor.CameraObject.Transform.Rotation;
var origin = Weapon.Actor.CameraObject.Transform.Position;
var arcRadius = arc.ArcRadius;
var startAngle = arc.InvertArc ? -arcRadius : arcRadius;
var endAngle = arc.InvertArc ? arcRadius : -arcRadius;
var t = startAngle.DegreeToRadian().LerpTo( endAngle.DegreeToRadian(), delta );
var x = MathF.Cos( t ) * arcRadius;
var y = MathF.Sin( t ) * arcRadius;
var z = MathF.Tan( t ) * arc.ArcUpAmount * arcRadius;
var currentSweepPosition = origin + (cameraRotation.Forward * x) + (cameraRotation.Right * y) + (cameraRotation.Up * z);
var didHit = Weapon.TestHit( currentSweepPosition, out var tr );
Gizmo.Draw.Color = delta < arc.MinimumTimeForHit ? Color.Red : Color.White;
if ( IsDebugging )
{
Gizmo.Draw.LineSphere( origin, arcRadius );
Gizmo.Draw.LineSphere( currentSweepPosition, 8 );
Gizmo.Draw.Line( origin, currentSweepPosition );
}
if ( didHit)
{
var isHitter = ShouldUseHitState( tr );
if ( isHitter && delta < arc.MinimumTimeForHit ) return;
if ( !StateObjects.Contains( tr.GameObject ) )
{
Weapon.DoMeleeHit( tr );
}
StateObjects.Add( tr.GameObject );
if ( isHitter )
{
State = AttackState.Hit;
}
else
{
var cameraEffect = GetNamedCameraEffect<CameraEffect>( "Hit Effect" );
if ( cameraEffect is not null )
{
cameraEffect.Player = Weapon.Actor as Opium.PlayerController;
cameraEffect.Enabled = true;
}
}
}
}
}
void UpdateStamina()
{
var stateInfo = GetStateInfo( State );
// No drain, no game
if ( stateInfo.StaminaDrainAmount == 0 )
{
return;
}
if ( TimeSinceState >= stateInfo.StaminaDrainDelay )
{
Weapon.Actor.GetMechanic<StaminaMechanic>()?.RemoveStamina( stateInfo.StaminaDrainAmount );
}
}
protected override void OnUpdate()
{
var stateInfo = GetStateInfo( State );
// Run our state-based action.
stateInfo.OnUpdate?.Invoke( this );
UpdateStateTransition();
UpdateArc();
UpdateStamina();
if ( StateController != null )
{
StateController.Update( this );
}
if (TimeSinceState >= stateInfo.StateLength )
{
hasAttacked = false;
}
}
public bool hasAttacked = false;
public void OnAttackReleased( bool doSound = true )
{
if ( hasAttacked ) return;
if ( Resource.OnAttackVoice is not null && doSound )
{
Weapon?.Actor?.PlayVoice( Resource.OnAttackVoice );
}
hasAttacked = true;
}
public void Activate()
{
// can't activate while already active, might be bullshit
if ( IsActive ) return;
State = ActivationState;
}
}