tModLoader [Tutorial] TModLoader: Projectile Help

aldgjjdbg

Terrarian
So, recently I've been trying to spice up my mod with more unique projectile weapons, and I found some nice tricks with existing tutorials, which I will compile here. I also decompiled Terraria because my homing AI wasn't cutting it and I needed Chloro bullet AI as a base, so I'll share the AI I derived from vanilla projectiles too. Read ahead for some tips on how to make a decent projectile in Terraria.
kdcROYP.png

1. Basic Projectiles and Useful Vanilla Things
Firstly, you may be wondering how to actually make a projectile. Firstly, here's a skeleton for a projectile. (Each Code section is 1 .cs file).
Code:
using System; //what sources the code uses, these sources allow for calling of terraria functions, existing system functions and microsoft vector functions (probably more)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Mod.Projectiles //where it's stored, replace Mod with the name of your mod. This file is stored in the folder \Mod Sources\(mod name, folder can't have spaces)\Projectiles.
{
    public class ProjectileSkeleton : ModProjectile //the class of the projectile. Change ProjectileSkeleton to whatever you want the projectile name to be.
    {
        public override void SetDefaults()
        {
        }
    }
}
Adding code in, here's what a basic bullet would look like.
Code:
using System; //what sources the code uses, these sources allow for calling of terraria functions, existing system functions and microsoft vector functions (probably more)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Mod.Projectiles //where it's stored, replace Mod with the name of your mod. This file is stored in the folder \Mod Sources\(mod name, folder can't have spaces)\Projectiles.
{
    public class Bullet : ModProjectile //the class of the projectile
    {
        public override void SetDefaults()
        {
            projectile.width = 2; //sprite is 2 pixels wide
            projectile.height = 20; //sprite is 20 pixels tall
            projectile.aiStyle = 0; //projectile moves in a straight line
            projectile.friendly  = true; //player projectile
            projectile.ranged = true; //ranged projectile
            projectile.timeLeft = 600; //lasts for 600 frames/ticks. Terraria runs at 60FPS, so it lasts 10 seconds.
            aiType = ProjectileID.Bullet; //This clones the exact AI of the vanilla projectile Bullet.
        }
    }
}
So now we have a projectile that, when spawned, moves straight forward until it hits something or has been active for 600 frames. However, you may notice that when it hits a tile, it simply despawns with no noise or particle effects. This is because AI does not do this; the hook Kill does this. We can fix that by adding the following code:
Code:
        public override void Kill(int timeLeft)
        {
            Collision.HitTiles(projectile.position, projectile.velocity, projectile.width, projectile.height); //makes dust based on tile
            Main.PlaySound(SoundID.Item10, projectile.position); //plays impact sound
        }
