Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using Everglow.Commons.Utilities;
using Terraria.DataStructures;

namespace Everglow.Commons.Templates.Weapons;

/// <summary>
/// Handhold projectile.
/// Handheld projectile.
/// </summary>
public abstract class HandholdProjectile : ModProjectile
public abstract class HandheldProjectile : ModProjectile
{
/// <summary>
/// The rotation of texture.
Expand Down Expand Up @@ -35,11 +36,54 @@ public abstract class HandholdProjectile : ModProjectile
public float DepartLength = 50;
public Vector2 ArmRootPos = Vector2.zeroVector;

/// <summary>
/// World-space cursor of the owner. Main.MouseWorld already accounts for local gravity.
/// </summary>
protected Vector2 AimPosition
{
get
{
Player player = Main.player[Projectile.owner];
player.ListenMouseWorld();
return Projectile.owner == Main.myPlayer ? Main.MouseWorld : player.MouseWorld();
}
}

protected Vector2 GravityDrawOffset => DrawOffset * new Vector2(1, Main.player[Projectile.owner].gravDir);

private bool rotationInitialized;

public override void OnSpawn(IEntitySource source)
{
base.OnSpawn(source);
InitializeRotation();
}

private void InitializeRotation()
{
rotationInitialized = true;
// Spawn velocity is synchronized by Terraria, unlike the observer's cursor.
Projectile.rotation = Projectile.velocity.SafeNormalize(Vector2.UnitX * Main.player[Projectile.owner].direction).ToRotation() + MathHelper.PiOver4;
}

public override bool PreAI()
{
Player player = Main.player[Projectile.owner];
if (!player.active || player.dead)
{
Projectile.Kill();
return false;
}
// Received projectiles do not run OnSpawn.
if (!rotationInitialized)
{
InitializeRotation();
}
player.ListenMouseWorld();
return true;
}

public override bool ShouldUpdatePosition() => false;

public override void SetDefaults()
{
Projectile.width = 32;
Expand Down Expand Up @@ -69,13 +113,13 @@ public virtual void HeldProjectileAI()
{
Player player = Main.player[Projectile.owner];
player.heldProj = Projectile.whoAmI;
ArmRootPos = player.MountedCenter + new Vector2(-4 * player.direction, -2);
ArmRootPos = player.MountedCenter + new Vector2(-4 * player.direction, -2 * player.gravDir);
player.SetCompositeArmFront(true, Player.CompositeArmStretchAmount.Full, (Projectile.rotation - MathF.PI * 0.25f) * player.gravDir - MathF.PI * 0.5f);
Vector2 mouseToPlayer = Main.MouseWorld - ArmRootPos;
mouseToPlayer = Vector2.Normalize(mouseToPlayer);
Vector2 mouseToPlayer = AimPosition - ArmRootPos;
mouseToPlayer = mouseToPlayer.SafeNormalize(Vector2.UnitX * player.direction);
if (player.controlUseItem)
{
float addRot = -MathF.Asin(Vector3.Cross(new Vector3(mouseToPlayer, 0), new Vector3(new Vector2(0, 1).RotatedBy(Projectile.rotation - Math.PI * 0.75), 0)).Z);
float addRot = MathHelper.WrapAngle(mouseToPlayer.ToRotation() + MathHelper.PiOver4 - Projectile.rotation);
addRot *= LerpFactorOfRotation;
if (Math.Abs(addRot) > MaxRotationSpeed)
{
Expand All @@ -102,7 +146,7 @@ public virtual void HeldProjectileAI()

public virtual void RemoveExtraSameProjectiles(Player owner)
{
if (owner.ownedProjectileCounts[Projectile.type] > 1)
if (Projectile.owner == Main.myPlayer && owner.ownedProjectileCounts[Projectile.type] > 1)
{
int count = owner.ownedProjectileCounts[Projectile.type];
for (int a = Main.projectile.Length - 1; a >= 0; a--)
Expand Down Expand Up @@ -137,12 +181,12 @@ public virtual void DrawBaseTexture(Color lightColor)
Player player = Main.player[Projectile.owner];
var texMain = (Texture2D)ModContent.Request<Texture2D>(Texture);
SpriteEffects se = SpriteEffects.None;
if (player.direction == -1)
if (player.direction * player.gravDir == -1)
{
se = SpriteEffects.FlipVertically;
}

float rot = Projectile.rotation - (float)(Math.PI * 0.25) + TextureRotation * player.direction;
Main.spriteBatch.Draw(texMain, Projectile.Center - Main.screenPosition + DrawOffset, null, lightColor, rot, texMain.Size() / 2f, 1f, se, 0);
float rot = Projectile.rotation - (float)(Math.PI * 0.25) + TextureRotation * player.direction * player.gravDir;
Main.spriteBatch.Draw(texMain, Projectile.Center - Main.screenPosition + GravityDrawOffset, null, lightColor, rot, texMain.Size() / 2f, 1f, se, 0);
}
}
172 changes: 172 additions & 0 deletions Sources/Everglow.UnitTests/Function/HandheldProjectileTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using System.Reflection;
using Everglow.Commons;
using Everglow.Commons.Templates.Weapons;
using Everglow.Yggdrasil.YggdrasilTown.Projectiles.Magic;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.ModLoader;

namespace Everglow.UnitTests.Function;

[TestClass]
[DoNotParallelize]
public class HandheldProjectileTest
{
private Player[] originalPlayers = null!;
private int originalMyPlayer;
private Vector2 originalScreenPosition;
private int originalMouseX;
private int originalMouseY;
private EverglowPlayer? originalModPlayer;
private Player owner = null!;
private EverglowPlayer modPlayer = null!;
private TestProjectile held = null!;

private static readonly PropertyInfo InstanceProperty = typeof(ContentInstance<EverglowPlayer>).GetProperty("Instance")!;
private static readonly FieldInfo MouseWorldField = typeof(EverglowPlayer).GetField("mouseWorld", BindingFlags.Instance | BindingFlags.NonPublic)!;

[TestInitialize]
public void Initialize()
{
Program.SavePath = string.Empty;
originalPlayers = Main.player;
originalMyPlayer = Main.myPlayer;
originalScreenPosition = Main.screenPosition;
originalMouseX = Main.mouseX;
originalMouseY = Main.mouseY;
originalModPlayer = ContentInstance<EverglowPlayer>.Instance;

owner = new Player { whoAmI = 1, active = true, controlUseItem = true, itemTimeMax = 30 };
owner.Center = new Vector2(100, 100);
Main.player = new Player[Main.maxPlayers + 1];
Main.player[0] = new Player { whoAmI = 0 };
Main.player[1] = owner;
Main.myPlayer = 0;
Main.screenPosition = Vector2.Zero;
Main.mouseX = 300;
Main.mouseY = 100;

modPlayer = (EverglowPlayer)new EverglowPlayer().NewInstance(owner);
InstanceProperty.SetValue(null, modPlayer);
typeof(Player).GetField("modPlayers", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(owner, new ModPlayer[] { modPlayer });
held = (TestProjectile)new TestProjectile().NewInstance(new Projectile { owner = 1, active = true });
held.SetDefaults();
held.Projectile.rotation = MathHelper.PiOver4;
}

[TestCleanup]
public void Cleanup()
{
Main.player = originalPlayers;
Main.myPlayer = originalMyPlayer;
Main.screenPosition = originalScreenPosition;
Main.mouseX = originalMouseX;
Main.mouseY = originalMouseY;
InstanceProperty.SetValue(null, originalModPlayer);
}

[TestMethod]
public void RemoteOwner_AimsAtSynchronizedMouseInsteadOfLocalMouse()
{
MouseWorldField.SetValue(modPlayer, new Vector2(96, 298));
held.HeldProjectileAI();
Assert.AreEqual(96f, held.Projectile.Center.X, 0.001f);
Assert.AreEqual(148f, held.Projectile.Center.Y, 0.001f);
}

[TestMethod]
public void InvertedGravity_MirrorsArmRootOffset()
{
owner.gravDir = -1;
MouseWorldField.SetValue(modPlayer, new Vector2(296, 102));
held.HeldProjectileAI();
Assert.AreEqual(new Vector2(96, 102), held.ArmRootPos);
}

[TestMethod]
public void AimAtArmRoot_DoesNotProduceNonFinitePosition()
{
Main.myPlayer = 1;
Main.mouseX = 96;
Main.mouseY = 98;
MouseWorldField.SetValue(modPlayer, new Vector2(96, 98));
held.HeldProjectileAI();
Assert.IsTrue(float.IsFinite(held.Projectile.rotation));
Assert.IsTrue(float.IsFinite(held.Projectile.Center.X));
Assert.IsTrue(float.IsFinite(held.Projectile.Center.Y));
}

[TestMethod]
public void SpawnDirection_UsesNetworkedVelocityInsteadOfObserverMouse()
{
held.Projectile.velocity = new Vector2(0, -12);
held.OnSpawn(new Terraria.DataStructures.EntitySource_Misc("Test"));
Assert.AreEqual(-MathHelper.PiOver4, held.Projectile.rotation, 0.001f);
}

[TestMethod]
public void LocalOwner_RequestsMouseSynchronizationBeforeDerivedAI()
{
Main.myPlayer = 1;
Assert.IsTrue(held.PreAI());
var listenField = typeof(EverglowPlayer).GetField("listenMouseWorld", BindingFlags.Instance | BindingFlags.NonPublic)!;
Assert.IsTrue((bool)listenField.GetValue(modPlayer)!);
}

[TestMethod]
public void OppositeAim_TurnsToTheOtherSide()
{
MouseWorldField.SetValue(modPlayer, new Vector2(-104, 98));
held.HeldProjectileAI();
Assert.AreEqual(46f, held.Projectile.Center.X, 0.001f);
Assert.AreEqual(98f, held.Projectile.Center.Y, 0.001f);
Assert.AreEqual(-1, owner.direction);
}

[TestMethod]
public void RemoteOwnerGravity_IsIndependentOfObserverGravity()
{
Main.player[0].gravDir = -1;
MouseWorldField.SetValue(modPlayer, new Vector2(96, 298));
held.HeldProjectileAI();
Assert.AreEqual(148f, held.Projectile.Center.Y, 0.001f);
}

[TestMethod]
public void InvertedGravity_MirrorsDrawOffset()
{
owner.gravDir = -1;
held.DrawOffset = new Vector2(-6, -8);
Assert.AreEqual(new Vector2(-6, 8), held.DrawOffsetInWorld);
}

[TestMethod]
public void CrystalBeforeParentSync_WaitsWithoutDereferencingMissingParent()
{
var crystal = (YggdrasilAmberLaser_crystal)new YggdrasilAmberLaser_crystal().NewInstance(new Projectile { owner = 1, timeLeft = 900 });
crystal.AI();
Assert.IsFalse(crystal.EndAI);
}

[TestMethod]
public void ReceivedProjectile_InitializesDirectionWithoutOnSpawn()
{
held.Projectile.velocity = new Vector2(0, -12);
held.PreAI();
Assert.AreEqual(-MathHelper.PiOver4, held.Projectile.rotation, 0.001f);
}

[TestMethod]
public void ReceivedCrystal_InitializesTrackWithoutOnSpawn()
{
var projectile = new Projectile { owner = 1, timeLeft = 900, Center = new Vector2(120, 140) };
var crystal = (YggdrasilAmberLaser_crystal)new YggdrasilAmberLaser_crystal().NewInstance(projectile);
crystal.AI();
Assert.AreEqual(new Vector2(120, 140), crystal.CrystalTrack[0]);
}

private sealed class TestProjectile : HandheldProjectile
{
public Vector2 DrawOffsetInWorld => GravityDrawOffset;
}
}
2 changes: 1 addition & 1 deletion Sources/Modules/Example/Test/HandholdProj.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Everglow.Example.Test;

public class HandholdProj : HandholdProjectile
public class HandholdProj : HandheldProjectile
{
public override string LocalizationCategory => Everglow.Commons.Utilities.LocalizationUtils.Categories.MiscsProjectiles;
}
3 changes: 1 addition & 2 deletions Sources/Modules/Example/Test/HandholdProj2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

namespace Everglow.Example.Test;

public class HandholdProj2 : HandholdProjectile
public class HandholdProj2 : HandheldProjectile
{
public override string LocalizationCategory => Everglow.Commons.Utilities.LocalizationUtils.Categories.MiscsProjectiles;

public override void SetDef()
{
TextureRotation = 1.204f;
base.SetDef();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public override void SetDefaults()

public override void HoldItem(Player player)
{
if (player.whoAmI != Main.myPlayer)
{
return;
}
player.ListenMouseWorld();
if (player.ownedProjectileCounts[ModContent.ProjectileType<GildingRevolver_Hold>()] <= 0)
{
Projectile.NewProjectile(Item.GetSource_FromAI(), player.Center, Vector2.zeroVector, ModContent.ProjectileType<GildingRevolver_Hold>(), 0, 0, player.whoAmI);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public override void SetDefaults()

public override void HoldItem(Player player)
{
if (player.whoAmI != Main.myPlayer)
{
return;
}
player.ListenMouseWorld();
if (UIBar is not null && UIBar.Active)
{
return;
Expand Down
Loading
Loading