diff --git a/Content/Items/Accessories/DevInfiniteMana.cs b/Content/Items/Accessories/DevInfiniteMana.cs new file mode 100644 index 0000000..9ab621e --- /dev/null +++ b/Content/Items/Accessories/DevInfiniteMana.cs @@ -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(); + Item.value = Item.sellPrice(platinum: 999); + } + + public override void UpdateAccessory(Player player, bool hideVisual) + { + player.statMana = player.statManaMax2; + } + } +} \ No newline at end of file diff --git a/Content/Items/Accessories/DevInfiniteMana.png b/Content/Items/Accessories/DevInfiniteMana.png new file mode 100644 index 0000000..9e0cf81 Binary files /dev/null and b/Content/Items/Accessories/DevInfiniteMana.png differ diff --git a/Content/Items/SummonItems/LyykaSummon.cs b/Content/Items/SummonItems/LyykaSummon.cs index 99b832a..b96b28e 100644 --- a/Content/Items/SummonItems/LyykaSummon.cs +++ b/Content/Items/SummonItems/LyykaSummon.cs @@ -18,8 +18,8 @@ namespace ForgottenWawiTech.Content.Items.SummonItems Item.width = 40; Item.height = 40; Item.useTime = 10; - Item.useAnimation = 10; - Item.useStyle = ItemUseStyleID.HoldUp; + Item.useAnimation = 3; + Item.useStyle = ItemUseStyleID.Swing; Item.consumable = false; Item.rare = ModContent.RarityType(); } diff --git a/Content/Items/Weapons/Magic/BookOfUnholy.cs b/Content/Items/Weapons/Magic/BookOfUnholy.cs new file mode 100644 index 0000000..0983954 --- /dev/null +++ b/Content/Items/Weapons/Magic/BookOfUnholy.cs @@ -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(); + } + } +} diff --git a/Content/Items/Weapons/Magic/BookOfUnholy.png b/Content/Items/Weapons/Magic/BookOfUnholy.png new file mode 100644 index 0000000..9e0cf81 Binary files /dev/null and b/Content/Items/Weapons/Magic/BookOfUnholy.png differ diff --git a/Content/Items/Weapons/Magic/ShatteredPrism.cs b/Content/Items/Weapons/Magic/ShatteredPrism.cs new file mode 100644 index 0000000..73c3f8c --- /dev/null +++ b/Content/Items/Weapons/Magic/ShatteredPrism.cs @@ -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(); + 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()] <= 0; + } + } +} \ No newline at end of file diff --git a/Content/Items/Weapons/Magic/ShatteredPrism.png b/Content/Items/Weapons/Magic/ShatteredPrism.png new file mode 100644 index 0000000..9e0cf81 Binary files /dev/null and b/Content/Items/Weapons/Magic/ShatteredPrism.png differ diff --git a/Content/NPCs/TownNPCs/AvalianTrader.cs b/Content/NPCs/TownNPCs/AvalianTrader.cs new file mode 100644 index 0000000..6e8ec83 --- /dev/null +++ b/Content/NPCs/TownNPCs/AvalianTrader.cs @@ -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 SetNPCNameList() + { + return new List() { + "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() + .Add(); + + + 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; + } + + } +} \ No newline at end of file diff --git a/Content/NPCs/TownNPCs/AvalianTrader.png b/Content/NPCs/TownNPCs/AvalianTrader.png new file mode 100644 index 0000000..9e0cf81 Binary files /dev/null and b/Content/NPCs/TownNPCs/AvalianTrader.png differ diff --git a/Content/NPCs/TownNPCs/AvalianTrader_Head.png b/Content/NPCs/TownNPCs/AvalianTrader_Head.png new file mode 100644 index 0000000..9e0cf81 Binary files /dev/null and b/Content/NPCs/TownNPCs/AvalianTrader_Head.png differ diff --git a/Content/NPCs/TownNPCs/AvalianTrader_Shimmer.png b/Content/NPCs/TownNPCs/AvalianTrader_Shimmer.png new file mode 100644 index 0000000..9e0cf81 Binary files /dev/null and b/Content/NPCs/TownNPCs/AvalianTrader_Shimmer.png differ diff --git a/Content/NPCs/TownNPCs/AvalianTrader_Shimmer_Head.png b/Content/NPCs/TownNPCs/AvalianTrader_Shimmer_Head.png new file mode 100644 index 0000000..9e0cf81 Binary files /dev/null and b/Content/NPCs/TownNPCs/AvalianTrader_Shimmer_Head.png differ diff --git a/Content/Projectiles/ShatteredPrismBeam.cs b/Content/Projectiles/ShatteredPrismBeam.cs new file mode 100644 index 0000000..c0e1209 --- /dev/null +++ b/Content/Projectiles/ShatteredPrismBeam.cs @@ -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() || !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); + } + } +} \ No newline at end of file diff --git a/Content/Projectiles/ShatteredPrismBeam.png b/Content/Projectiles/ShatteredPrismBeam.png new file mode 100644 index 0000000..4bcb09b Binary files /dev/null and b/Content/Projectiles/ShatteredPrismBeam.png differ diff --git a/Content/Projectiles/ShatteredPrismHoldout.cs b/Content/Projectiles/ShatteredPrismHoldout.cs new file mode 100644 index 0000000..c92f08a --- /dev/null +++ b/Content/Projectiles/ShatteredPrismHoldout.cs @@ -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(), 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; + } + } +} \ No newline at end of file diff --git a/Localization/en-US_Mods.ForgottenWawiTech.hjson b/Localization/en-US_Mods.ForgottenWawiTech.hjson index ec9ed5a..dcd83ab 100644 --- a/Localization/en-US_Mods.ForgottenWawiTech.hjson +++ b/Localization/en-US_Mods.ForgottenWawiTech.hjson @@ -79,6 +79,21 @@ Items: { DisplayName: High Tech Marvel Tooltip: A high-tech crafting station. } + + BookOfUnholy: { + DisplayName: Book Of Unholy + Tooltip: "" + } + + ShatteredPrism: { + DisplayName: Shattered Prism + Tooltip: "" + } + + DevInfiniteMana: { + DisplayName: Dev Infinite Mana + Tooltip: "" + } } NPCs: { @@ -152,4 +167,24 @@ NPCs: { 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 }