Making our new projectile code look like this:
Code:
using System; //what sources the code uses, these sources allow for calling of terraria functions, existing system functions and microsoft vector functions (probably more)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Mod.Projectiles //where it's stored. Replace Mod with the name of your mod. This file is stored in the folder \Mod Sources\(mod name, folder can't have spaces)\Projectiles.
{
    public class Bullet : ModProjectile //the class of the projectile
    {
        public override void SetDefaults()
        {
            projectile.width = 2; //sprite is 2 pixels wide
            projectile.height = 20; //sprite is 20 pixels tall
            projectile.aiStyle = 0; //projectile moves in a straight line
            projectile.friendly  = true; //player projectile
            projectile.ranged = true; //ranged projectile
            projectile.timeLeft = 600; //lasts for 600 frames/ticks. Terraria runs at 60FPS, so it lasts 10 seconds.
            aiType = ProjectileID.Bullet; //This clones the exact AI of the vanilla projectile Bullet.
        }
        public override void Kill(int timeLeft)
        {
            Collision.HitTiles(projectile.position, projectile.velocity, projectile.width, projectile.height);
            Main.PlaySound(SoundID.Item10, projectile.position);
        }
    }
}
You can also make it trail particles behind it like so, by spawning them every tick in the AI() hook:
Code:
using System; //what sources the code uses, these sources allow for calling of terraria functions, existing system functions and microsoft vector functions (probably more)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Mod.Projectiles //where it's stored. Replace Mod with the name of your mod. This file is stored in the folder \Mod Sources\(mod name, folder can't have spaces)\Projectiles.
{
    public class Bullet : ModProjectile //the class of the projectile
    {
        public override void SetDefaults()
        {
            projectile.width = 2; //sprite is 2 pixels wide
            projectile.height = 20; //sprite is 20 pixels tall
            projectile.aiStyle = 0; //projectile moves in a straight line
            projectile.friendly  = true; //player projectile
            projectile.ranged = true; //ranged projectile
            projectile.timeLeft = 600; //lasts for 600 frames/ticks. Terraria runs at 60FPS, so it lasts 10 seconds.
            aiType = ProjectileID.Bullet; //This clones the exact AI of the vanilla projectile Bullet.
        }
        public override void AI()
        {
            Dust.NewDust(projectile.position + projectile.velocity, projectile.width, projectile.height, 15, projectile.velocity.X * -0.5f, projectile.velocity.Y * -0.5f);   //spawns dust behind it, this is a spectral light blue dust
        }
        public override void Kill(int timeLeft)
        {
            Collision.HitTiles(projectile.position, projectile.velocity, projectile.width, projectile.height);
            Main.PlaySound(SoundID.Item10, projectile.position);
        }
    }
}
And, to top off what I would consider basic, adding a debuff to the bullet. This is done with the hook OnHitNPC.
Code:
using System; //what sources the code uses, these sources allow for calling of terraria functions, existing system functions and microsoft vector functions (probably more)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Mod.Projectiles //where it's stored. Replace Mod with the name of your mod. This file is stored in the folder \Mod Sources\(mod name, folder can't have spaces)\Projectiles.
{
    public class EtherealBullet : ModProjectile //the class of the projectile. Change EtherealBullet to the ID of your projectile. The ID has to match the name of the sprite for that item in your folder and can have no spaces.
    {
        public override void SetDefaults()
        {
            projectile.width = 2; //sprite is 2 pixels wide
            projectile.height = 20; //sprite is 20 pixels tall
            projectile.aiStyle = 0; //projectile moves in a straight line
            projectile.friendly  = true; //player projectile
            projectile.ranged = true; //ranged projectile
            projectile.timeLeft = 600; //lasts for 600 frames/ticks. Terraria runs at 60FPS, so it lasts 10 seconds.
            aiType = ProjectileID.Bullet; //This clones the exact AI of the vanilla projectile Bullet.
        }
        public override void AI()
        {
            Dust.NewDust(projectile.position + projectile.velocity, projectile.width, projectile.height, 15, projectile.velocity.X * -0.5f, projectile.velocity.Y * -0.5f);   //spawns dust behind it, this is a spectral light blue dust. 15 is the dust, change that to what you want.
        }
        public override void Kill(int timeLeft)
        {
            Collision.HitTiles(projectile.position, projectile.velocity, projectile.width, projectile.height);
            Main.PlaySound(SoundID.Item10, projectile.position);
        }

        public override void OnHitNPC(NPC target, int damage, float knockback, bool crit)
        {
            target.AddBuff(mod.BuffType("EtherealFlames"), 180, false); //The debuff inflicted is the modded debuff Ethereal Flames. 180 is the duration in frames: Terraria runs at 60 FPS, so that's 3 seconds (180/60=3). To change the modded debuff, change EtherealFlames to whatever the buff is called; to add a vanilla debuff, change mod.BuffType("EtherealFlames") to a number based on the terraria buff IDs. Some useful ones are 20 for poison, 24 for On Fire!, 39 for Cursed Flames, 69 for Ichor, and 70 for Venom.
        }

    }
}
(In case you're wondering, to make a weapon shoot a projectile, add the code item.shoot = ___. This can be a number for a vanilla projectile, ProjectileID.___ for a vanilla projectile, or mod.ProjectileType("") for a modded. For the latter, put the ID in the quotation marks. For a gun, you must use 10 as your shoot; for a bow, use 3.)

Speaking of shooting vanilla projectiles, here are some useful vanilla projectiles.
668: Flameburst Fireball
This is an exploding fireball that inflicts On Fire! and is very useful for a fire weapon.
521: Crystal Serpent
A pink projectile great for magic weapons that explodes into more for massive damage. May be a tad hard to balance.

Finally, here's a basic magic weapon that shoots a single projectile.
Code:
using System;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Terrxplosion.Items.Weapons.Magic // under Mod Sources\Terrxplosion\Items\Weapons\Magic
{
    public class StarryWand : ModItem
    {
        public override void SetStaticDefaults() //used to set the name (defaults to the ID but with spaces before every capital letter except the first letter of the ID), tooltip and more.
        {
            Tooltip.SetDefault(""); //tooltip
            Item.staff[item.type] = true; //this code makes a gun-style item into a staff-style item
        }
        public override void SetDefaults()
        {
            item.damage = 5; //deals 5 damage
            item.magic = true; //deals magic damage
            item.noMelee = true; //the item sprite itself does no damage
            item.width = 26; //26 px wide
            item.height = 26; //26 px tall
            item.useTime = 18; //shoots a projectile every 18 frames
            item.useAnimation = 18; //the item itself is visible for 18 frames (no frame where it's invisible if you use auto-fire) and has a displayed use time of 18
            item.useStyle = 5; //animates like a gun (staff because of what we did in SetStaticDefaults)
            item.knockBack = 5f; //deals 5 units of knockback
            item.value = Item.buyPrice(0, 0, 5, 0); //if you buy it in a shop, it would cost 5 silver, but it would be 1/5 if you sold it, so it has a sell price of 1 silver
            item.rare = 0; //white rarity
            item.autoReuse = true; //auto-fires
            item.shoot = mod.ProjectileType ("Star"); //shoots a modded projectile, in this case Star
            item.shootSpeed = 12; //projectile has a velocity of 12
            item.mana = 2; //2 mana to use
        }
        public override void AddRecipes()
        {
            ModRecipe recipe = new ModRecipe(mod);
            recipe.AddIngredient(ItemID.Wood, 10); //crafted with 10 wood
            recipe.AddIngredient(75); //crafted with Item 75, Fallen Star
            recipe.AddTile(18); //crafted at Tile 18, Workbench
            recipe.SetResult(this);
            recipe.AddRecipe();
        }
    }
}
kdcROYP.png

Section 2: Advanced Projectile AIs
Add these to your AI hook in your projectile to make them do what it says on the label.
Code:
                float num132 = (float)Math.Sqrt((double)(projectile.velocity.X * projectile.velocity.X + projectile.velocity.Y * projectile.velocity.Y));
                float num133 = projectile.localAI[0];
                if (num133 == 0f)
                {
                    projectile.localAI[0] = num132;
                    num133 = num132;
                }
                float num134 = projectile.position.X;
                float num135 = projectile.position.Y;
                float num136 = 300f;
                bool flag3 = false;
                int num137 = 0;
                if (projectile.ai[1] == 0f)
                {
                    for (int num138 = 0; num138 < 200; num138++)
                    {
                        if (Main.npc[num138].CanBeChasedBy(this, false) && (projectile.ai[1] == 0f || projectile.ai[1] == (float)(num138 + 1)))
                        {
                            float num139 = Main.npc[num138].position.X + (float)(Main.npc[num138].width / 2);
                            float num140 = Main.npc[num138].position.Y + (float)(Main.npc[num138].height / 2);
                            float num141 = Math.Abs(projectile.position.X + (float)(projectile.width / 2) - num139) + Math.Abs(projectile.position.Y + (float)(projectile.height / 2) - num140);
                            if (num141 < num136 && Collision.CanHit(new Vector2(projectile.position.X + (float)(projectile.width / 2), projectile.position.Y + (float)(projectile.height / 2)), 1, 1, Main.npc[num138].position, Main.npc[num138].width, Main.npc[num138].height))
                            {
                                num136 = num141;
                                num134 = num139;
                                num135 = num140;
                                flag3 = true;
                                num137 = num138;
                            }
                        }
                    }
                    if (flag3)
                    {
                        projectile.ai[1] = (float)(num137 + 1);
                    }
                    flag3 = false;
                }
                if (projectile.ai[1] > 0f)
                {
                    int num142 = (int)(projectile.ai[1] - 1f);
                    if (Main.npc[num142].active && Main.npc[num142].CanBeChasedBy(this, true) && !Main.npc[num142].dontTakeDamage)
                    {
                        float num143 = Main.npc[num142].position.X + (float)(Main.npc[num142].width / 2);
                        float num144 = Main.npc[num142].position.Y + (float)(Main.npc[num142].height / 2);
                        if (Math.Abs(projectile.position.X + (float)(projectile.width / 2) - num143) + Math.Abs(projectile.position.Y + (float)(projectile.height / 2) - num144) < 1000f)
                        {
                            flag3 = true;
                            num134 = Main.npc[num142].position.X + (float)(Main.npc[num142].width / 2);
                            num135 = Main.npc[num142].position.Y + (float)(Main.npc[num142].height / 2);
                        }
                    }
                    else
                    {
                        projectile.ai[1] = 0f;
                    }
                }
                if (!projectile.friendly)
                {
                    flag3 = false;
                }
                if (flag3)
                {
                    float num145 = num133;
                    Vector2 vector10 = new Vector2(projectile.position.X + (float)projectile.width * 0.5f, projectile.position.Y + (float)projectile.height * 0.5f);
                    float num146 = num134 - vector10.X;
                    float num147 = num135 - vector10.Y;
                    float num148 = (float)Math.Sqrt((double)(num146 * num146 + num147 * num147));
                    num148 = num145 / num148;
                    num146 *= num148;
                    num147 *= num148;
                    int num149 = 8;
                    projectile.velocity.X = (projectile.velocity.X * (float)(num149 - 1) + num146) / (float)num149;
                    projectile.velocity.Y = (projectile.velocity.Y * (float)(num149 - 1) + num147) / (float)num149;
                }
                }
