Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example Rockets #3501

Merged
merged 21 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 20 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
5 changes: 5 additions & 0 deletions ExampleMod/Common/GlobalNPCs/ExampleNPCShop.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using ExampleMod.Content.Items;
using ExampleMod.Content.Items.Ammo;
using ExampleMod.Content.Items.Mounts;
using ExampleMod.Content.NPCs;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
Expand Down Expand Up @@ -35,6 +37,9 @@ public override void ModifyShop(NPCShop shop) {
else if (shop.NpcType == NPCID.Stylist) {
shop.Add<ExampleHairDye>();
}
else if (shop.NpcType == NPCID.Cyborg) {
shop.Add<ExampleRocket>(Condition.NpcIsPresent(ModContent.NPCType<ExamplePerson>()));
}

// Example of adding new items with complex conditions in the Merchant shop.
// Style 1 check for application
Expand Down
59 changes: 59 additions & 0 deletions ExampleMod/Content/Items/Ammo/ExampleRocket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
using ExampleMod.Content.Projectiles.Rockets;

namespace ExampleMod.Content.Items.Ammo
{
public class ExampleRocket : ModItem
{
// Rocket Ammo is a little weird and does not work the same as bullets or arrows.
// Rockets I through IV have four versions: normal Rocket, Grenade, Proximity Mine, and Snowman Rocket.
// This example is a clone of Rocket I.

public override void SetStaticDefaults() {
AmmoID.Sets.IsSpecialist[Type] = true; // This item will benefit from the Shroomite Helmet.

// This is where we tell the game which projectile to spawn when using this rocket as ammo with certain launchers.
// This specific rocket ammo is like Rocket I's.
AmmoID.Sets.SpecificLauncherAmmoProjectileMatches[ItemID.RocketLauncher].Add(Type, ModContent.ProjectileType<ExampleRocketProjectile>());
AmmoID.Sets.SpecificLauncherAmmoProjectileMatches[ItemID.GrenadeLauncher].Add(Type, ModContent.ProjectileType<ExampleGrenadeProjectile>());
AmmoID.Sets.SpecificLauncherAmmoProjectileMatches[ItemID.ProximityMineLauncher].Add(Type, ModContent.ProjectileType<ExampleProximityMineProjectile>());
AmmoID.Sets.SpecificLauncherAmmoProjectileMatches[ItemID.SnowmanCannon].Add(Type, ModContent.ProjectileType<ExampleSnowmanRocketProjectile>());
// We also need to say which type of Celebration Mk2 rockets to use.
// The Celebration Mk 2 only has four types of rockets. Change the projectile to match your ammo type.
// Rocket I like == ProjectileID.Celeb2Rocket
// Rocket II like == ProjectileID.Celeb2RocketExplosive
// Rocket III like == ProjectileID.Celeb2RocketLarge
// Rocket IV like == ProjectileID.Celeb2RocketExplosiveLarge
AmmoID.Sets.SpecificLauncherAmmoProjectileMatches[ItemID.Celeb2].Add(Type, ProjectileID.Celeb2Rocket);
// The Celebration and Electrosphere Launcher will always use their own projectiles no matter which rocket you use as ammo.
}

public override void SetDefaults() {
Item.width = 20;
Item.height = 14;
Item.damage = 40;
Item.knockBack = 4f;
Item.consumable = true;
Item.DamageType = DamageClass.Ranged;
Item.maxStack = Item.CommonMaxStack;
Item.value = Item.buyPrice(copper: 50);
Item.ammo = AmmoID.Rocket; // The ammo type is Rocket Ammo

// The projectile that is set in Item.shoot will be the "default" projectile.
// i.e. if a launcher doesn't have this ammo defined in SpecificLauncherAmmoProjectileMatches, it'll use this projectile instead.
Item.shoot = ModContent.ProjectileType<ExampleRocketProjectile>();
}

// Please see Content/ExampleRecipes.cs for a detailed explanation of recipe creation.
public override void AddRecipes() {
CreateRecipe(100)
.AddIngredient(ItemID.RocketI, 100)
.AddIngredient<ExampleItem>()
.AddTile(TileID.Anvils)
.AddCondition(Condition.NpcIsPresent(NPCID.Cyborg))
.Register();
}
}
}
Binary file added ExampleMod/Content/Items/Ammo/ExampleRocket.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ExampleMod/Content/Items/Weapons/ExampleExplosive.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions ExampleMod/Content/Items/Weapons/ExampleRocketLauncher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;

namespace ExampleMod.Content.Items.Weapons
{
// Rocket launchers are special because they typically have ammo-specific variant projectiles.
// ExampleRocketLauncher will inherit the variants specified by the Rocket Launcher weapon
public class ExampleRocketLauncher : ModItem {
public override void SetStaticDefaults() {
// This line lets ExampleRocketLauncher act like a normal RocketLauncher in regard to any variant projectiles
// corresponding to ammo that aren't specifically populated in SpecificLauncherAmmoProjectileMatches below.
AmmoID.Sets.SpecificLauncherAmmoProjectileFallback[Type] = ItemID.RocketLauncher;

// SpecificLauncherAmmoProjectileMatches can be used to provide specific projectiles for specific ammo items.
// This example dictates that when RocketIII ammo is used, this weapon will fire the Meowmere projectile.
// This is purely to show off this capability, typically SpecificLauncherAmmoProjectileFallback is all
// that is needed for an "upgrade". A completely custom rocket launcher would instead specify new and
// unique projectiles for all possible rocket ammo.
AmmoID.Sets.SpecificLauncherAmmoProjectileMatches.Add(Type, new Dictionary<int, int> {
{ ItemID.RocketIII, ProjectileID.Meowmere },
});

// Note that some rocket launchers, like Celebration and Electrosphere Launcher, will always
// use their own projectiles no matter which rocket is used as ammo.
// This type of behavior can be implemented in ModifyShootStats
}

public override void SetDefaults() {
Item.DefaultToRangedWeapon(ProjectileID.RocketI, AmmoID.Rocket, singleShotTime: 30, shotVelocity: 5f, hasAutoReuse: true);
Item.width = 50;
Item.height = 20;
Item.damage = 55;
Item.knockBack = 4f;
Item.UseSound = SoundID.Item11;
Item.value = Item.buyPrice(gold: 40);
Item.rare = ItemRarityID.Yellow;
}

public override Vector2? HoldoutOffset() {
return new Vector2(-8f, 2f); // Moves the position of the weapon in the player's hand.
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 51 additions & 15 deletions ExampleMod/Content/Projectiles/ExampleExplosive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@
using System;
using Terraria;
using Terraria.Audio;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.ModLoader;

namespace ExampleMod.Content.Projectiles
{
// This projectile demonstrates exploding tiles (like a bomb or dynamite), spawning child projectiles, and explosive visual effects.
// TODO: This projectile does not currently damage the owner, or damage other players on the For the worthy secret seed.
public class ExampleExplosive : ModProjectile
{
private const int DefaultWidthHeight = 15;
private const int ExplosionWidthHeight = 250;

private bool IsChild {
get => Projectile.localAI[0] == 1;
set => Projectile.localAI[0] = value.ToInt();
}

public override void SetStaticDefaults() {
ProjectileID.Sets.PlayerHurtDamageIgnoresDifficultyScaling[Type] = true; // Damage dealt to players does not scale with difficulty in vanilla.

// This set handles some things for us already:
// Sets the timeLeft to 3 and the projectile direction when colliding with an NPC or player in PVP (so the explosive can detonate).
// Explosives also bounce off the top of Shimmer, detonate with no blast damage when touching the bottom or sides of Shimmer, and damage other players in For the Worthy worlds.
ProjectileID.Sets.Explosive[Type] = true;
}

public override void SetDefaults() {
// While the sprite is actually bigger than 15x15, we use 15x15 since it lets the projectile clip into tiles as it bounces. It looks better.
Projectile.width = DefaultWidthHeight;
Expand All @@ -40,8 +54,11 @@ public override void ModifyHitNPC(NPC target, ref NPC.HitModifiers modifiers) {

// The projectile is very bouncy, but the spawned children projectiles shouldn't bounce at all.
public override bool OnTileCollide(Vector2 oldVelocity) {
// Die immediately if ai[1] isn't 0 (We set this to 1 for the 5 extra explosives we spawn in Kill)
if (Projectile.ai[1] != 0) {
// Die immediately if localAI[0] is 1 (We set this to 1 for the 5 extra explosives we spawn in Kill)
if (IsChild) {
// These two are so the bomb will damage the player correctly.
Projectile.timeLeft = 0;
Projectile.PrepareBombToBlow();
return true;
}
// OnTileCollide can trigger quite frequently, so using soundDelay helps prevent the sound from overlapping too much.
Expand All @@ -68,15 +85,7 @@ public override bool OnTileCollide(Vector2 oldVelocity) {
public override void AI() {
// The projectile is in the midst of exploding during the last 3 updates.
if (Projectile.owner == Main.myPlayer && Projectile.timeLeft <= 3) {
Projectile.tileCollide = false;
// Set to transparent. This projectile technically lives as transparent for about 3 frames
Projectile.alpha = 255;

// change the hitbox size, centered about the original projectile center. This makes the projectile damage enemies during the explosion.
Projectile.Resize(ExplosionWidthHeight, ExplosionWidthHeight);

Projectile.damage = 250;
Projectile.knockBack = 10f;
Projectile.PrepareBombToBlow(); // Get ready to explode.
}
else {
// Smoke and fuse dust spawn. The position is calculated to spawn the dust directly on the fuse.
Expand Down Expand Up @@ -112,14 +121,41 @@ public override void AI() {
Projectile.rotation += Projectile.velocity.X * 0.1f;
}

/// <summary> Resizes the projectile for the explosion blast radius. </summary>
public override void PrepareBombToBlow() {
Projectile.tileCollide = false; // This is important or the explosion will be in the wrong place if the bomb explodes on slopes.
Projectile.alpha = 255; // Set to transparent. This projectile technically lives as transparent for about 3 frames

// Change the hitbox size, centered about the original projectile center. This makes the projectile damage enemies during the explosion.
Projectile.Resize(ExplosionWidthHeight, ExplosionWidthHeight);

Projectile.damage = 250; // Bomb: 100, Dynamite: 250
Projectile.knockBack = 10f; // Bomb: 8f, Dynamite: 10f
}

public override void OnKill(int timeLeft) {

// Damage the player who threw the bomb.
if (Projectile.friendly && Projectile.owner == Main.myPlayer && !Projectile.npcProj) {
Projectile.HurtPlayer(Projectile.Hitbox);
CutTiles(); // Destroy tall grass and flowers around the explosion.
}

// If in For the Worthy or Get Fixed Boi worlds, the blast damage can damage other players.
if (Main.getGoodWorld && Projectile.owner != Main.myPlayer && Main.netMode == NetmodeID.MultiplayerClient && Projectile.friendly && !Projectile.npcProj) {
Projectile.PrepareBombToBlow();
Projectile.HurtPlayer(Projectile.Hitbox);
}

// If we are the original projectile running on the owner, spawn the 5 child projectiles.
if (Projectile.owner == Main.myPlayer && Projectile.ai[1] == 0) {
if (Projectile.owner == Main.myPlayer && !IsChild) {
for (int i = 0; i < 5; i++) {
// Random upward vector.
Vector2 launchVelocity = new Vector2(Main.rand.NextFloat(-3, 3), Main.rand.NextFloat(-10, -8));
// Importantly, ai1 is set to 1 here. This is checked in OnTileCollide to prevent bouncing and here in Kill to prevent an infinite chain of splitting projectiles.
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, launchVelocity, Projectile.type, Projectile.damage, Projectile.knockBack, Main.myPlayer, 0, 1);
Projectile child = Projectile.NewProjectileDirect(Projectile.GetSource_FromThis(), Projectile.Center, launchVelocity, Projectile.type, Projectile.damage, Projectile.knockBack, Main.myPlayer, 0, 1);
(child.ModProjectile as ExampleExplosive).IsChild = true;
// Usually editing a projectile after NewProjectile would require sending MessageID.SyncProjectile, but IsChild only affects logic running for the owner so it is not necessary here.
}
}

Expand Down Expand Up @@ -165,7 +201,7 @@ public override void OnKill(int timeLeft) {

// Finally, actually explode the tiles and walls. Run this code only for the owner
if (Projectile.owner == Main.myPlayer) {
int explosionRadius = 7;
int explosionRadius = 7; // Bomb: 4, Dynamite: 7, Explosives & TNT Barrel: 10
int minTileX = (int)(Projectile.Center.X / 16f - explosionRadius);
int maxTileX = (int)(Projectile.Center.X / 16f + explosionRadius);
int minTileY = (int)(Projectile.Center.Y / 16f - explosionRadius);
Expand Down
145 changes: 145 additions & 0 deletions ExampleMod/Content/Projectiles/Rockets/ExampleGrenadeProjectile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.Audio;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.ModLoader;

namespace ExampleMod.Content.Projectiles.Rockets
{
// This grenade is for the grenades shot by the Grenade Launcher, not the grenades that you can throw.
public class ExampleGrenadeProjectile : ModProjectile
{
public override void SetStaticDefaults() {
ProjectileID.Sets.PlayerHurtDamageIgnoresDifficultyScaling[Type] = true; // Damage dealt to players does not scale with difficulty in vanilla.

// This set handles some things for us already:
// Sets the timeLeft to 3 and the projectile direction when colliding with an NPC or player in PVP (so the explosive can detonate).
// Explosives also bounce off the top of Shimmer, detonate with no blast damage when touching the bottom or sides of Shimmer, and damage other players in For the Worthy worlds.
ProjectileID.Sets.Explosive[Type] = true;
}
public override void SetDefaults() {
Projectile.width = 14;
Projectile.height = 14;
// Grenades use explosive AI, ProjAIStyleID.Explosive (16). You could use that instead here with the correct AIType.
// But, using our own AI allows us to customize things like the dusts that the grenade creates.
Projectile.aiStyle = -1;
Projectile.friendly = true;
Projectile.penetrate = -1; // Infinite penetration so that the blast can hit all enemies within its radius.
Projectile.DamageType = DamageClass.Ranged;
// usesLocalNPCImmunity and localNPCHitCooldown of -1 mean the projectile can only hit the same target once.
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = -1;

Projectile.timeLeft = 180;

// AIType = ProjectileID.GrenadeI;
}
public override void AI() {
// If timeLeft is <= 3, then explode the grenade.
if (Projectile.owner == Main.myPlayer && Projectile.timeLeft <= 3) {
Projectile.PrepareBombToBlow();
}
else {
// Spawn a smoke dust.
var smokeDust = Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.Smoke, 0f, 0f, 100);
smokeDust.scale *= 1f + Main.rand.Next(10) * 0.1f;
smokeDust.velocity *= 0.2f;
smokeDust.noGravity = true;
}

Projectile.ai[0] += 1f;
// Wait 15 ticks until applying friction and gravity.
if (Projectile.ai[0] > 15f) {
// Slow down if on the ground.
if (Projectile.velocity.Y == 0f) {
Projectile.velocity.X *= 0.95f;
}

// Fall down. Remember, positive Y is down.
Projectile.velocity.Y += 0.2f;
}

// Rotate the grenade in the direction it is moving.
Projectile.rotation += Projectile.velocity.X * 0.1f;
}

public override bool OnTileCollide(Vector2 oldVelocity) {
// Bounce off of tiles.
if (Projectile.velocity.X != oldVelocity.X) {
Projectile.velocity.X = oldVelocity.X * -0.4f;
}

if (Projectile.velocity.Y != oldVelocity.Y && oldVelocity.Y > 0.7f) {
Projectile.velocity.Y = oldVelocity.Y * -0.4f;
}

// Return false so the projectile doesn't get killed. If you do want your projectile to explode on contact with tiles, do not return true here.
// If you return true, the projectile will die without being resized (no blast radius).
// Instead, set `Projectile.timeLeft = 3;` like the Example Rocket Projectile.
return false;
}

public override void PrepareBombToBlow() {
Projectile.tileCollide = false; // This is important or the explosion will be in the wrong place if the grenade explodes on slopes.
Projectile.alpha = 255; // Make the grenade invisible.

// Resize the hitbox of the projectile for the blast "radius".
// Rocket I: 128, Rocket III: 200, Mini Nuke Rocket: 250
// Measurements are in pixels, so 128 / 16 = 8 tiles.
Projectile.Resize(128, 128);
// Set the knockback of the blast.
// Rocket I: 8f, Rocket III: 10f, Mini Nuke Rocket: 12f
Projectile.knockBack = 8f;
}

public override void OnKill(int timeLeft) {
// Play an exploding sound.
SoundEngine.PlaySound(SoundID.Item62, Projectile.position);

// Resize the projectile again so the explosion dust and gore spawn from the middle.
// Rocket I: 22, Rocket III: 80, Mini Nuke Rocket: 50
Projectile.Resize(22, 22);

// Spawn a bunch of smoke dusts.
for (int i = 0; i < 30; i++) {
var smoke = Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.Smoke, 0f, 0f, 100, default, 1.5f);
smoke.velocity *= 1.4f;
}

// Spawn a bunch of fire dusts.
for (int j = 0; j < 20; j++) {
var fireDust = Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.Torch, 0f, 0f, 100, default, 3.5f);
fireDust.noGravity = true;
fireDust.velocity *= 7f;
fireDust = Dust.NewDustDirect(Projectile.position, Projectile.width, Projectile.height, DustID.Torch, 0f, 0f, 100, default, 1.5f);
fireDust.velocity *= 3f;
}

// Spawn a bunch of smoke gores.
for (int k = 0; k < 2; k++) {
float speedMulti = 0.4f;
if (k == 1) {
speedMulti = 0.8f;
}

var smokeGore = Gore.NewGoreDirect(Projectile.GetSource_Death(), Projectile.position, default, Main.rand.Next(GoreID.Smoke1, GoreID.Smoke3 + 1));
smokeGore.velocity *= speedMulti;
smokeGore.velocity += Vector2.One;
smokeGore = Gore.NewGoreDirect(Projectile.GetSource_Death(), Projectile.position, default, Main.rand.Next(GoreID.Smoke1, GoreID.Smoke3 + 1));
smokeGore.velocity *= speedMulti;
smokeGore.velocity.X -= 1f;
smokeGore.velocity.Y += 1f;
smokeGore = Gore.NewGoreDirect(Projectile.GetSource_Death(), Projectile.position, default, Main.rand.Next(GoreID.Smoke1, GoreID.Smoke3 + 1));
smokeGore.velocity *= speedMulti;
smokeGore.velocity.X += 1f;
smokeGore.velocity.Y -= 1f;
smokeGore = Gore.NewGoreDirect(Projectile.GetSource_Death(), Projectile.position, default, Main.rand.Next(GoreID.Smoke1, GoreID.Smoke3 + 1));
smokeGore.velocity *= speedMulti;
smokeGore.velocity -= Vector2.One;
}

// To make the explosion destroy tiles, take a look at the commented out code in Example Rocket Projectile.
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading