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() || !hostPrism.active || hostPrism.type != ModContent.ProjectileType()) { 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); } } }