Credit to @Sin Costan for this AI.
Code:
for(int i = 0; i < 200; i++)
            {
                NPC target = Main.npc[i];
                float shootToX = target.position.X + (float)target.width * 0.5f - projectile.Center.X;
                float shootToY = target.position.Y - projectile.Center.Y;
                float distance = (float)System.Math.Sqrt((double)(shootToX * shootToX + shootToY * shootToY));
                if(distance < 480f && !target.friendly && target.active)
                {
                    if(projectile.ai[0] > 10f)
                    {
                        distance = 3f / distance;
                        shootToX *= distance * 5;
                        shootToY *= distance * 5;
                        int proj = Projectile.NewProjectile(projectile.Center.X, projectile.Center.Y, shootToX, shootToY, mod.ProjectileType("Laser"), projectile.damage, projectile.knockBack, Main.myPlayer, 0f, 0f); //mod.ProjectileType("Laser") is the projectile it shoots, change it to what you like
                            Main.projectile[proj].timeLeft = 300;
                            Main.projectile[proj].netUpdate = true;
                            projectile.netUpdate = true;
                        Main.PlaySound(2, (int)projectile.position.X, (int)projectile.position.Y, 12);
                        projectile.ai[0] = -50f;
                    }
                }
                }
            projectile.ai[0] += 1f;
Code:
            projectile.localAI[0] += 1f;
            if (projectile.localAI[0] > 3f)
            {
                projectile.alpha = 0;
            }
            if (projectile.ai[0] >= 20f)
            {
                projectile.ai[0] = 20f;
                projectile.velocity.Y += 0.075f;
            }
            if (Main.myPlayer == projectile.owner)
            {
                if (projectile.ai[1] >= 0f)
                {
                    projectile.penetrate = -1;
                }
                else if (projectile.penetrate < 0)
                {
                    projectile.penetrate = 1;
                }
                if (projectile.ai[1] >= 0f)
                {
                    projectile.ai[1] += 1f;
                }
                if (projectile.ai[1] > (float)Main.rand.Next(5, 30))
                {
                    projectile.ai[1] = -1000f;
                    float scaleFactor4 = projectile.velocity.Length();
                    Vector2 velocity = projectile.velocity;
                    velocity.Normalize();
                    int num161 = Main.rand.Next(2, 4);
                    if (Main.rand.Next(4) == 0)
                    {
                        num161++;
                    }
                    for (int num162 = 0; num162 < num161; num162++)
                    {
                        Vector2 vector12 = new Vector2((float)Main.rand.Next(-100, 101), (float)Main.rand.Next(-100, 101));
                        vector12.Normalize();
                        vector12 += velocity * 2f;
                        vector12.Normalize();
                        vector12 *= scaleFactor4;
                        Projectile.NewProjectile(projectile.Center.X, projectile.Center.Y, vector12.X, vector12.Y, projectile.type, projectile.damage, projectile.knockBack, projectile.owner, 0f, -1000f);
                    }
                }
            }
kdcROYP.png

Section 3: Weapons that Shoot Projectiles
So now you need your item to shoot a projectile. As demonstrated in Section 1, you can easily make a magic weapon like a Gem Staff that shoots a single projectile. What about a ranged weapon? To make a weapon ranged, do the following:
  • Change item.magic = true; to item.ranged = true; //so it will do ranged damage
  • Remove item.mana = x;
  • Add item.useAmmo = x; //replace x with AmmoID.whatever you want from here. If you want it to not consume ammo like the Toxikarp, ignore this. Common ammo types include AmmoID.Arrow, AmmoID.Bullet, and AmmoID.Rocket
  • Set item.shoot to:
  • 3 for arrow
  • 10 for bullet
  • 134 for rocket
  • Projectile ID of the projectile you want for anything else
