Pingas Pi
Terrarian
Haiiii!! :3
I'm somewhat new to modding and have a basic understanding of c#, but I have an issue.
I've been working on a spear (built from ExampleSpear) and a homing projectile (built from the ground up) for the last few days. I've just about managed to get them working completely except for one thing, the homing projectile appears from where the spear was at the time of use, rather than where the spear is currently. This becomes really obvious the the player is moving at fast speeds and the spear is fired perpendicular to their path.
It sounds like a really simple and easy fix, and maybe it is! But I've tried just about everything I can think of: spawning it from Projectile.center, player.center, Player.MountedCenter (yes I know the latter 2 spawn it on the player,) but no matter what there's an offset, so I have come here for help.
My best guesses:
-I'm not spawning it from the right place or using the right vector
-The projectile it fires accelerates from a slow-ish speed, which may exacerbate the problem
-It's using the initial vectors player/projectile.center, and I need to get the current ones elsewhere
Pictures for clarity:
*As you can see by the afterimages of the character, I'm moving fast which makes the offset very obvious.
Compare this to what it should look like if we look at the North Pole, where the projectile spawns right where the tip of the spear is no matter what.
Here are the pieces of relevant code.
Spear:
float rotation = MathHelper.ToRadians(10);
Vector2 velocity = Projectile.velocity * 3f; //initial speed
Vector2 newVelocity = velocity.RotatedBy(rotation);
Vector2 newVelocity1 = velocity.RotatedBy(-rotation);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, newVelocity, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, newVelocity1, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, velocity, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
What this does is just create three new velocities and make three separate projectiles, I'll figure out a more elegant way to do this later, but what matters is the offset, where I currently just use Projectile.center for the position.
Homing projectile:
void NormalAI()
{
Projectile.rotation = (Projectile.velocity.ToRotation() + MathHelper.PiOver4);
if (timeAlive < 10)
Projectile.alpha = 255;
if (timeAlive <= 20 && timeAlive >= 10)
Projectile.alpha -= 32;
if (timeAlive < 20)
{
Projectile.velocity *= 1.135f;
}
else if (timeAlive >= 25 && !fadeAndKillFlag)
{
Projectile.velocity *= 0.85f;
}
if (fadeAndKillFlag)
{
if (!adjustTimeLeftFlag)
{
adjustTimeLeftFlag = true;
Projectile.timeLeft = 30;
Projectile.velocity.Normalize();
Projectile.velocity *= 7;
}
Projectile.velocity *= 0.80f;
Projectile.alpha += 11;
Projectile.scale -= 0.015f;
}
else if ((Projectile.velocity.X >= -0.6f && Projectile.velocity.Y >= -0.6f && Projectile.velocity.X <= 0.6f && Projectile.velocity.Y <= 0.6f) && !fadeAndKillFlag)
{
for (int i = 0; i < 5; i++)
{
Projectile.velocity *= 0.4f;
}
whichAIToUse = 1;
}
}
If this isn't enough information to work with, here are the full versions:
spear:
public class VoidShardSpearProjectile : ModProjectile
{
protected virtual float HoldoutRangeMin => 85f;
protected virtual float HoldoutRangeMax => 190f;
float opacity = 255; //might be unnecessary
//clones spear but changes the size
public override void SetDefaults()
{
Projectile.CloneDefaults(ProjectileID.Spear);
Projectile.width = 25;
Projectile.height = 25;
}
public override bool PreAI()
{
Player player = Main.player[Projectile.owner]; // Since we access the owner player instance so much, it's useful to create a helper local variable for this
int duration = player.itemAnimationMax; // Define the duration the projectile will exist in frames
player.heldProj = Projectile.whoAmI; // Update the player's held projectile id
// Reset projectile time left if necessary
if (Projectile.timeLeft > duration)
{
Projectile.timeLeft = duration;
}
Projectile.velocity = Vector2.Normalize(Projectile.velocity);
float halfDuration = duration * 0.5f;
float progress;
bool isGoOut = true;
if (Projectile.timeLeft < halfDuration)
{
// Ease-in: starts slow, then speeds up
progress = 0.5f * (1 - (float)Math.Cos((Math.PI * Projectile.timeLeft) / halfDuration));
isGoOut = false;
}
else
{
// Ease-out: starts fast, then slows down
progress = 0.5f * (1 + (float)Math.Cos((Math.PI * (Projectile.timeLeft - halfDuration)) / halfDuration));
isGoOut = true;
}
if (isGoOut)
opacity -= 60;
else if (!isGoOut && progress < 0.1)
opacity += 60;
if (opacity > 255)
opacity = 255;
if (opacity < 0)
opacity = 0;
Projectile.alpha = Convert.ToByte(opacity);
if (Projectile.timeLeft == 22) //changing this can make projectiles fail to spawn!!!
{
SoundEngine.PlaySound(SoundID.Item8); //sound when projectiles spawn
float rotation = MathHelper.ToRadians(10); //simply to reuse
//there is likely a more efficient way to do this
Vector2 newVelocity = (Projectile.velocity * 3f).RotatedBy(rotation);
Vector2 newVelocity1 = (Projectile.velocity * 3f).RotatedBy(-rotation);
Vector2 spearTip = Projectile.Center;
//Spawns 3 projectiles at each angle
Projectile.NewProjectile(Projectile.GetSource_FromThis(), spearTip, newVelocity, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), spearTip, newVelocity1, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), spearTip, (Projectile.velocity * 3f), ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
}
// Move the projectile from the HoldoutRangeMin to the HoldoutRangeMax and back, using SmoothStep for easing the movement
Projectile.Center = player.MountedCenter + Vector2.SmoothStep(Projectile.velocity * HoldoutRangeMin, Projectile.velocity * HoldoutRangeMax, progress); //look into this
// Apply proper rotation to the sprite.
if (Projectile.spriteDirection == -1)
{
// If sprite is facing left, rotate 45 degrees
Projectile.rotation += MathHelper.ToRadians(45f);
}
else
{
// If sprite is facing right, rotate 135 degrees
Projectile.rotation += MathHelper.ToRadians(135f);
}
// Avoid spawning dusts on dedicated servers
if (!Main.dedServ)
{
// These dusts are added later, for the 'ExampleMod' effect
if (Main.rand.NextBool(3))
{
Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.CorruptPlants, Projectile.velocity.X * 2f, Projectile.velocity.Y * 2f, Alpha: 128, Scale: 1.2f);
}
if (Main.rand.NextBool(8))
{
Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.CursedTorch, Projectile.velocity.X * -1.5f, Projectile.velocity.Y * -1.5f, Alpha: 128, Scale: 0.8f);
}
if (Main.rand.NextBool(4))
{
Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.CorruptGibs, Alpha: 128, Scale: 1.2f);
}
}
return false; // Don't execute vanilla AI.
}
}
Homing:
public class SpaceProjectile : ModProjectile
{
//find ways to use ai and LocalAI
int maxTimeLeft; //total frames
int timeAlive; //how many frames it has been alive for after firing
float changeAngleBy = 0;
float maxVelocity = 30f;
bool fadeAndKillFlag; //normal flag for kill animation
bool adjustTimeLeftFlag = false; //normal flag to not repeat setting timeLeft to the same value
bool resetForHomingFlag = false; //homing flag to reset some values, like time alive (for convenience)
int whichAIToUse = 0; //used to determine ai in AI(), not bool because I may make more than 2 sub-ais
int FramesTostartSlowDown = 20; //frames before NormalAI() slows the projectile *might delete
public override void SetDefaults()
{
Projectile.width = 42;
Projectile.height = 42;
Projectile.scale = 0.8f; //might change later!
Projectile.alpha = 255; //255 is transparent, don't let anyone lie to you
Projectile.noEnchantmentVisuals = false;
Projectile.friendly = true; //can hurt enemies and not players
Projectile.hostile = false;
Projectile.knockBack = 2f;
Projectile.DamageType = DamageClass.Melee;
Projectile.aiStyle = 0; //-1 does the same thing I think
Projectile.penetrate = 3;
Projectile.tileCollide = false; //disables automatic collision detection
Projectile.ignoreWater = true;
Projectile.localNPCHitCooldown = 0; //figure this out pls
Projectile.timeLeft = 600; //10 seconds, but it will NEVER last this long
maxTimeLeft = Projectile.timeLeft; //must initialize in here, since timeLeft isn't set yet above.
FramesTostartSlowDown = 20;
}
//is split up into multiple parts similar to ExampleJavelinProjectile
public override void AI()
{
timeAlive++; //old way to calc was: timeAlive = maxTimeLeft - Projectile.timeLeft
//add purple light that changes with projectile alpha, instead of putting projectile.light in defaults
Lighting.AddLight(Projectile.Center, Color.MediumPurple.ToVector3() * (1.05f - (float)Projectile.alpha/255f) * Main.essScale); //since alpha is inversely related to 0-255, we must do 1 - alpha/255 to get the proper decimal, we also need to convert the alpha to a float
//finds the tile at the projectile's position
int tileX = (int)(Projectile.position.X / 16f);
int tileY = (int)(Projectile.position.Y / 16f);
Tile tile = Main.tile[tileX, tileY];
if (TrevorsMod.IsSolidTile(tile)) //determines if that tile is an actual block, and begins kill animation if so
{
fadeAndKillFlag = true; //allows kill animation for projectile
}
//handles which AI instead of using 2 projectiles
if (whichAIToUse == 0)
{
NormalAI(); //ai when first fired
}
else if (whichAIToUse == 1)
{
HomingAI(); //ai if projectile expires in the air
}
//ensures alpha can't go below 0, or above 255, is run after ais to be sure
if (Projectile.alpha < 0) Projectile.alpha = 0;
if (Projectile.alpha > 255) Projectile.alpha = 255;
}
//ai to spawn, fade in + accelerate, then slow down; if hits something, kill animation plays
void NormalAI()
{
Projectile.rotation = (Projectile.velocity.ToRotation() + MathHelper.PiOver4); //faces diection of travel, offset 45 degrees, or Pi/4 radians
if (timeAlive < 10)
Projectile.alpha = 255;
//fades in after 10 frames, and stops after another 10 frames
if (timeAlive <= FramesTostartSlowDown && timeAlive >= FramesTostartSlowDown / 2)
Projectile.alpha -= 32; //0 is fully visible, so 255 - 10 * 26 does the trick
//accelerates for a period of time
if (timeAlive < FramesTostartSlowDown)
{
Projectile.velocity *= 1.135f; //arbitrary
}
//slows down the projectile unless the projectile has already hit something
else if (timeAlive >= FramesTostartSlowDown * 5 / 4 && !fadeAndKillFlag)
{
Projectile.velocity *= 0.85f;
}
//kill animation when hitting something
if (fadeAndKillFlag)
{
if (!adjustTimeLeftFlag) //runs once
{
adjustTimeLeftFlag = true;
Projectile.timeLeft = 30; //this acts as the kill timer, by reducing the timeleft greatly
Projectile.velocity.Normalize();
Projectile.velocity *= 7;
}
Projectile.velocity *= 0.80f; //faster slow down
Projectile.alpha += 11; //fade out
Projectile.scale -= 0.015f;//mild shrinking effect
}
//slowing animation when in air
else if ((Projectile.velocity.X >= -0.6f && Projectile.velocity.Y >= -0.6f && Projectile.velocity.X <= 0.6f && Projectile.velocity.Y <= 0.6f) && !fadeAndKillFlag) //checks if a projectile's velocity is below a certain point
{
//repeats for i number of frames
for (int i = 0; i < 5; i++)
{
Projectile.velocity *= 0.4f; //even faster, faster slow down
//no alpha changes
}
whichAIToUse = 1; //changes to use HomingAI()
}
}
//ai to spin, choose a target (or die), home on them, and then die
void HomingAI()
{
//reset certain things for changed ai
if (!resetForHomingFlag)
{
resetForHomingFlag = true;
timeAlive = 0; //counts up from 0 again
Projectile.damage = Projectile.damage * 2 / 3; //2 thirds damage
Projectile.penetrate = 1; //no penetration
}
//speeds up rotation for a bit
if (timeAlive < 50 || fadeAndKillFlag)
{
Projectile.rotation += MathHelper.ToRadians(changeAngleBy); //converts the changing angle to radians
changeAngleBy++; //allows spin acceleration
if (changeAngleBy >= 40)
changeAngleBy = 40; //ensures 40 is the max
}
//The actual homing logic; look more into this
else
{
float detectionRange = 1200f; // Range in pixels to detect enemies
float homingStrength = 0.1f; // How strongly the projectile turns towards its target
NPC target = null; //default value for targeted npc
float closestDistance = detectionRange; //initializes to the max range, and may decrease if any enemies are closer
for (int i = 0; i < Main.maxNPCs; i++)
{
NPC npc = Main.npc;
if (npc.CanBeChasedBy(Projectile, false)) // Check if the NPC is a valid target
{
float distance = Vector2.Distance(npc.Center, Projectile.Center);
if (distance < closestDistance)
{
closestDistance = distance;
target = npc;
}
}
}
//if theres no targets in range at time of checking, allows kill animation
if (target == null && timeAlive <= 60)
{
fadeAndKillFlag = true;
}
//homes on target if in range
if (target != null)
{
// Calculate direction to target
Vector2 direction = target.Center - Projectile.Center;
direction.Normalize();
direction *= maxVelocity; // Max speed the projectile can reach
maxVelocity += 0.5f;
// Smoothly adjust velocity
Projectile.velocity = Vector2.Lerp(Projectile.velocity, direction, homingStrength);
}
// Adjust rotation to match velocity direction
Projectile.rotation = Projectile.velocity.ToRotation() + MathHelper.PiOver4;
}
//homing kill animation
if (fadeAndKillFlag || Projectile.timeLeft < 20)
{
if (!adjustTimeLeftFlag)
{
Projectile.timeLeft = 15;
adjustTimeLeftFlag = true;
}
Projectile.velocity *= 0.55f;
Projectile.alpha += 22;
Projectile.scale -= 0.05f;
}
}
//allows kill animation, and spawns purple particles from the nights edge
public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
{
fadeAndKillFlag = true;
//no clue how this works, I copied it from ExampleSwingingEnergySwordProjectile, and changed the particle type
ParticleOrchestrator.RequestParticleSpawn(clientOnly: false, ParticleOrchestraType.NightsEdge,
new ParticleOrchestraSettings { PositionInWorld = Main.rand.NextVector2FromRectangle(target.Hitbox) },
Projectile.owner); //seems like it produces a particle at a random position inside of the target's hitbox
hit.HitDirection = (Main.player[Projectile.owner].Center.X < target.Center.X) ? 1 : (-1); //clueless
}
}
}
If I find the solution I'll post it here.
I'm somewhat new to modding and have a basic understanding of c#, but I have an issue.
I've been working on a spear (built from ExampleSpear) and a homing projectile (built from the ground up) for the last few days. I've just about managed to get them working completely except for one thing, the homing projectile appears from where the spear was at the time of use, rather than where the spear is currently. This becomes really obvious the the player is moving at fast speeds and the spear is fired perpendicular to their path.
It sounds like a really simple and easy fix, and maybe it is! But I've tried just about everything I can think of: spawning it from Projectile.center, player.center, Player.MountedCenter (yes I know the latter 2 spawn it on the player,) but no matter what there's an offset, so I have come here for help.
My best guesses:
-I'm not spawning it from the right place or using the right vector
-The projectile it fires accelerates from a slow-ish speed, which may exacerbate the problem
-It's using the initial vectors player/projectile.center, and I need to get the current ones elsewhere
Pictures for clarity:
*As you can see by the afterimages of the character, I'm moving fast which makes the offset very obvious.
Compare this to what it should look like if we look at the North Pole, where the projectile spawns right where the tip of the spear is no matter what.
Here are the pieces of relevant code.
Spear:
float rotation = MathHelper.ToRadians(10);
Vector2 velocity = Projectile.velocity * 3f; //initial speed
Vector2 newVelocity = velocity.RotatedBy(rotation);
Vector2 newVelocity1 = velocity.RotatedBy(-rotation);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, newVelocity, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, newVelocity1, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, velocity, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
What this does is just create three new velocities and make three separate projectiles, I'll figure out a more elegant way to do this later, but what matters is the offset, where I currently just use Projectile.center for the position.
Homing projectile:
void NormalAI()
{
Projectile.rotation = (Projectile.velocity.ToRotation() + MathHelper.PiOver4);
if (timeAlive < 10)
Projectile.alpha = 255;
if (timeAlive <= 20 && timeAlive >= 10)
Projectile.alpha -= 32;
if (timeAlive < 20)
{
Projectile.velocity *= 1.135f;
}
else if (timeAlive >= 25 && !fadeAndKillFlag)
{
Projectile.velocity *= 0.85f;
}
if (fadeAndKillFlag)
{
if (!adjustTimeLeftFlag)
{
adjustTimeLeftFlag = true;
Projectile.timeLeft = 30;
Projectile.velocity.Normalize();
Projectile.velocity *= 7;
}
Projectile.velocity *= 0.80f;
Projectile.alpha += 11;
Projectile.scale -= 0.015f;
}
else if ((Projectile.velocity.X >= -0.6f && Projectile.velocity.Y >= -0.6f && Projectile.velocity.X <= 0.6f && Projectile.velocity.Y <= 0.6f) && !fadeAndKillFlag)
{
for (int i = 0; i < 5; i++)
{
Projectile.velocity *= 0.4f;
}
whichAIToUse = 1;
}
}
If this isn't enough information to work with, here are the full versions:
spear:
public class VoidShardSpearProjectile : ModProjectile
{
protected virtual float HoldoutRangeMin => 85f;
protected virtual float HoldoutRangeMax => 190f;
float opacity = 255; //might be unnecessary
//clones spear but changes the size
public override void SetDefaults()
{
Projectile.CloneDefaults(ProjectileID.Spear);
Projectile.width = 25;
Projectile.height = 25;
}
public override bool PreAI()
{
Player player = Main.player[Projectile.owner]; // Since we access the owner player instance so much, it's useful to create a helper local variable for this
int duration = player.itemAnimationMax; // Define the duration the projectile will exist in frames
player.heldProj = Projectile.whoAmI; // Update the player's held projectile id
// Reset projectile time left if necessary
if (Projectile.timeLeft > duration)
{
Projectile.timeLeft = duration;
}
Projectile.velocity = Vector2.Normalize(Projectile.velocity);
float halfDuration = duration * 0.5f;
float progress;
bool isGoOut = true;
if (Projectile.timeLeft < halfDuration)
{
// Ease-in: starts slow, then speeds up
progress = 0.5f * (1 - (float)Math.Cos((Math.PI * Projectile.timeLeft) / halfDuration));
isGoOut = false;
}
else
{
// Ease-out: starts fast, then slows down
progress = 0.5f * (1 + (float)Math.Cos((Math.PI * (Projectile.timeLeft - halfDuration)) / halfDuration));
isGoOut = true;
}
if (isGoOut)
opacity -= 60;
else if (!isGoOut && progress < 0.1)
opacity += 60;
if (opacity > 255)
opacity = 255;
if (opacity < 0)
opacity = 0;
Projectile.alpha = Convert.ToByte(opacity);
if (Projectile.timeLeft == 22) //changing this can make projectiles fail to spawn!!!
{
SoundEngine.PlaySound(SoundID.Item8); //sound when projectiles spawn
float rotation = MathHelper.ToRadians(10); //simply to reuse
//there is likely a more efficient way to do this
Vector2 newVelocity = (Projectile.velocity * 3f).RotatedBy(rotation);
Vector2 newVelocity1 = (Projectile.velocity * 3f).RotatedBy(-rotation);
Vector2 spearTip = Projectile.Center;
//Spawns 3 projectiles at each angle
Projectile.NewProjectile(Projectile.GetSource_FromThis(), spearTip, newVelocity, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), spearTip, newVelocity1, ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
Projectile.NewProjectile(Projectile.GetSource_FromThis(), spearTip, (Projectile.velocity * 3f), ModContent.ProjectileType<SpaceProjectile>(), Projectile.damage * 2 / 3, 5f, Projectile.owner);
}
// Move the projectile from the HoldoutRangeMin to the HoldoutRangeMax and back, using SmoothStep for easing the movement
Projectile.Center = player.MountedCenter + Vector2.SmoothStep(Projectile.velocity * HoldoutRangeMin, Projectile.velocity * HoldoutRangeMax, progress); //look into this
// Apply proper rotation to the sprite.
if (Projectile.spriteDirection == -1)
{
// If sprite is facing left, rotate 45 degrees
Projectile.rotation += MathHelper.ToRadians(45f);
}
else
{
// If sprite is facing right, rotate 135 degrees
Projectile.rotation += MathHelper.ToRadians(135f);
}
// Avoid spawning dusts on dedicated servers
if (!Main.dedServ)
{
// These dusts are added later, for the 'ExampleMod' effect
if (Main.rand.NextBool(3))
{
Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.CorruptPlants, Projectile.velocity.X * 2f, Projectile.velocity.Y * 2f, Alpha: 128, Scale: 1.2f);
}
if (Main.rand.NextBool(8))
{
Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.CursedTorch, Projectile.velocity.X * -1.5f, Projectile.velocity.Y * -1.5f, Alpha: 128, Scale: 0.8f);
}
if (Main.rand.NextBool(4))
{
Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.CorruptGibs, Alpha: 128, Scale: 1.2f);
}
}
return false; // Don't execute vanilla AI.
}
}
Homing:
public class SpaceProjectile : ModProjectile
{
//find ways to use ai and LocalAI
int maxTimeLeft; //total frames
int timeAlive; //how many frames it has been alive for after firing
float changeAngleBy = 0;
float maxVelocity = 30f;
bool fadeAndKillFlag; //normal flag for kill animation
bool adjustTimeLeftFlag = false; //normal flag to not repeat setting timeLeft to the same value
bool resetForHomingFlag = false; //homing flag to reset some values, like time alive (for convenience)
int whichAIToUse = 0; //used to determine ai in AI(), not bool because I may make more than 2 sub-ais
int FramesTostartSlowDown = 20; //frames before NormalAI() slows the projectile *might delete
public override void SetDefaults()
{
Projectile.width = 42;
Projectile.height = 42;
Projectile.scale = 0.8f; //might change later!
Projectile.alpha = 255; //255 is transparent, don't let anyone lie to you
Projectile.noEnchantmentVisuals = false;
Projectile.friendly = true; //can hurt enemies and not players
Projectile.hostile = false;
Projectile.knockBack = 2f;
Projectile.DamageType = DamageClass.Melee;
Projectile.aiStyle = 0; //-1 does the same thing I think
Projectile.penetrate = 3;
Projectile.tileCollide = false; //disables automatic collision detection
Projectile.ignoreWater = true;
Projectile.localNPCHitCooldown = 0; //figure this out pls
Projectile.timeLeft = 600; //10 seconds, but it will NEVER last this long
maxTimeLeft = Projectile.timeLeft; //must initialize in here, since timeLeft isn't set yet above.
FramesTostartSlowDown = 20;
}
//is split up into multiple parts similar to ExampleJavelinProjectile
public override void AI()
{
timeAlive++; //old way to calc was: timeAlive = maxTimeLeft - Projectile.timeLeft
//add purple light that changes with projectile alpha, instead of putting projectile.light in defaults
Lighting.AddLight(Projectile.Center, Color.MediumPurple.ToVector3() * (1.05f - (float)Projectile.alpha/255f) * Main.essScale); //since alpha is inversely related to 0-255, we must do 1 - alpha/255 to get the proper decimal, we also need to convert the alpha to a float
//finds the tile at the projectile's position
int tileX = (int)(Projectile.position.X / 16f);
int tileY = (int)(Projectile.position.Y / 16f);
Tile tile = Main.tile[tileX, tileY];
if (TrevorsMod.IsSolidTile(tile)) //determines if that tile is an actual block, and begins kill animation if so
{
fadeAndKillFlag = true; //allows kill animation for projectile
}
//handles which AI instead of using 2 projectiles
if (whichAIToUse == 0)
{
NormalAI(); //ai when first fired
}
else if (whichAIToUse == 1)
{
HomingAI(); //ai if projectile expires in the air
}
//ensures alpha can't go below 0, or above 255, is run after ais to be sure
if (Projectile.alpha < 0) Projectile.alpha = 0;
if (Projectile.alpha > 255) Projectile.alpha = 255;
}
//ai to spawn, fade in + accelerate, then slow down; if hits something, kill animation plays
void NormalAI()
{
Projectile.rotation = (Projectile.velocity.ToRotation() + MathHelper.PiOver4); //faces diection of travel, offset 45 degrees, or Pi/4 radians
if (timeAlive < 10)
Projectile.alpha = 255;
//fades in after 10 frames, and stops after another 10 frames
if (timeAlive <= FramesTostartSlowDown && timeAlive >= FramesTostartSlowDown / 2)
Projectile.alpha -= 32; //0 is fully visible, so 255 - 10 * 26 does the trick
//accelerates for a period of time
if (timeAlive < FramesTostartSlowDown)
{
Projectile.velocity *= 1.135f; //arbitrary
}
//slows down the projectile unless the projectile has already hit something
else if (timeAlive >= FramesTostartSlowDown * 5 / 4 && !fadeAndKillFlag)
{
Projectile.velocity *= 0.85f;
}
//kill animation when hitting something
if (fadeAndKillFlag)
{
if (!adjustTimeLeftFlag) //runs once
{
adjustTimeLeftFlag = true;
Projectile.timeLeft = 30; //this acts as the kill timer, by reducing the timeleft greatly
Projectile.velocity.Normalize();
Projectile.velocity *= 7;
}
Projectile.velocity *= 0.80f; //faster slow down
Projectile.alpha += 11; //fade out
Projectile.scale -= 0.015f;//mild shrinking effect
}
//slowing animation when in air
else if ((Projectile.velocity.X >= -0.6f && Projectile.velocity.Y >= -0.6f && Projectile.velocity.X <= 0.6f && Projectile.velocity.Y <= 0.6f) && !fadeAndKillFlag) //checks if a projectile's velocity is below a certain point
{
//repeats for i number of frames
for (int i = 0; i < 5; i++)
{
Projectile.velocity *= 0.4f; //even faster, faster slow down
//no alpha changes
}
whichAIToUse = 1; //changes to use HomingAI()
}
}
//ai to spin, choose a target (or die), home on them, and then die
void HomingAI()
{
//reset certain things for changed ai
if (!resetForHomingFlag)
{
resetForHomingFlag = true;
timeAlive = 0; //counts up from 0 again
Projectile.damage = Projectile.damage * 2 / 3; //2 thirds damage
Projectile.penetrate = 1; //no penetration
}
//speeds up rotation for a bit
if (timeAlive < 50 || fadeAndKillFlag)
{
Projectile.rotation += MathHelper.ToRadians(changeAngleBy); //converts the changing angle to radians
changeAngleBy++; //allows spin acceleration
if (changeAngleBy >= 40)
changeAngleBy = 40; //ensures 40 is the max
}
//The actual homing logic; look more into this
else
{
float detectionRange = 1200f; // Range in pixels to detect enemies
float homingStrength = 0.1f; // How strongly the projectile turns towards its target
NPC target = null; //default value for targeted npc
float closestDistance = detectionRange; //initializes to the max range, and may decrease if any enemies are closer
for (int i = 0; i < Main.maxNPCs; i++)
{
NPC npc = Main.npc;
if (npc.CanBeChasedBy(Projectile, false)) // Check if the NPC is a valid target
{
float distance = Vector2.Distance(npc.Center, Projectile.Center);
if (distance < closestDistance)
{
closestDistance = distance;
target = npc;
}
}
}
//if theres no targets in range at time of checking, allows kill animation
if (target == null && timeAlive <= 60)
{
fadeAndKillFlag = true;
}
//homes on target if in range
if (target != null)
{
// Calculate direction to target
Vector2 direction = target.Center - Projectile.Center;
direction.Normalize();
direction *= maxVelocity; // Max speed the projectile can reach
maxVelocity += 0.5f;
// Smoothly adjust velocity
Projectile.velocity = Vector2.Lerp(Projectile.velocity, direction, homingStrength);
}
// Adjust rotation to match velocity direction
Projectile.rotation = Projectile.velocity.ToRotation() + MathHelper.PiOver4;
}
//homing kill animation
if (fadeAndKillFlag || Projectile.timeLeft < 20)
{
if (!adjustTimeLeftFlag)
{
Projectile.timeLeft = 15;
adjustTimeLeftFlag = true;
}
Projectile.velocity *= 0.55f;
Projectile.alpha += 22;
Projectile.scale -= 0.05f;
}
}
//allows kill animation, and spawns purple particles from the nights edge
public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
{
fadeAndKillFlag = true;
//no clue how this works, I copied it from ExampleSwingingEnergySwordProjectile, and changed the particle type
ParticleOrchestrator.RequestParticleSpawn(clientOnly: false, ParticleOrchestraType.NightsEdge,
new ParticleOrchestraSettings { PositionInWorld = Main.rand.NextVector2FromRectangle(target.Hitbox) },
Projectile.owner); //seems like it produces a particle at a random position inside of the target's hitbox
hit.HitDirection = (Main.player[Projectile.owner].Center.X < target.Center.X) ? 1 : (-1); //clueless
}
}
}
If I find the solution I'll post it here.