Add Avalian Trader Town NPC, add Infinite Mana accessory for testing, add two new magic weapons

This commit is contained in:
2025-11-09 23:53:14 +00:00
parent b2972be531
commit a73cf0965d
16 changed files with 919 additions and 2 deletions
@@ -0,0 +1,23 @@
using Terraria;
using Terraria.ModLoader;
using ForgottenWawiTech.Content.Rarities;
namespace ForgottenWawiTech.Content.Items.Accessories
{
public class DevInfiniteMana : ModItem
{
public override void SetDefaults()
{
Item.width = 28;
Item.height = 28;
Item.accessory = true;
Item.rare = ModContent.RarityType<FWTDeveloperItem>();
Item.value = Item.sellPrice(platinum: 999);
}
public override void UpdateAccessory(Player player, bool hideVisual)
{
player.statMana = player.statManaMax2;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

+2 -2
View File
@@ -18,8 +18,8 @@ namespace ForgottenWawiTech.Content.Items.SummonItems
Item.width = 40; Item.width = 40;
Item.height = 40; Item.height = 40;
Item.useTime = 10; Item.useTime = 10;
Item.useAnimation = 10; Item.useAnimation = 3;
Item.useStyle = ItemUseStyleID.HoldUp; Item.useStyle = ItemUseStyleID.Swing;
Item.consumable = false; Item.consumable = false;
Item.rare = ModContent.RarityType<FWTDeveloperItem>(); Item.rare = ModContent.RarityType<FWTDeveloperItem>();
} }
@@ -0,0 +1,80 @@
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.Audio;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.ModLoader;
namespace ForgottenWawiTech.Content.Items.Weapons.Magic
{
public class BookOfUnholy: ModItem
{
public override void SetDefaults()
{
Item.damage = 10000;
Item.mana = 0; // Temp value for development
Item.crit = 100;
Item.DamageType = DamageClass.Magic;
Item.width = 40;
Item.height = 40;
Item.useTime = 10;
Item.useAnimation = 10;
Item.useStyle = ItemUseStyleID.Shoot;
Item.knockBack = 6;
Item.value = Item.buyPrice(platinum: 20);
Item.rare = ItemRarityID.Red;
Item.autoReuse = true;
Item.noMelee = true;
Item.shoot = ProjectileID.PurificationPowder;
Item.shootSpeed = 50f;
Item.useAmmo = AmmoID.None;
}
// On shoot, fire Projectile ID 502 (the rainbow projectile) instead of the default ammo projectile
public override bool Shoot(Player player, Terraria.DataStructures.EntitySource_ItemUse_WithAmmo source, Vector2 position, Vector2 velocity, int type, int damage, float knockBack)
{
// Shoot every projectile from vanilla Terraria magic weapons at once.
int[] projectiles = new int[] {
ProjectileID.WoodenArrowFriendly,
ProjectileID.EnchantedBoomerang,
ProjectileID.MagicMissile,
ProjectileID.Flamelash,
ProjectileID.DemonScythe,
ProjectileID.RainbowRodBullet,
ProjectileID.UnholyTridentFriendly,
ProjectileID.LightBeam,
ProjectileID.NightBeam,
ProjectileID.RainFriendly,
ProjectileID.IceSickle,
ProjectileID.LastPrismLaser,
ProjectileID.NebulaBlaze2
};
foreach (int projectileType in projectiles)
{
Terraria.Projectile.NewProjectile(source, position, velocity, projectileType, damage, knockBack, player.whoAmI);
}
return false; // return false to prevent tModLoader from shooting the default projectile
}
public override Vector2? HoldoutOffset() {
return new Vector2(2f, -2f);
}
public override void ModifyShootStats(Player player, ref Vector2 position, ref Vector2 velocity, ref int type, ref int damage, ref float knockback) {
Vector2 muzzleOffset = Vector2.Normalize(velocity) * 25f;
if (Collision.CanHit(position, 0, 0, position + muzzleOffset, 0, 0)) {
position += muzzleOffset;
}
}
public override void AddRecipes()
{
Recipe recipe = CreateRecipe();
recipe.AddIngredient(ItemID.BouncyBoulder, 100);
recipe.AddIngredient(ItemID.LunarBar, 20);
recipe.AddTile(TileID.LunarCraftingStation);
recipe.Register();
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

@@ -0,0 +1,32 @@
using ForgottenWawiTech.Content.Projectiles;
using Microsoft.Xna.Framework;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace ForgottenWawiTech.Content.Items.Weapons.Magic
{
public class ShatteredPrism : ModItem
{
public override string Texture => "Terraria/Images/Item_" + ItemID.LastPrism; // TODO: Change
public static Color OverrideColor = new(80,0,80);
public override void SetDefaults() {
Item.CloneDefaults(ItemID.LastPrism);
Item.mana = 4;
Item.damage = 2000;
Item.shoot = ModContent.ProjectileType<ShatteredPrismHoldout>();
Item.shootSpeed = 30f;
// Change the item's draw color so that it is visually distinct from the vanilla Last Prism.
Item.color = OverrideColor;
}
// Because this weapon fires a holdout projectile, it needs to block usage if its projectile already exists.
public override bool CanUseItem(Player player) {
return player.ownedProjectileCounts[ModContent.ProjectileType<ShatteredPrismHoldout>()] <= 0;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

+138
View File
@@ -0,0 +1,138 @@
using System.Collections.Generic;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
using Terraria.GameContent.Personalities;
using Terraria.Localization;
namespace ForgottenWawiTech.Content.NPCs.TownNPCs
{
public class AvalianTrader : ModNPC
{
public override void SetStaticDefaults()
{
Main.npcFrameCount[NPC.type] = 25; // The amount of frames the NPC has
NPCID.Sets.ExtraFramesCount[NPC.type] = 9; // Generally for Town NPCs, this should be 9
NPCID.Sets.AttackFrameCount[NPC.type] = 4;
NPCID.Sets.DangerDetectRange[NPC.type] = 700; // The amount of pixels away from the center of the npc that it tries to attack enemies
NPCID.Sets.AttackType[NPC.type] = 0;
NPCID.Sets.AttackTime[NPC.type] = 90; // The amount of time it takes for the NPC's attack animation to be over
NPCID.Sets.AttackAverageChance[NPC.type] = 30;
NPCID.Sets.HatOffsetY[NPC.type] = 4; // For when a party is active, the party hat spawns at a Y offset
// Influences how the NPC looks in the Bestiary
NPCID.Sets.NPCBestiaryDrawModifiers drawModifiers = new NPCID.Sets.NPCBestiaryDrawModifiers(0)
{
Velocity = 1f, // Draws the NPC in the bestiary as if its walking +1 tiles in the x direction
Direction = 1 // -1 is left and 1 is right. NPCs are drawn facing the left by default
};
NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, drawModifiers);
}
public override void SetDefaults()
{
NPC.townNPC = true;
NPC.friendly = true;
NPC.width = 18;
NPC.height = 40;
NPC.aiStyle = 7;
NPC.damage = 10;
NPC.defense = 15;
NPC.lifeMax = 250;
NPC.HitSound = SoundID.NPCHit1;
NPC.DeathSound = SoundID.NPCDeath1;
NPC.knockBackResist = 0.5f;
AnimationType = NPCID.Guide;
}
public override bool CanTownNPCSpawn(int numTownNPCs)
{
// Example: Always spawn if there's room
return true;
}
public override List<string> SetNPCNameList()
{
return new List<string>() {
"Kivuli",
"Sekai",
"Taerik",
"Nykra",
"Vekir",
"Shyrik",
"Kaelis",
"Nyxal",
"Verik",
"Skyris",
"Taeli",
"Nyrik",
"Kaevi",
"Vyxen",
"Syrrik",
"Nykai"
};
}
public override void SetChatButtons(ref string button, ref string button2)
{
button = Language.GetTextValue("LegacyInterface.28"); // "Shop"
}
public override void OnChatButtonClicked(bool firstButton, ref string shop)
{
if (firstButton)
{
shop = "Shop";
}
}
public override void AddShops()
{
var npcShop = new NPCShop(Type)
.Add<Items.Materials.RGBass>()
.Add<Items.Accessories.DevInfiniteMana>();
npcShop.Register();
}
public override string GetChat()
{
switch (0)
{
case 0:
return "*avalian chirping*";
default:
return "I have no idea what to say.";
}
}
public override void TownNPCAttackStrength(ref int damage, ref float knockback)
{
damage = 50;
knockback = 4f;
}
public override void TownNPCAttackCooldown(ref int cooldown, ref int randExtraCooldown)
{
cooldown = 10;
randExtraCooldown = 30;
}
public override void TownNPCAttackProj(ref int projType, ref int attackDelay)
{
projType = 502;
attackDelay = 1;
}
public override void TownNPCAttackProjSpeed(ref float multiplier, ref float gravityCorrection, ref float randomOffset)
{
multiplier = 12f;
randomOffset = 2f;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

+364
View File
@@ -0,0 +1,364 @@
using ForgottenWawiTech.Content.Projectiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.IO;
using Terraria;
using Terraria.Enums;
using Terraria.GameContent;
using Terraria.GameContent.Shaders;
using Terraria.Graphics.Effects;
using Terraria.ID;
using Terraria.ModLoader;
namespace ForgottenWawiTech.Content.Projectiles
{
public class ShatteredPrismBeam : ModProjectile
{
// A helpful math constant for performing beam angling calculations.
private const float PiBeamDivisor = MathHelper.Pi / ShatteredPrismHoldout.NumBeams;
// How much more damage the beams do when the Prism is fully charged. Damage smoothly scales up to this multiplier.
private const float MaxDamageMultiplier = 20f;
// Beams increase their scale from 0 to this value as the Prism charges up.
private const float MaxBeamScale = 1.8f;
// Beams reduce their spread to zero as the Prism charges up. This controls the maximum spread.
private const float MaxBeamSpread = 2f;
// The maximum possible range of the beam. Don't set this too high or it will cause significant lag.
private const float MaxBeamLength = 2400f;
// The width of the beam in pixels for the purposes of tile collision.
// This should generally be left at 1, otherwise the beam tends to stop early when touching tiles.
private const float BeamTileCollisionWidth = 1f;
// The width of the beam in pixels for the purposes of entity hitbox collision.
// This gets scaled with the beam's scale value, so as the beam visually grows its hitbox gets wider as well.
private const float BeamHitboxCollisionWidth = 22f;
// The number of sample points to use when performing a collision hitscan for the beam.
// More points theoretically leads to a higher quality result, but can cause more lag. 3 tends to be enough.
private const int NumSamplePoints = 3;
// How quickly the beam adjusts to sudden changes in length.
// Every frame, the beam replaces this ratio of its current length with its intended length.
// Generally you shouldn't need to change this.
// Setting it too low will make the beam lazily pass through walls before being blocked by them.
private const float BeamLengthChangeFactor = 0.75f;
// The charge percentage required on the host prism for the beam to begin visual effects (e.g. impact dust).
private const float VisualEffectThreshold = 0.1f;
// Each Last Prism beam draws two lasers separately: an inner beam and an outer beam. This controls their opacity.
private const float OuterBeamOpacityMultiplier = 0.75f;
private const float InnerBeamOpacityMultiplier = 0.1f;
// The maximum brightness of the light emitted by the beams. Brightness scales from 0 to this value as the Prism's charge increases.
private const float BeamLightBrightness = 0f;
// These variables control the beam's potential coloration.
// As a value, hue ranges from 0f to 1f, both of which are pure red. The laser beams vary from 0.57 to 0.75, which winds up being a blue-to-purple gradient.
// Saturation ranges from 0f to 1f and controls how greyed out the color is. 0 is fully grayscale, 1 is vibrant, intense color.
// Lightness ranges from 0f to 1f and controls how dark or light the color is. 0 is pitch black. 1 is pure white.
private const float BeamColorHue = 0f;
private const float BeamHueVariance = 0f;
private const float BeamColorSaturation = 0f;
private const float BeamColorLightness = 0f;
// This property encloses the internal AI variable Projectile.ai[0]. It makes the code easier to read.
private float BeamID {
get => Projectile.ai[0];
set => Projectile.ai[0] = value;
}
// This property encloses the internal AI variable Projectile.ai[1].
private float HostPrismIndex {
get => Projectile.ai[1];
set => Projectile.ai[1] = value;
}
// This property encloses the internal AI variable Projectile.localAI[1].
// Normally, localAI is not synced over the network. This beam manually syncs this variable using SendExtraAI and ReceiveExtraAI.
private float BeamLength {
get => Projectile.localAI[1];
set => Projectile.localAI[1] = value;
}
public override void SetDefaults() {
Projectile.width = 18;
Projectile.height = 18;
Projectile.DamageType = DamageClass.Magic;
Projectile.penetrate = -1;
Projectile.alpha = 255;
// The beam itself still stops on tiles, but its invisible "source" Projectile ignores them.
// This prevents the beams from vanishing if the player shoves the Prism into a wall.
Projectile.tileCollide = false;
// Using local NPC immunity allows each beam to strike independently from one another.
Projectile.usesLocalNPCImmunity = true;
Projectile.localNPCHitCooldown = 10;
}
// Send beam length over the network to prevent hitbox-affecting and thus cascading desyncs in multiplayer.
public override void SendExtraAI(BinaryWriter writer) => writer.Write(BeamLength);
public override void ReceiveExtraAI(BinaryReader reader) => BeamLength = reader.ReadSingle();
public override void AI() {
// If something has gone wrong with either the beam or the host Prism, destroy the beam.
Projectile hostPrism = Main.projectile[(int)HostPrismIndex];
if (Projectile.type != ModContent.ProjectileType<ShatteredPrismBeam>() || !hostPrism.active || hostPrism.type != ModContent.ProjectileType<ShatteredPrismHoldout>()) {
Projectile.Kill();
return;
}
// Grab some variables from the host Prism.
Vector2 hostPrismDir = Vector2.Normalize(hostPrism.velocity);
float chargeRatio = MathHelper.Clamp(hostPrism.ai[0] / ShatteredPrismHoldout.MaxCharge, 0f, 1f);
// Update the beam's damage every frame based on charge and the host Prism's damage.
Projectile.damage = (int)(hostPrism.damage * GetDamageMultiplier(chargeRatio));
// The beam cannot strike enemies until the host Prism is at a certain charge level.
Projectile.friendly = hostPrism.ai[0] > ShatteredPrismHoldout.DamageStart;
// This offset is used to make each individual beam orient differently based on its Beam ID.
float beamIdOffset = BeamID - ShatteredPrismHoldout.NumBeams / 2f + 0.5f;
float beamSpread;
float spinRate;
float beamStartSidewaysOffset;
float beamStartForwardsOffset;
// Variables scale smoothly while the host Prism is charging up.
if (chargeRatio < 1f) {
Projectile.scale = MathHelper.Lerp(0f, MaxBeamScale, chargeRatio);
beamSpread = MathHelper.Lerp(MaxBeamSpread, 0f, chargeRatio);
beamStartSidewaysOffset = MathHelper.Lerp(20f, 6f, chargeRatio);
beamStartForwardsOffset = MathHelper.Lerp(-21f, -17f, chargeRatio);
// For the first 2/3 of charge time, the opacity scales up from 0% to 40%.
// Spin rate increases slowly during this time.
if (chargeRatio <= 0.66f) {
float phaseRatio = chargeRatio * 1.5f;
Projectile.Opacity = MathHelper.Lerp(0f, 0.4f, phaseRatio);
spinRate = MathHelper.Lerp(20f, 16f, phaseRatio);
}
// For the last 1/3 of charge time, the opacity scales up from 40% to 100%.
// Spin rate increases dramatically during this time.
else {
float phaseRatio = (chargeRatio - 0.66f) * 3f;
Projectile.Opacity = MathHelper.Lerp(0.4f, 1f, phaseRatio);
spinRate = MathHelper.Lerp(16f, 6f, phaseRatio);
}
}
// If the host Prism is already at max charge, don't calculate anything. Just use the max values.
else {
Projectile.scale = MaxBeamScale;
Projectile.Opacity = 1f;
beamSpread = 0f;
spinRate = 6f;
beamStartSidewaysOffset = 6f;
beamStartForwardsOffset = -17f;
}
// The amount to which the angle changes reduces over time so that the beams look like they are focusing.
float deviationAngle = (hostPrism.ai[0] + beamIdOffset * spinRate) / (spinRate * ShatteredPrismHoldout.NumBeams) * MathHelper.TwoPi;
// This trigonometry calculates where the beam is supposed to be pointing.
Vector2 unitRot = Vector2.UnitY.RotatedBy(deviationAngle);
Vector2 yVec = new Vector2(4f, beamStartSidewaysOffset);
float hostPrismAngle = hostPrism.velocity.ToRotation();
Vector2 beamSpanVector = (unitRot * yVec).RotatedBy(hostPrismAngle);
float sinusoidYOffset = unitRot.Y * PiBeamDivisor * beamSpread;
// Calculate the beam's emanating position. Start with the Prism's center.
Projectile.Center = hostPrism.Center;
// Add a fixed offset to align with the Prism's sprite sheet.
Projectile.position += hostPrismDir * 16f + new Vector2(0f, -hostPrism.gfxOffY);
// Add the forwards offset, measured in pixels.
Projectile.position += hostPrismDir * beamStartForwardsOffset;
// Add the sideways offset vector, which is calculated for the current angle of the beam and scales with the beam's sideways offset.
Projectile.position += beamSpanVector;
// Set the beam's velocity to point towards its current spread direction and sanity check it. It should have magnitude 1.
Projectile.velocity = hostPrismDir.RotatedBy(sinusoidYOffset);
if (Projectile.velocity.HasNaNs() || Projectile.velocity == Vector2.Zero) {
Projectile.velocity = -Vector2.UnitY;
}
Projectile.rotation = Projectile.velocity.ToRotation();
// Update the beam's length by performing a hitscan collision check.
float hitscanBeamLength = PerformBeamHitscan(hostPrism, chargeRatio >= 1f);
BeamLength = MathHelper.Lerp(BeamLength, hitscanBeamLength, BeamLengthChangeFactor);
// This Vector2 stores the beam's hitbox statistics. X = beam length. Y = beam width.
Vector2 beamDims = new Vector2(Projectile.velocity.Length() * BeamLength, Projectile.width * Projectile.scale);
// Only produce dust and cause water ripples if the beam is above a certain charge level.
Color beamColor = GetOuterBeamColor();
if (chargeRatio >= VisualEffectThreshold) {
ProduceBeamDust(beamColor);
// If the game is rendering (i.e. isn't a dedicated server), make the beam disturb water.
if (Main.netMode != NetmodeID.Server) {
ProduceWaterRipples(beamDims);
}
}
// Make the beam cast light along its length. The brightness of the light scales with the charge.
// v3_1 is an unnamed decompiled variable which is the color of the light cast by DelegateMethods.CastLight.
DelegateMethods.v3_1 = beamColor.ToVector3() * BeamLightBrightness * chargeRatio;
Utils.PlotTileLine(Projectile.Center, Projectile.Center + Projectile.velocity * BeamLength, beamDims.Y, new Utils.TileActionAttempt(DelegateMethods.CastLight));
}
// Uses a simple polynomial (x^3) to get sudden but smooth damage increase near the end of the charge-up period.
private float GetDamageMultiplier(float chargeRatio) {
float f = chargeRatio * chargeRatio * chargeRatio;
return MathHelper.Lerp(1f, MaxDamageMultiplier, f);
}
private float PerformBeamHitscan(Projectile prism, bool fullCharge) {
// By default, the hitscan interpolation starts at the Projectile's center.
// If the host Prism is fully charged, the interpolation starts at the Prism's center instead.
Vector2 samplingPoint = Projectile.Center;
if (fullCharge) {
samplingPoint = prism.Center;
}
// Overriding that, if the player shoves the Prism into or through a wall, the interpolation starts at the player's center.
// This last part prevents the player from projecting beams through walls under any circumstances.
Player player = Main.player[Projectile.owner];
if (!Collision.CanHitLine(player.Center, 0, 0, prism.Center, 0, 0)) {
samplingPoint = player.Center;
}
// Perform a laser scan to calculate the correct length of the beam.
// Alternatively, if you want the beam to ignore tiles, just set it to be the max beam length with the following line.
// return MaxBeamLength;
float[] laserScanResults = new float[NumSamplePoints];
Collision.LaserScan(samplingPoint, Projectile.velocity, 0 * Projectile.scale, MaxBeamLength, laserScanResults);
float averageLengthSample = 0f;
for (int i = 0; i < laserScanResults.Length; ++i) {
averageLengthSample += laserScanResults[i];
}
averageLengthSample /= NumSamplePoints;
return averageLengthSample;
}
// Determines whether the specified target hitbox is intersecting with the beam.
public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox) {
// If the target is touching the beam's hitbox (which is a small rectangle vaguely overlapping the host Prism), that's good enough.
if (projHitbox.Intersects(targetHitbox)) {
return true;
}
// Otherwise, perform an AABB line collision check to check the whole beam.
float _ = float.NaN;
Vector2 beamEndPos = Projectile.Center + Projectile.velocity * BeamLength;
return Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(), Projectile.Center, beamEndPos, BeamHitboxCollisionWidth * Projectile.scale, ref _);
}
public override bool PreDraw(ref Color lightColor) {
// If the beam doesn't have a defined direction, don't draw anything.
if (Projectile.velocity == Vector2.Zero) {
return false;
}
Texture2D texture = TextureAssets.Projectile[Type].Value;
Vector2 centerFloored = Projectile.Center.Floor() + Projectile.velocity * Projectile.scale * 10.5f;
Vector2 drawScale = new Vector2(Projectile.scale);
// Reduce the beam length proportional to its square area to reduce block penetration.
float visualBeamLength = BeamLength - 14.5f * Projectile.scale * Projectile.scale;
DelegateMethods.f_1 = 1f; // f_1 is an unnamed decompiled variable whose function is unknown. Leave it at 1.
Vector2 startPosition = centerFloored - Main.screenPosition;
Vector2 endPosition = startPosition + Projectile.velocity * visualBeamLength;
// Draw the outer beam.
DrawBeam(Main.spriteBatch, texture, startPosition, endPosition, drawScale, GetOuterBeamColor() * OuterBeamOpacityMultiplier * Projectile.Opacity);
// Draw the inner beam, which is half size.
drawScale *= 0.5f;
DrawBeam(Main.spriteBatch, texture, startPosition, endPosition, drawScale, GetInnerBeamColor() * InnerBeamOpacityMultiplier * Projectile.Opacity);
// Returning false prevents Terraria from trying to draw the Projectile itself.
return false;
}
private void DrawBeam(SpriteBatch spriteBatch, Texture2D texture, Vector2 startPosition, Vector2 endPosition, Vector2 drawScale, Color beamColor) {
Utils.LaserLineFraming lineFraming = new Utils.LaserLineFraming(DelegateMethods.RainbowLaserDraw);
// c_1 is an unnamed decompiled variable which is the render color of the beam drawn by DelegateMethods.RainbowLaserDraw.
DelegateMethods.c_1 = beamColor;
Utils.DrawLaser(spriteBatch, texture, startPosition, endPosition, drawScale, lineFraming);
}
private Color GetOuterBeamColor() {
// This hue calculation produces a unique color for each beam based on its Beam ID.
float hue = (BeamID / ShatteredPrismHoldout.NumBeams) % BeamHueVariance + BeamColorHue;
// Main.hslToRgb converts Hue, Saturation, Lightness into a Color for general purpose use.
Color c = Main.hslToRgb(hue, BeamColorSaturation, BeamColorLightness);
// Manually reduce the opacity of the color so beams can overlap without completely overwriting each other.
c.A = 64;
return c;
}
private Color GetInnerBeamColor() => Color.Black;
private void ProduceBeamDust(Color beamColor) {
// Create one dust per frame a small distance from where the beam ends.
const int type = 15;
Vector2 endPosition = Projectile.Center + Projectile.velocity * (BeamLength - 14.5f * Projectile.scale);
// Main.rand.NextBool is used to give a 50/50 chance for the angle to point to the left or right.
// This gives the dust a 50/50 chance to fly off on either side of the beam.
float angle = Projectile.rotation + (Main.rand.NextBool() ? 1f : -1f) * MathHelper.PiOver2;
float startDistance = Main.rand.NextFloat(1f, 1.8f);
float scale = Main.rand.NextFloat(0.7f, 1.1f);
Vector2 velocity = angle.ToRotationVector2() * startDistance;
Dust dust = Dust.NewDustDirect(endPosition, 0, 0, type, velocity.X, velocity.Y, 0, beamColor, scale);
dust.color = beamColor;
dust.noGravity = true;
// If the beam is currently large, make the dust faster and larger to match.
if (Projectile.scale > 1f) {
dust.velocity *= Projectile.scale;
dust.scale *= Projectile.scale;
}
}
private void ProduceWaterRipples(Vector2 beamDims) {
WaterShaderData shaderData = (WaterShaderData)Filters.Scene["WaterDistortion"].GetShader();
// A universal time-based sinusoid which updates extremely rapidly. GlobalTime is 0 to 3600, measured in seconds.
float waveSine = 0.1f * (float)Math.Sin(Main.GlobalTimeWrappedHourly * 20f);
Vector2 ripplePos = Projectile.position + new Vector2(beamDims.X * 0.5f, 0f).RotatedBy(Projectile.rotation);
// WaveData is encoded as a Color. Not really sure why.
Color waveData = new Color(0.5f, 0.1f * Math.Sign(waveSine) + 0.5f, 0f, 1f) * Math.Abs(waveSine);
shaderData.QueueRipple(ripplePos, waveData, beamDims, RippleShape.Square, Projectile.rotation);
}
// Automatically iterates through every tile the laser is overlapping to cut grass at all those locations.
public override void CutTiles() {
// tilecut_0 is an unnamed decompiled variable which tells CutTiles how the tiles are being cut (in this case, via a Projectile).
DelegateMethods.tilecut_0 = TileCuttingContext.AttackProjectile;
Utils.TileActionAttempt cut = new Utils.TileActionAttempt(DelegateMethods.CutTiles);
Vector2 beamStartPos = Projectile.Center;
Vector2 beamEndPos = beamStartPos + Projectile.velocity * BeamLength;
// PlotTileLine is a function which performs the specified action to all tiles along a drawn line, with a specified width.
// In this case, it is cutting all tiles which can be destroyed by Projectiles, for example grass or pots.
Utils.PlotTileLine(beamStartPos, beamEndPos, Projectile.width * Projectile.scale, cut);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

@@ -0,0 +1,245 @@
using ForgottenWawiTech.Content.Items.Weapons.Magic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.Audio;
using Terraria.GameContent;
using Terraria.ID;
using Terraria.ModLoader;
namespace ForgottenWawiTech.Content.Projectiles
{
public class ShatteredPrismHoldout : ModProjectile
{
public override string Texture => "Terraria/Images/Projectile_" + ProjectileID.LastPrism;
// The vanilla Last Prism is an animated item with 5 frames of animation. We copy that here.
private const int NumAnimationFrames = 5;
// This controls how many individual beams are fired by the Prism.
public const int NumBeams = 10;
// This value controls how many frames it takes for the Prism to reach "max charge". 60 frames = 1 second.
public const float MaxCharge = 180f;
// This value controls how many frames it takes for the beams to begin dealing damage. Before then they can't hit anything.
public const float DamageStart = 30f;
// This value controls how sluggish the Prism turns while being used. Vanilla Last Prism is 0.08f.
// Higher values make the Prism turn faster.
private const float AimResponsiveness = 0.5f;
// This value controls how frequently the Prism emits sound once it's firing.
private const int SoundInterval = 20;
// These values place caps on the mana consumption rate of the Prism.
// When first used, the Prism consumes mana once every MaxManaConsumptionDelay frames.
// Every time mana is consumed, the pace becomes one frame faster, meaning mana consumption smoothly increases.
// When capped out, the Prism consumes mana once every MinManaConsumptionDelay frames.
private const float MaxManaConsumptionDelay = 15f;
private const float MinManaConsumptionDelay = 5f;
// This property encloses the internal AI variable Projectile.ai[0]. It makes the code easier to read.
private float FrameCounter {
get => Projectile.ai[0];
set => Projectile.ai[0] = value;
}
// This property encloses the internal AI variable Projectile.ai[1].
private float NextManaFrame {
get => Projectile.ai[1];
set => Projectile.ai[1] = value;
}
// This property encloses the internal AI variable Projectile.localAI[0].
// localAI is not automatically synced over the network, but that does not cause any problems in this case.
private float ManaConsumptionRate {
get => Projectile.localAI[0];
set => Projectile.localAI[0] = value;
}
public override void SetStaticDefaults() {
Main.projFrames[Projectile.type] = NumAnimationFrames;
// Signals to Terraria that this Projectile requires a unique identifier beyond its index in the Projectile array.
// This prevents the issue with the vanilla Last Prism where the beams are invisible in multiplayer.
ProjectileID.Sets.NeedsUUID[Projectile.type] = true;
// Prevents jitter when stepping up and down blocks and half blocks
ProjectileID.Sets.HeldProjDoesNotUsePlayerGfxOffY[Type] = true;
}
public override void SetDefaults() {
// Use CloneDefaults to clone all basic Projectile statistics from the vanilla Last Prism.
Projectile.CloneDefaults(ProjectileID.LastPrism);
}
// Held projectiles should set this to false otherwise they will deal contact damage.
public override bool? CanDamage() => false;
public override void AI() {
Player player = Main.player[Projectile.owner];
Vector2 rrp = player.RotatedRelativePoint(player.MountedCenter, true);
// Update the Prism's damage every frame so that it is dynamically affected by Mana Sickness.
UpdateDamageForManaSickness(player);
// Update the frame counter.
FrameCounter += 1f;
// Update Projectile visuals and sound.
UpdateAnimation();
PlaySounds();
// Update the Prism's position in the world and relevant variables of the player holding it.
UpdatePlayerVisuals(player, rrp);
// Update the Prism's behavior: project beams on frame 1, consume mana, and despawn if out of mana.
if (Projectile.owner == Main.myPlayer) {
// Slightly re-aim the Prism every frame so that it gradually sweeps to point towards the mouse.
UpdateAim(rrp, player.HeldItem.shootSpeed);
// player.CheckMana returns true if the mana cost can be paid. Since the second argument is true, the mana is actually consumed.
// If mana shouldn't consumed this frame, the || operator short-circuits its evaluation player.CheckMana never executes.
bool manaIsAvailable = !ShouldConsumeMana() || player.CheckMana(player.HeldItem.mana, true, false);
// The Prism immediately stops functioning if the player is Cursed (player.noItems) or "Crowd Controlled", e.g. the Frozen debuff.
// player.channel indicates whether the player is still holding down the mouse button to use the item.
bool stillInUse = player.channel && manaIsAvailable && !player.noItems && !player.CCed;
// Spawn in the Prism's lasers on the first frame if the player is capable of using the item.
if (stillInUse && FrameCounter == 1f) {
FireBeams();
}
// If the Prism cannot continue to be used, then destroy it immediately.
else if (!stillInUse) {
Projectile.Kill();
}
}
// This ensures that the Prism never times out while in use.
Projectile.timeLeft = 2;
}
private void UpdateDamageForManaSickness(Player player) {
Projectile.damage = (int)player.GetDamage(DamageClass.Magic).ApplyTo(player.HeldItem.damage);
}
private void UpdateAnimation() {
Projectile.frameCounter++;
// As the Prism charges up and focuses the beams, its animation plays faster.
int framesPerAnimationUpdate = FrameCounter >= MaxCharge ? 2 : FrameCounter >= (MaxCharge * 0.66f) ? 3 : 4;
// If necessary, change which specific frame of the animation is displayed.
if (Projectile.frameCounter >= framesPerAnimationUpdate) {
Projectile.frameCounter = 0;
if (++Projectile.frame >= NumAnimationFrames) {
Projectile.frame = 0;
}
}
}
private void PlaySounds() {
// The Prism makes sound intermittently while in use, using the vanilla Projectile variable soundDelay.
if (Projectile.soundDelay <= 0) {
Projectile.soundDelay = SoundInterval;
// On the very first frame, the sound playing is skipped. This way it doesn't overlap the starting hiss sound.
if (FrameCounter > 1f) {
SoundEngine.PlaySound(SoundID.Item15, Projectile.position);
}
}
}
private void UpdatePlayerVisuals(Player player, Vector2 playerHandPos) {
// Place the Prism directly into the player's hand at all times.
Projectile.Center = playerHandPos;
// The beams emit from the tip of the Prism, not the side. As such, rotate the sprite by pi/2 (90 degrees).
Projectile.rotation = Projectile.velocity.ToRotation() + MathHelper.PiOver2;
Projectile.spriteDirection = Projectile.direction;
// The Prism is a holdout Projectile, so change the player's variables to reflect that.
// Constantly resetting player.itemTime and player.itemAnimation prevents the player from switching items or doing anything else.
player.ChangeDir(Projectile.direction);
player.heldProj = Projectile.whoAmI;
player.itemTime = 2;
player.itemAnimation = 2;
// If you do not multiply by Projectile.direction, the player's hand will point the wrong direction while facing left.
player.itemRotation = (Projectile.velocity * Projectile.direction).ToRotation();
}
private bool ShouldConsumeMana() {
// If the mana consumption timer hasn't been initialized yet, initialize it and consume mana on frame 1.
if (ManaConsumptionRate == 0f) {
NextManaFrame = ManaConsumptionRate = MaxManaConsumptionDelay;
return true;
}
// Should mana be consumed this frame?
bool consume = FrameCounter == NextManaFrame;
// If mana is being consumed this frame, update the rate of mana consumption and write down the next frame mana will be consumed.
if (consume) {
// MathHelper.Clamp(X,A,B) guarantees that A <= X <= B. If X is outside the range, it will be set to A or B accordingly.
ManaConsumptionRate = MathHelper.Clamp(ManaConsumptionRate - 1f, MinManaConsumptionDelay, MaxManaConsumptionDelay);
NextManaFrame += ManaConsumptionRate;
}
return consume;
}
private void UpdateAim(Vector2 source, float speed) {
// Get the player's current aiming direction as a normalized vector.
Vector2 aim = Vector2.Normalize(Main.MouseWorld - source);
if (aim.HasNaNs()) {
aim = -Vector2.UnitY;
}
// Change a portion of the Prism's current velocity so that it points to the mouse. This gives smooth movement over time.
aim = Vector2.Normalize(Vector2.Lerp(Vector2.Normalize(Projectile.velocity), aim, AimResponsiveness));
aim *= speed;
if (aim != Projectile.velocity) {
Projectile.netUpdate = true;
}
Projectile.velocity = aim;
}
private void FireBeams() {
// If for some reason the beam velocity can't be correctly normalized, set it to a default value.
Vector2 beamVelocity = Vector2.Normalize(Projectile.velocity);
if (beamVelocity.HasNaNs()) {
beamVelocity = -Vector2.UnitY;
}
// This UUID will be the same between all players in multiplayer, ensuring that the beams are properly anchored on the Prism on everyone's screen.
int uuid = Projectile.GetByUUID(Projectile.owner, Projectile.whoAmI);
int damage = Projectile.damage;
float knockback = Projectile.knockBack;
for (int b = 0; b < NumBeams; ++b) {
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, beamVelocity, ModContent.ProjectileType<ShatteredPrismBeam>(), damage, knockback, Projectile.owner, b, uuid);
}
// After creating the beams, mark the Prism as having an important network event. This will make Terraria sync its data to other players ASAP.
Projectile.netUpdate = true;
}
// Because the Prism is a holdout Projectile and stays glued to its user, it needs custom drawcode.
public override bool PreDraw(ref Color lightColor) {
SpriteEffects effects = Projectile.spriteDirection == -1 ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
Texture2D texture = TextureAssets.Projectile[Type].Value;
int frameHeight = texture.Height / Main.projFrames[Projectile.type];
int spriteSheetOffset = frameHeight * Projectile.frame;
Vector2 sheetInsertPosition = (Projectile.Center + Vector2.UnitY * Projectile.gfxOffY - Main.screenPosition).Floor();
// The Prism is always at full brightness, regardless of the surrounding light. This is equivalent to it being its own glowmask.
// It is drawn in a non-white color to distinguish it from the vanilla Last Prism.
Color drawColor = ShatteredPrism.OverrideColor;
Main.EntitySpriteDraw(texture, sheetInsertPosition, new Rectangle?(new Rectangle(0, spriteSheetOffset, texture.Width, frameHeight)), drawColor, Projectile.rotation, new Vector2(texture.Width / 2f, frameHeight / 2f), Projectile.scale, effects, 0f);
return false;
}
}
}
@@ -79,6 +79,21 @@ Items: {
DisplayName: High Tech Marvel DisplayName: High Tech Marvel
Tooltip: A high-tech crafting station. Tooltip: A high-tech crafting station.
} }
BookOfUnholy: {
DisplayName: Book Of Unholy
Tooltip: ""
}
ShatteredPrism: {
DisplayName: Shattered Prism
Tooltip: ""
}
DevInfiniteMana: {
DisplayName: Dev Infinite Mana
Tooltip: ""
}
} }
NPCs: { NPCs: {
@@ -152,4 +167,24 @@ NPCs: {
Princess_LovesNPC: I like {NPCName}. Princess_LovesNPC: I like {NPCName}.
} }
} }
AvalianTrader: {
DisplayName: Avalian Trader
TownNPCMood: {
Content: I am content.
NoHome: I hate not having a home.
FarFromHome: I am too far from home.
LoveSpace: I love having so much space.
DislikeCrowded: I dislike how crowded I am.
HateCrowded: I hate how crowded I am.
LikeNPC_Princess: I like {NPCName}.
Princess_LovesNPC: I like {NPCName}.
}
}
}
Projectiles: {
ShatteredPrismBeam.DisplayName: Shattered Prism Beam
ShatteredPrismHoldout.DisplayName: Shattered Prism Holdout
} }