However, suppose you want:
- A magic weapon that rains projectiles from the sky
- A magic weapon that fires projectiles in a spread
- A magic weapon that fires more than one projectile
- A magic weapon that shoots a double, triple, quadruple, etc. burst of projectiles in a straight line while only consuming 1 burst of mana
- Any ranged weapon with the above qualities
then I can help!
Firstly, add following the hook to your weapon:
Code:
public override bool Shoot(Player player, ref Microsoft.Xna.Framework.Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)

        {
        }
All of the following blocks of code will be put in this hook, between the {} (unless it is stated otherwise).
Code:
float numberProjectiles = 3; // 3 shots
            float rotation = MathHelper.ToRadians(45);//Shoots them in a 45 degree radius. (This is technically 90 degrees because it's 45 degrees up from your cursor and 45 degrees down)
            position += Vector2.Normalize(new Vector2(speedX, speedY)) * 45f; //45 should equal whatever number you had on the previous line
            for (int i = 0; i < numberProjectiles; i++)
            {
                Vector2 perturbedSpeed = new Vector2(speedX, speedY).RotatedBy(MathHelper.Lerp(-rotation, rotation, i / (numberProjectiles - 1))) * .2f; // Vector for spread. Watch out for dividing by 0 if there is only 1 projectile.
                Projectile.NewProjectile(position.X, position.Y, perturbedSpeed.X, perturbedSpeed.Y, type, damage, knockBack, player.whoAmI); //Creates a new projectile with our new vector for spread.
            }
            return false; //makes sure it doesn't shoot the projectile again after this
Code:
for (int i = 0; i < 3; i++) //replace 3 with however many projectiles you like

            {
                Vector2 perturbedSpeed = new Vector2(speedX, speedY).RotatedByRandom(MathHelper.ToRadians(12)); //12 is the spread in degrees, although like with Set Spread it's technically a 24 degree spread due to the fact that it's randomly between 12 degrees above and 12 degrees below your cursor.
                Projectile.NewProjectile(position.X, position.Y, perturbedSpeed.X, perturbedSpeed.Y, type, damage, knockBack, player.whoAmI); //create the projectile
            }
            return false;
Code:
int numberProjectiles = 6; // shoots 6 projectiles
            for (int index = 0; index < numberProjectiles; ++index)
            {
                Vector2 vector2_1 = new Vector2((float)((double)player.position.X + (double)player.width * 0.5 + (double)(Main.rand.Next(201) * -player.direction) + ((double)Main.mouseX + (double)Main.screenPosition.X - (double)player.position.X)), (float)((double)player.position.Y + (double)player.height * 0.5 - 600.0));   //this defines the projectile width, direction and position
                vector2_1.X = (float)(((double)vector2_1.X + (double)player.Center.X) / 2.0) + (float)Main.rand.Next(-200, 201);
                vector2_1.Y -= (float)(100 * index);
                float num12 = (float)Main.mouseX + Main.screenPosition.X - vector2_1.X;
                float num13 = (float)Main.mouseY + Main.screenPosition.Y - vector2_1.Y;
                if ((double)num13 < 0.0) num13 *= -1f;
                if ((double)num13 < 20.0) num13 = 20f;
                float num14 = (float)Math.Sqrt((double)num12 * (double)num12 + (double)num13 * (double)num13);
                float num15 = item.shootSpeed / num14;
                float num16 = num12 * num15;
                float num17 = num13 * num15;
                float SpeedX = num16 + (float)Main.rand.Next(-40, 41) * 0.02f; //change the Main.rand.Next here to, for example, (-10, 11) to reduce the spread. Change this to 0 to remove it altogether
                float SpeedY = num17 + (float)Main.rand.Next(-40, 41) * 0.02f;
                Projectile.NewProjectile(vector2_1.X, vector2_1.Y, SpeedX, SpeedY, type, damage, knockBack, Main.myPlayer, 0.0f, (float)Main.rand.Next(5));
            }
            return false;
kdcROYP.png

Section 4: Custom Ammo
Making custom ammo is surprisingly easy, and you can even make your own ammo types!
There are 3 sub-sections:
Using a Modded Item as a Vanilla Ammo Type
To make an ammo, all you really need is item.ammo. However, to make it actually function, you need this code in SetDefaults:
Code:
        public override void SetDefaults()
        {
            item.damage = 1; //how much additional damage your ammo does
            item.width = 12;
            item.height = 12;
            item.maxStack = 999; //how much fit in one inventory slot
            item.consumable = true; //makes it so the bullet is used on shooting. Delete this if you want an endless ammo pouch
            item.knockBack = 1.5f; //how much additional knockback your bullet does.
            item.value = 10; //if value is just a number, it's the sell price in copper coins. This is worth 10 copper. Every 100 means it sells for a silver, every 10000 means gold, and every 1000000 means platinum
            item.rare = 0;
            item.shoot = mod.ProjectileType("WoodenBullet"); //IMPORTANT! Make sure you have a projectile for your ammo and this shoots it
            item.shootSpeed = 10f; //how much additional velocity it applies to the projectile
            item.ammo = AmmoID.Bullet; //IMPORTANT! This makes the item ammo of the according type. Common ammo types include AmmoID.Arrow, AmmoID.Bullet, and AmmoID.Rocket
        }

Using a Modded Item as a Modded Ammo Type
First, set your weapon to use the ammo ModdedAmmo or CrossbowBolt or whatever your ammo type is called (i.e. set item.useAmmo to the ID). Next, set item.ammo in your ammo to the same thing. That's it!
Using a Vanilla Item as a Modded Ammo Type
This is a little bit trickier: how do we mod an existing item? The answer is simple: GlobalItem! GlobalItem is a class that overrides every item in the game. You may be confused as to how overriding every item in the game helps, but if you use if(item.type == id), you can make it only apply to items with that ID! Here's GlobalItem, put this in a new cs file in your items folder:
Code:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria.ModLoader.IO;
using Terraria.Utilities;

