SupaLidlGame/State/Thinker/AttackState.cs

230 lines
6.3 KiB
C#

using Godot;
using SupaLidlGame.Extensions;
using System;
namespace SupaLidlGame.State.Thinker;
public partial class AttackState : ThinkerState
{
[Export]
public float PreferredWeightDistance
{
get => _preferredWeightDistance;
protected set
{
_preferredWeightDistance = value;
_preferredWeightDistanceSq = value * value;
}
}
[Export]
public float MaxWeightDistance
{
get => _maxWeightDistance;
protected set
{
_maxWeightDistance = value;
_maxWeightDistanceSq = value * value;
}
}
[Export]
public float MaxDistanceToTarget { get; set; }
[Export]
public float UseItemDistance { get; set; } = 40;
[Export]
public ThinkerState PassiveState { get; set; }
[Export]
public ThinkerState PursueState { get; set; }
[Export]
public bool PursueOnNoLOS { get; set; } = true;
protected Characters.Character _bestTarget;
protected float _preferredWeightDistance = 64.0f;
protected float _maxWeightDistance = 8.0f;
protected float _preferredWeightDistanceSq = 4096.0f;
protected float _maxWeightDistanceSq = 64.0f;
public float[] Weights => _weights;
protected float[] _weights = new float[16];
protected int _bestWeightIdx;
protected static readonly Vector2[] _weightDirs = new Vector2[16];
static AttackState()
{
for (int i = 0; i < 16; i++)
{
float y = Mathf.Sin(Mathf.Pi * i * 2 / 16);
float x = Mathf.Cos(Mathf.Pi * i * 2 / 16);
_weightDirs[i] = new Vector2(x, y);
}
}
public override IState<ThinkerState> Enter(IState<ThinkerState> prev)
{
GD.Print("attacking");
return base.Enter(prev);
}
public void UpdateWeights(Vector2 pos)
{
// FIXME: TODO: remove all the spaghetti
Vector2 dir = NPC.Target.Normalized();
float distSq = NPC.GlobalPosition.DistanceSquaredTo(pos);
var spaceState = NPC.GetWorld2D().DirectSpaceState;
var exclude = new Godot.Collections.Array<Godot.Rid>();
exclude.Add(NPC.GetRid());
// calculate weights based on distance
for (int i = 0; i < 16; i++)
{
float directDot = _weightDirs[i].Dot(dir);
// clamp dot from [-1, 1] to [0, 1]
directDot = (directDot + 1) / 2;
float strafeDot = Math.Abs(_weightDirs[i].Dot(dir.Clockwise90()));
float currDirDot = (_weightDirs[i].Dot(NPC.Direction) + 1) / 16;
strafeDot = Mathf.Pow((strafeDot + 1) / 2, 2) + currDirDot;
// favor strafing when getting closer
if (distSq > _preferredWeightDistanceSq)
{
_weights[i] = directDot;
}
else if (distSq > _maxWeightDistanceSq)
{
float dDotWeight = Mathf.Sqrt(distSq / 4096);
float sDotWeight = 1 - dDotWeight;
_weights[i] = (dDotWeight * directDot) +
(sDotWeight * strafeDot);
}
else
{
_weights[i] = strafeDot;
}
}
// subtract weights that collide
for (int i = 0; i < 16; i++)
{
var rayParams = new PhysicsRayQueryParameters2D
{
Exclude = exclude,
CollideWithBodies = true,
From = NPC.GlobalPosition,
To = NPC.GlobalPosition + (_weightDirs[i] * 24),
CollisionMask = 1 + 2 + 16
};
var result = spaceState.IntersectRay(rayParams);
// if we hit something
if (result.Count > 0)
{
// then we subtract the value of this from the other weights
float oldWeight = _weights[i];
for (int j = 0; j < 16; j++)
{
if (i == j)
{
_weights[i] = 0;
}
else
{
float dot = _weightDirs[i].Dot(_weightDirs[j]);
if (dot >= 0)
{
_weights[j] -= _weights[j] * dot;
}
}
}
}
}
float bestWeight = 0;
for (int i = 0; i < 16; i++)
{
if (_weights[i] > bestWeight)
{
_bestWeightIdx = i;
bestWeight = _weights[i];
}
}
}
public override ThinkerState Think()
{
// TODO: the entity should wander if it doesn't find a best target
var bestTarget = NPC.FindBestTarget();
_bestTarget = bestTarget;
if (bestTarget is not null)
{
Vector2 pos = NPC.LastSeenPosition = bestTarget.GlobalPosition;
NPC.Target = pos - NPC.GlobalPosition;
Vector2 dir = NPC.Target;
float dist = NPC.GlobalPosition.DistanceTo(pos);
if (PursueState is not null)
{
// pursue the player if they can not be seen or is too far away
if (dist > MaxDistanceToTarget)
{
return PursueState;
}
if (PursueOnNoLOS && !NPC.HasLineOfSight(bestTarget))
{
return PursueState;
}
}
NPC.LastSeenPosition = bestTarget.GlobalPosition;
UpdateWeights(pos);
if (dist <= UseItemDistance && NPC.CanAttack)
{
Attack(bestTarget);
}
}
else
{
return PassiveState;
}
return base.Think();
}
public override ThinkerState Process(double delta)
{
if (!NPC.ShouldMove ||
(!NPC.ShouldMoveWhenUsingItem && NPC.Inventory.IsUsingItem))
{
NPC.Direction = Vector2.Zero;
}
else
{
NPC.Direction = _weightDirs[_bestWeightIdx];
}
return base.Process(delta);
}
public virtual void Attack(Characters.Character bestTarget)
{
if (NPC.Inventory.SelectedItem is Items.Weapon weapon)
{
NPC.UseCurrentItem();
}
}
}