Add Avalian Trader Town NPC, add Infinite Mana accessory for testing, add two new magic weapons
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user