namespace Terraria.ModLoader
{
    public class ModGlobalItem : GlobalItem
    {
        public override bool InstancePerEntity
        {
            get
            {
                return true;
            }
        }
        
        public override void SetDefaults(Item item)
        {
        }
    }
}
Now you can do whatever you want to vanilla items! If you want to make the Reaver Shark have less pickaxe power, you can put this in SetDefaults:
Code:
            if (item.type == 2341) //reaver sharks item ID, a list is here: https://terraria.wiki.gg/Item_IDs
            {
                item.pick = 58;
            }
Replace the id with the item you want to modify, and item.pick to whatever value you want to modify. This can do everything from making copper shortswords one-shot bosses to making musketballs shoot explosive fireballs instead of bullets!
With this, we can make vanilla items do whatever we want, including making them ammo with item.ammo!

kdcROYP.png
If you need anything else that relates to a vanilla projectile's AI, ask me about it. I have the decompiled source code for Terraria, so I can access the exact AI for you. I can also help with most issues of any kind that don't relate to bosses.

Here's a .cs file about creative ways to make a gun shoot things: blushiemagic/tModLoader
Scroll down to see how to get a clockwork assault rifle-type effect, shotgun effect, reduced ammo consumption effect, set spread effect, and chain gun effect.

Code:
        public override void OnHitNPC(NPC target, int damage, float knockback, bool crit)
        {
            Player p = Main.player[projectile.owner];
            int healingAmount = damage/30; //decrease the value 30 to increase heal, increase value to decrease. Or you can just replace damage/x with a set value to heal, instead of making it based on damage.
            p.statLife +=healingAmount;
            p.HealEffect(healingAmount, true);
        }
 
Last edited:
I like what you did here, but I need help with an npc to shoot some unique projectiles. I know you are done, but could you point me to a guide for npc projectiles?
I asked Sin Costan this some time ago, this was his reply:
You can use the code "Having a projectile shoot at something" in the next post after the opening post and replace all the "projectile" to "npc" and the "npc" to "player". After that, change the projectile ID parameter in Projectile.NewProjectile to the projectile you want.
So use the code for a projectile that shoots at enemies that I posted and use it on your NPC, except change the "projectile" to "npc" and the "npc" to "player".
EDIT: Tried it, for some reason it says the arguments in Projectile.NewProjectile(npc.Center.X, npc.Center.Y, shootToX, shootToY, 81, npc.damage, 1f, this, 0f, 0f) are invalid.
 
Last edited:
I asked Sin Costan this some time ago, this was his reply:

So use the code for a projectile that shoots at enemies that I posted and use it on your NPC, except change the "projectile" to "npc" and the "npc" to "player".
EDIT: Tried it, for some reason it says the arguments in Projectile.NewProjectile(npc.Center.X, npc.Center.Y, shootToX, shootToY, 81, npc.damage, 1f, this, 0f, 0f) are invalid.

Very cool, and helpful! thanks a bunch, I'll use this guide a lot.
 
i cant shoot with my staff......

is it because it's a melee staff?

heres the thing
You aren't using Terraria. This means that you can't access the files item.cs, player.cs, projectile.cs, etc., which are necessary for nearly everything in modding.
Try:
Code:
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace RianMod.Items
{
    public class taeee : ModItem
    {
        public override void SetStaticDefaults()
        {
            DisplayName.SetDefault("Developer's Staff");
            Tooltip.SetDefault("DEVELOPER ITEM.");
             Item.staff[item.type] = true;
        }
        public override void SetDefaults()
        {
            item.damage = 220;
            item.melee = true;
            item.width = 40;
            item.height = 40;
            item.useTime = 20;
            item.useAnimation = 20;
            item.useStyle = 1;
            item.knockBack = 6;
            item.value = 10000;
            item.rare = -12;
            item.UseSound = SoundID.Item1;
            item.autoReuse = true;
            item.shoot = mod.ProjectileType ("Bullet");
            item.shootSpeed = 120;
            item.mana = 1;
        }

        public override void AddRecipes()
        {
            ModRecipe recipe = new ModRecipe(mod);
            recipe.AddIngredient(ItemID.Sapphire, 50);
            recipe.AddIngredient(ItemID.BrokenHeroSword, 1);
            recipe.AddIngredient(ItemID.Ruby, 50);
            recipe.AddTile(TileID.MythrilAnvil);
            recipe.SetResult(this);
            recipe.AddRecipe();
        }
    }
}
Also, when you are giving code because of an error, you normally hit Insert -> Code and then paste your code. Then, outside of the code, post your error.
Edit: Also, always use the following:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
 
using Terraria.ID;
using Terraria.ModLoader;
using Microsoft.Xna.Framework;
using System;

namespace ExampleMod.Projectiles
{
public class EmberScythe : ModProjectile
{
public override void SetStaticDefaults()
{
DisplayName.SetDefault("EmberScythe");
}

public override void SetDefaults()
{
projectile.width = 48;
projectile.height = 42;
projectile.aiStyle = 18;
projectile.friendly = true;
projectile.melee = true;
projectile.penetrate = -1;
projectile.tileCollide = false;
aiType = ProjectileID.DeathSickle;
projectile.timeLeft = 100;
}
}
}

Thats my Code for a custom Death Scythe, It will shoot but it will never move.
 
using Terraria.ID;
using Terraria.ModLoader;
using Microsoft.Xna.Framework;
using System;

namespace ExampleMod.Projectiles
{
public class EmberScythe : ModProjectile
{
public override void SetStaticDefaults()
{
DisplayName.SetDefault("EmberScythe");
}

public override void SetDefaults()
{
projectile.width = 48;
projectile.height = 42;
projectile.aiStyle = 18;
projectile.friendly = true;
projectile.melee = true;
projectile.penetrate = -1;
projectile.tileCollide = false;
aiType = ProjectileID.DeathSickle;
projectile.timeLeft = 100;
}
}
}

Thats my Code for a custom Death Scythe, It will shoot but it will never move.
Pretty sure you need
Code:
using Terraria;
to make anything in TModLoader. Otherwise, it will not load things such as every line of code in SetDefaults except aiType.
 
Hi, it seems that you are nice with throwing n' projectile code. I am a mess with any code.

Im trying to mod throwing weapons, and, now, im trying to make a item throw variations of the same item (color variation)
I have projectile.cs's for each color variation, but I dont understant how to make the item circle radomly between the colors/projectiles.

I tried this:
Code:
using knivesplus.Projectiles;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace knivesplus.Items.Weapons
{
    public class kunai : ModItem
    {
        public override void SetStaticDefaults()
        {
            DisplayName.SetDefault("kunai");
            Tooltip.SetDefault("Iron Kunai");
        }
        public override void SetDefaults()
        {
            item.shootSpeed = 10f;
            item.damage = 10;
            item.knockBack = 5f;
            item.useStyle = 1;
            item.useAnimation = 25;
            item.useTime = 25;
            item.width = 30;
            item.height = 30;
            item.maxStack = 999;
            item.rare = 5;

            item.consumable = true;
            item.noUseGraphic = true;
            item.noMelee = true;
            item.autoReuse = true;
            item.thrown = true;

            item.UseSound = SoundID.Item1;
            item.value = Item.sellPrice(copper: 2);
            item.shoot = mod.ProjectileType<kunaia>();
            item.shoot = mod.ProjectileType<kunaib>();
            item.shoot = mod.ProjectileType<kunaic>();
            item.shoot = mod.ProjectileType<kunaid>();
        }

        public override void AddRecipes()
        {
            ModRecipe recipe = new ModRecipe(mod);
            recipe.AddIngredient(ItemID.DirtBlock, 1);
            recipe.AddTile(TileID.WorkBenches);
            recipe.SetResult(this, 30);
            recipe.AddRecipe();
        }
    }
}

and other variables like :

Code:
            item.shoot = mod.ProjectileType("kunaia");
                                        mod.ProjectileType("kunaib");
                                        mod.ProjectileType("kunaic");
                                        mod.ProjectileType("kunaid");
Code:
            item.shoot = mod.ProjectileType("kunaia"); mod.ProjectileType("kunaib"); mod.ProjectileType("kunaic");mod.ProjectileType("kunaid");

But my item only throw the first , or the final kunai.
Can you help me?
 
Hi, it seems that you are nice with throwing n' projectile code. I am a mess with any code.

Im trying to mod throwing weapons, and, now, im trying to make a item throw variations of the same item (color variation)
I have projectile.cs's for each color variation, but I dont understant how to make the item circle radomly between the colors/projectiles.

I tried this:
Code:
using knivesplus.Projectiles;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace knivesplus.Items.Weapons
{
    public class kunai : ModItem
    {
        public override void SetStaticDefaults()
        {
            DisplayName.SetDefault("kunai");
            Tooltip.SetDefault("Iron Kunai");
        }
        public override void SetDefaults()
        {
            item.shootSpeed = 10f;
            item.damage = 10;
            item.knockBack = 5f;
            item.useStyle = 1;
            item.useAnimation = 25;
            item.useTime = 25;
            item.width = 30;
            item.height = 30;
            item.maxStack = 999;
            item.rare = 5;

            item.consumable = true;
            item.noUseGraphic = true;
            item.noMelee = true;
            item.autoReuse = true;
            item.thrown = true;

            item.UseSound = SoundID.Item1;
            item.value = Item.sellPrice(copper: 2);
            item.shoot = mod.ProjectileType<kunaia>();
            item.shoot = mod.ProjectileType<kunaib>();
            item.shoot = mod.ProjectileType<kunaic>();
            item.shoot = mod.ProjectileType<kunaid>();
        }

        public override void AddRecipes()
        {
            ModRecipe recipe = new ModRecipe(mod);
            recipe.AddIngredient(ItemID.DirtBlock, 1);
            recipe.AddTile(TileID.WorkBenches);
            recipe.SetResult(this, 30);
            recipe.AddRecipe();
        }
    }
}

and other variables like :

Code:
            item.shoot = mod.ProjectileType("kunaia");
                                        mod.ProjectileType("kunaib");
                                        mod.ProjectileType("kunaic");
                                        mod.ProjectileType("kunaid");
Code:
            item.shoot = mod.ProjectileType("kunaia"); mod.ProjectileType("kunaib"); mod.ProjectileType("kunaic");mod.ProjectileType("kunaid");

But my item only throw the first , or the final kunai.
Can you help me?
Add this in the kunai class:
Code:
        public override bool Shoot(Player player, ref Microsoft.Xna.Framework.Vector2 position, ref float speedX, ref float speedY, ref int type, ref int damage, ref float knockBack)
        {
            int projSpawn = Main.rand.Next(1, 4); //picks a number between 1 and 4
            if(projSpawn = 1) //if it picked 1
            {
                Projectile.NewProjectile(position.X, position.Y, speedX, speedY, mod.ProjectileType("kunaia"), damage, knockBack, item.owner); //spawn kunai a
            } else if (projSpawn = 2) //if not, what about 2?
            {
                Projectile.NewProjectile(position.X, position.Y, speedX, speedY, mod.ProjectileType("kunaib"), damage, knockBack, item.owner); //spawn kunai b
            } else if (projSpawn = 3) //etc.
            {
                Projectile.NewProjectile(position.X, position.Y, speedX, speedY, mod.ProjectileType("kunaic"), damage, knockBack, item.owner); //etc.
            } else if (projSpawn = 4)
            {
                Projectile.NewProjectile(position.X, position.Y, speedX, speedY, mod.ProjectileType("kunaid"), damage, knockBack, item.owner);
            }
        }
 
I don't understand how to add the AI hook to my projectile... :/


EDIT: This is my projectile cs file:
Code:
using System; //what sources the code uses, these sources allow for calling of terraria functions, existing system functions and microsoft vector functions (probably more)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Mod.Projectiles //where it's stored. Replace Mod with the name of your mod. This file is stored in the folder \Mod Sources\(mod name, folder can't have spaces)\Projectiles.
{
    public class Bullet : ModProjectile //the class of the projectile
    {
        public override void SetDefaults()
        {
            projectile.width = 2; //sprite is 2 pixels wide
            projectile.height = 20; //sprite is 20 pixels tall
            projectile.aiStyle = 0; //projectile moves in a straight line
            projectile.friendly  = true; //player projectile
            projectile.ranged = true; //ranged projectile
            projectile.timeLeft = 600; //lasts for 600 frames/ticks. Terraria runs at 60FPS, so it lasts 10 seconds.
            aiType = ProjectileID.Bullet; //This clones the exact AI of the vanilla projectile Bullet.
        }
        public override void AI()
        {
            Dust.NewDust(projectile.position + projectile.velocity, projectile.width, projectile.height, 15, projectile.velocity.X * -0.5f, projectile.velocity.Y * -0.5f);   //spawns dust behind it, this is a spectral light blue dust
                            float num132 = (float)Math.Sqrt((double)(projectile.velocity.X * projectile.velocity.X + projectile.velocity.Y * projectile.velocity.Y));
                float num133 = projectile.localAI[0];
                if (num133 == 0f)
                {
                    projectile.localAI[0] = num132;
                    num133 = num132;
                }
                float num134 = projectile.position.X;
                float num135 = projectile.position.Y;
                float num136 = 300f;
                bool flag3 = false;
                int num137 = 0;
                if (projectile.ai[1] == 0f)
                {
                    for (int num138 = 0; num138 < 200; num138++)
                    {
                        if (Main.npc[num138].CanBeChasedBy(this, false) && (projectile.ai[1] == 0f || projectile.ai[1] == (float)(num138 + 1)))
                        {
                            float num139 = Main.npc[num138].position.X + (float)(Main.npc[num138].width / 2);
                            float num140 = Main.npc[num138].position.Y + (float)(Main.npc[num138].height / 2);
                            float num141 = Math.Abs(projectile.position.X + (float)(projectile.width / 2) - num139) + Math.Abs(projectile.position.Y + (float)(projectile.height / 2) - num140);
                            if (num141 < num136 && Collision.CanHit(new Vector2(projectile.position.X + (float)(projectile.width / 2), projectile.position.Y + (float)(projectile.height / 2)), 1, 1, Main.npc[num138].position, Main.npc[num138].width, Main.npc[num138].height))
                            {
                                num136 = num141;
                                num134 = num139;
                                num135 = num140;
                                flag3 = true;
                                num137 = num138;
                            }
                        }
                    }
                    if (flag3)
                    {
                        projectile.ai[1] = (float)(num137 + 1);
                    }
                    flag3 = false;
                }
                if (projectile.ai[1] > 0f)
                {
                    int num142 = (int)(projectile.ai[1] - 1f);
                    if (Main.npc[num142].active && Main.npc[num142].CanBeChasedBy(this, true) && !Main.npc[num142].dontTakeDamage)
                    {
                        float num143 = Main.npc[num142].position.X + (float)(Main.npc[num142].width / 2);
                        float num144 = Main.npc[num142].position.Y + (float)(Main.npc[num142].height / 2);
                        if (Math.Abs(projectile.position.X + (float)(projectile.width / 2) - num143) + Math.Abs(projectile.position.Y + (float)(projectile.height / 2) - num144) < 1000f)
                        {
                            flag3 = true;
                            num134 = Main.npc[num142].position.X + (float)(Main.npc[num142].width / 2);
                            num135 = Main.npc[num142].position.Y + (float)(Main.npc[num142].height / 2);
                        }
                    }
                    else
                    {
                        projectile.ai[1] = 0f;
                    }
                }
                if (!projectile.friendly)
                {
                    flag3 = false;
                }
                if (flag3)
                {
                    float num145 = num133;
                    Vector2 vector10 = new Vector2(projectile.position.X + (float)projectile.width * 0.5f, projectile.position.Y + (float)projectile.height * 0.5f);
                    float num146 = num134 - vector10.X;
                    float num147 = num135 - vector10.Y;
                    float num148 = (float)Math.Sqrt((double)(num146 * num146 + num147 * num147));
                    num148 = num145 / num148;
                    num146 *= num148;
                    num147 *= num148;
                    int num149 = 8;
                    projectile.velocity.X = (projectile.velocity.X * (float)(num149 - 1) + num146) / (float)num149;
                    projectile.velocity.Y = (projectile.velocity.Y * (float)(num149 - 1) + num147) / (float)num149;
                }
                }
        }
        public override void Kill(int timeLeft)
        {
            Collision.HitTiles(projectile.position, projectile.velocity, projectile.width, projectile.height);
            Main.PlaySound(SoundID.Item10, projectile.position);
        }
    }
}


And I receive these errors when trying to compile:

c:\Users\Gabi\Documents\My Games\Terraria\ModLoader\Mod Sources\HomingCrystalBullets\Items\Projectiles\HomingCrystalBullet.cs(101,25) : error CS1518: Expected Class, delegate, enum, interface or struct

c:\Users\Gabi\Documents\My Games\Terraria\ModLoader\Mod Sources\HomingCrystalBullets\Items\Projectiles\HomingCrystalBullet.cs(107,1) : error CS1022: Type or namespace definition, or end-of-file expected


What should I do to solve this?
 
Last edited:
I don't understand how to add the AI hook to my projectile... :/


EDIT: This is my projectile cs file:
Code:
using System; //what sources the code uses, these sources allow for calling of terraria functions, existing system functions and microsoft vector functions (probably more)
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace Mod.Projectiles //where it's stored. Replace Mod with the name of your mod. This file is stored in the folder \Mod Sources\(mod name, folder can't have spaces)\Projectiles.
{
    public class Bullet : ModProjectile //the class of the projectile
    {
        public override void SetDefaults()
        {
            projectile.width = 2; //sprite is 2 pixels wide
            projectile.height = 20; //sprite is 20 pixels tall
            projectile.aiStyle = 0; //projectile moves in a straight line
            projectile.friendly  = true; //player projectile
            projectile.ranged = true; //ranged projectile
            projectile.timeLeft = 600; //lasts for 600 frames/ticks. Terraria runs at 60FPS, so it lasts 10 seconds.
            aiType = ProjectileID.Bullet; //This clones the exact AI of the vanilla projectile Bullet.
        }
        public override void AI()
        {
            Dust.NewDust(projectile.position + projectile.velocity, projectile.width, projectile.height, 15, projectile.velocity.X * -0.5f, projectile.velocity.Y * -0.5f);   //spawns dust behind it, this is a spectral light blue dust
                            float num132 = (float)Math.Sqrt((double)(projectile.velocity.X * projectile.velocity.X + projectile.velocity.Y * projectile.velocity.Y));
                float num133 = projectile.localAI[0];
                if (num133 == 0f)
                {
                    projectile.localAI[0] = num132;
                    num133 = num132;
                }
                float num134 = projectile.position.X;
                float num135 = projectile.position.Y;
                float num136 = 300f;
                bool flag3 = false;
                int num137 = 0;
                if (projectile.ai[1] == 0f)
                {
                    for (int num138 = 0; num138 < 200; num138++)
                    {
                        if (Main.npc[num138].CanBeChasedBy(this, false) && (projectile.ai[1] == 0f || projectile.ai[1] == (float)(num138 + 1)))
                        {
                            float num139 = Main.npc[num138].position.X + (float)(Main.npc[num138].width / 2);
                            float num140 = Main.npc[num138].position.Y + (float)(Main.npc[num138].height / 2);
                            float num141 = Math.Abs(projectile.position.X + (float)(projectile.width / 2) - num139) + Math.Abs(projectile.position.Y + (float)(projectile.height / 2) - num140);
                            if (num141 < num136 && Collision.CanHit(new Vector2(projectile.position.X + (float)(projectile.width / 2), projectile.position.Y + (float)(projectile.height / 2)), 1, 1, Main.npc[num138].position, Main.npc[num138].width, Main.npc[num138].height))
                            {
                                num136 = num141;
                                num134 = num139;
                                num135 = num140;
                                flag3 = true;
                                num137 = num138;
                            }
                        }
                    }
                    if (flag3)
                    {
                        projectile.ai[1] = (float)(num137 + 1);
                    }
                    flag3 = false;
                }
                if (projectile.ai[1] > 0f)
                {
                    int num142 = (int)(projectile.ai[1] - 1f);
                    if (Main.npc[num142].active && Main.npc[num142].CanBeChasedBy(this, true) && !Main.npc[num142].dontTakeDamage)
                    {
                        float num143 = Main.npc[num142].position.X + (float)(Main.npc[num142].width / 2);
                        float num144 = Main.npc[num142].position.Y + (float)(Main.npc[num142].height / 2);
                        if (Math.Abs(projectile.position.X + (float)(projectile.width / 2) - num143) + Math.Abs(projectile.position.Y + (float)(projectile.height / 2) - num144) < 1000f)
                        {
                            flag3 = true;
                            num134 = Main.npc[num142].position.X + (float)(Main.npc[num142].width / 2);
                            num135 = Main.npc[num142].position.Y + (float)(Main.npc[num142].height / 2);
                        }
                    }
                    else
                    {
                        projectile.ai[1] = 0f;
                    }
                }
                if (!projectile.friendly)
                {
                    flag3 = false;
                }
                if (flag3)
                {
                    float num145 = num133;
                    Vector2 vector10 = new Vector2(projectile.position.X + (float)projectile.width * 0.5f, projectile.position.Y + (float)projectile.height * 0.5f);
                    float num146 = num134 - vector10.X;
                    float num147 = num135 - vector10.Y;
                    float num148 = (float)Math.Sqrt((double)(num146 * num146 + num147 * num147));
                    num148 = num145 / num148;
                    num146 *= num148;
                    num147 *= num148;
                    int num149 = 8;
                    projectile.velocity.X = (projectile.velocity.X * (float)(num149 - 1) + num146) / (float)num149;
                    projectile.velocity.Y = (projectile.velocity.Y * (float)(num149 - 1) + num147) / (float)num149;
                }
                }
        }
        public override void Kill(int timeLeft)
        {
            Collision.HitTiles(projectile.position, projectile.velocity, projectile.width, projectile.height);
            Main.PlaySound(SoundID.Item10, projectile.position);
        }
    }
}


And I receive these errors when trying to compile:

c:\Users\Gabi\Documents\My Games\Terraria\ModLoader\Mod Sources\HomingCrystalBullets\Items\Projectiles\HomingCrystalBullet.cs(101,25) : error CS1518: Expected Class, delegate, enum, interface or struct

c:\Users\Gabi\Documents\My Games\Terraria\ModLoader\Mod Sources\HomingCrystalBullets\Items\Projectiles\HomingCrystalBullet.cs(107,1) : error CS1022: Type or namespace definition, or end-of-file expected


What should I do to solve this?
There was an extra curly bracket after AI.
Code:
projectile.velocity.X = (projectile.velocity.X * (float)(num149 - 1) + num146) / (float)num149;
                    projectile.velocity.Y = (projectile.velocity.Y * (float)(num149 - 1) + num147) / (float)num149;
                }
                } // useless bracket, ruins the entire code
Try deleting that.
 
Back
Top Bottom