diff --git a/Content/Items/Global/EyeOfCthulhuBagLoot.cs b/Content/Items/Global/EyeOfCthulhuBagLoot.cs new file mode 100644 index 0000000..30b2ee1 --- /dev/null +++ b/Content/Items/Global/EyeOfCthulhuBagLoot.cs @@ -0,0 +1,20 @@ +using Terraria; +using Terraria.ID; +using Terraria.ModLoader; +using Terraria.GameContent.ItemDropRules; + +namespace ForgottenWawiTech.Content.Items.Global +{ + // Adds BeamAttackWand to Eye of Cthulhu treasure bag loot table. + public class EyeOfCthulhuBagLoot : GlobalItem + { + public override void ModifyItemLoot(Item item, ItemLoot itemLoot) + { + if (item.type == ItemID.EyeOfCthulhuBossBag) + { + // 1 in 3 chance (adjust if too common) + itemLoot.Add(ItemDropRule.Common(ModContent.ItemType(), 3)); + } + } + } +} diff --git a/Content/Items/Weapons/Magic/BeamAttackWand.cs b/Content/Items/Weapons/Magic/BeamAttackWand.cs new file mode 100644 index 0000000..2d8249b --- /dev/null +++ b/Content/Items/Weapons/Magic/BeamAttackWand.cs @@ -0,0 +1,95 @@ +using ForgottenWawiTech.Content.Projectiles; +using ForgottenWawiTech.Content.Players; +using Microsoft.Xna.Framework; +using Terraria; +using Terraria.ID; +using Terraria.ModLoader; + +namespace ForgottenWawiTech.Content.Items.Weapons.Magic +{ + // A post-Eye of Cthulhu melee/magic hybrid style weapon that swings like a sword and + // emits a flurry of short-lived beam projectiles toward the cursor during the swing. + // Beams have reduced damage (50% of item base) and very short lifetime. + public class BeamAttackWand : ModItem + { + public override void SetStaticDefaults() + { + // DisplayName.SetDefault("Beam Attack Wand"); // tML auto localizes; optional if using hjson. + // Tooltip.SetDefault("Swings and releases a flurry of brief energy beams"); + } + + public override void SetDefaults() + { + Item.width = 32; + Item.height = 32; + Item.useStyle = ItemUseStyleID.Swing; + Item.useAnimation = 28; // frames the swing lasts + Item.useTime = 28; // one use per full animation + Item.autoReuse = true; + Item.UseSound = SoundID.Item1; + Item.damage = 25; + Item.knockBack = 4.5f; + Item.DamageType = DamageClass.Magic; // treat beams as melee for synergy + Item.rare = ItemRarityID.Blue; // around early game progression + Item.value = Item.buyPrice(silver: 50); + Item.shoot = ModContent.ProjectileType(); + Item.shootSpeed = 12f; // base speed of beams + Item.noMelee = false; + Item.mana = 3; + } + + // No recipe: obtained from Eye of Cthulhu treasure bag (see GlobalItem loot class) + + public override void HoldItem(Player player) + { + // Emit beams only while the item is mid-swing + if (player.itemAnimation > 0 && player.HeldItem.type == Item.type && player.whoAmI == Main.myPlayer) + { + var modPlayer = player.GetModPlayer(); + modPlayer.BeamEmissionTimer--; + if (modPlayer.BeamEmissionTimer <= 0) + { + // Kirby-style beam: a sweeping whip that leaves a multi-beam arc. + // We'll spawn several beams at once in a tight angular spread around a smoothed central swing rotation. + float targetRot = player.itemRotation + (player.direction == -1 ? MathHelper.Pi : 0f); + if (modPlayer.LastBeamRot == 0f) + modPlayer.LastBeamRot = targetRot; + // Smooth aggressively so the arc persists briefly (lower factor = larger persistence) + float smoothedRot = modPlayer.LastBeamRot + MathHelper.WrapAngle(targetRot - modPlayer.LastBeamRot) * 0.25f; + modPlayer.LastBeamRot = smoothedRot; + + // Determine swing progress (0..1) to slightly widen arc near middle of swing + float swingProgress = 1f - (player.itemAnimation / (float)player.itemAnimationMax); + // Arc width scales up mid-swing then back down + float baseArc = 0.30f; // radians total small arc + float arcWidth = baseArc * (float)(1.0 - System.Math.Pow(2 * swingProgress - 1, 2)); // bell curve widen + + int beamsThisBurst = 4; // number of simultaneous beams + int beamDamage = (int)(Item.damage * 0.45f); // slight reduction since multiple per tick + for (int i = 0; i < beamsThisBurst; i++) + { + // Evenly distribute within arc centered on smoothedRot + float lerp = beamsThisBurst == 1 ? 0f : i / (float)(beamsThisBurst - 1); + float offset = MathHelper.Lerp(-arcWidth * 0.5f, arcWidth * 0.5f, lerp); + float beamRot = smoothedRot + offset; + Vector2 dir = beamRot.ToRotationVector2(); + if (dir.HasNaNs()) dir = new Vector2(player.direction, 0f); + dir = dir.SafeNormalize(new Vector2(player.direction, 0f)); + + // Stagger spawn distance to create a whip chain illusion (front beams slightly further) + float distance = 14f + 3f * i; + Vector2 spawnPos = player.Center + dir * distance + new Vector2(0f, -4f); + Vector2 velocity = dir * Item.shootSpeed * (1f + i * 0.05f); // slight incremental speed + + Projectile.NewProjectile(player.GetSource_ItemUse(Item), spawnPos, velocity, + Item.shoot, beamDamage, Item.knockBack, player.whoAmI); + } + + // Emit burst every 3 ticks to keep DPS reasonable while visually dense + modPlayer.BeamEmissionTimer = 3; + } + } + } + } +} + diff --git a/Content/Items/Weapons/Magic/BeamAttackWand.png b/Content/Items/Weapons/Magic/BeamAttackWand.png new file mode 100644 index 0000000..9dc9497 Binary files /dev/null and b/Content/Items/Weapons/Magic/BeamAttackWand.png differ diff --git a/Content/NPCs/Enemies/AvalianInvasion/AvalianEliteCommando.cs b/Content/NPCs/Enemies/AvalianInvasion/AvalianEliteCommando.cs new file mode 100644 index 0000000..8fd480a --- /dev/null +++ b/Content/NPCs/Enemies/AvalianInvasion/AvalianEliteCommando.cs @@ -0,0 +1,119 @@ +using Terraria; +using Terraria.ID; +using Terraria.ModLoader; +using Microsoft.Xna.Framework; + +namespace ForgottenWawiTech.Content.NPCs.Enemies.AvalianInvasion +{ + public class AvalianEliteCommando : ModNPC + { + private int attackTimer = 0; + private int teleportTimer = 0; + + public override void SetStaticDefaults() + { + Main.npcFrameCount[Type] = 8; + NPCID.Sets.NPCBestiaryDrawModifiers value = new NPCID.Sets.NPCBestiaryDrawModifiers(0) + { + Velocity = 1f + }; + NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, value); + } + + public override void SetDefaults() + { + NPC.width = 38; + NPC.height = 48; + NPC.damage = 70; + NPC.defense = 30; + NPC.lifeMax = 800; + NPC.HitSound = SoundID.NPCHit1; + NPC.DeathSound = SoundID.NPCDeath6; + NPC.value = Item.buyPrice(0, 0, 50, 0); + NPC.knockBackResist = 0.1f; + NPC.aiStyle = NPCAIStyleID.Bat; + } + + public override void AI() + { + Player target = Main.player[NPC.target]; + if (target.active && !target.dead) + { + attackTimer++; + teleportTimer++; + + // Fast movement + Vector2 toPlayer = Vector2.Normalize(target.Center - NPC.Center); + float distanceToPlayer = Vector2.Distance(NPC.Center, target.Center); + + // Teleport mechanic every 180 frames (3 seconds) + if (teleportTimer >= 180 && distanceToPlayer > 200f) + { + teleportTimer = 0; + + // Teleport dust effect + for (int i = 0; i < 20; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Electric); + } + + // Teleport near player + Vector2 teleportPos = target.Center + new Vector2(Main.rand.Next(-200, 200), -150); + NPC.position = teleportPos; + + for (int i = 0; i < 20; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Electric); + } + } + + // Rapid fire attack every 30 frames (0.5 seconds) + if (attackTimer >= 30) + { + attackTimer = 0; + + if (Main.netMode != NetmodeID.MultiplayerClient) + { + Vector2 shootDirection = Vector2.Normalize(target.Center - NPC.Center); + + // Triple shot spread + for (int i = -1; i <= 1; i++) + { + Vector2 perturbedSpeed = shootDirection.RotatedBy(MathHelper.ToRadians(15 * i)); + Projectile.NewProjectile(NPC.GetSource_FromAI(), NPC.Center, + perturbedSpeed * 14f, ProjectileID.MartianTurretBolt, + NPC.damage / 3, 1f); + } + } + } + + // Aggressive movement + if (distanceToPlayer > 150f) + { + NPC.velocity = toPlayer * 6f; + } + } + } + + public override void HitEffect(NPC.HitInfo hit) + { + if (NPC.life <= 0) + { + // Epic death effects + for (int i = 0; i < 20; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Feather); + } + for (int i = 0; i < 15; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Electric); + } + } + } + + public override float SpawnChance(NPCSpawnInfo spawnInfo) + { + return 0f; // Controlled by invasion system + } + } +} diff --git a/Content/NPCs/Enemies/AvalianInvasion/AvalianRifleman.cs b/Content/NPCs/Enemies/AvalianInvasion/AvalianRifleman.cs new file mode 100644 index 0000000..30d3821 --- /dev/null +++ b/Content/NPCs/Enemies/AvalianInvasion/AvalianRifleman.cs @@ -0,0 +1,85 @@ +using Terraria; +using Terraria.ID; +using Terraria.ModLoader; +using Microsoft.Xna.Framework; + +namespace ForgottenWawiTech.Content.NPCs.Enemies.AvalianInvasion +{ + public class AvalianRifleman : ModNPC + { + private int shootTimer = 0; + + public override void SetStaticDefaults() + { + Main.npcFrameCount[Type] = 6; + NPCID.Sets.NPCBestiaryDrawModifiers value = new NPCID.Sets.NPCBestiaryDrawModifiers(0) + { + Velocity = 1f + }; + NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, value); + } + + public override void SetDefaults() + { + NPC.width = 34; + NPC.height = 42; + NPC.damage = 55; + NPC.defense = 20; + NPC.lifeMax = 400; + NPC.HitSound = SoundID.NPCHit1; + NPC.DeathSound = SoundID.NPCDeath6; + NPC.value = Item.buyPrice(0, 0, 10, 0); + NPC.knockBackResist = 0.3f; + NPC.aiStyle = NPCAIStyleID.Bat; // Flying ranged enemy + } + + public override void AI() + { + // Ranged attack behavior + Player target = Main.player[NPC.target]; + if (target.active && !target.dead) + { + shootTimer++; + + // Keep distance from player + float distanceToPlayer = Vector2.Distance(NPC.Center, target.Center); + if (distanceToPlayer < 300f) + { + // Move away + Vector2 awayDirection = Vector2.Normalize(NPC.Center - target.Center); + NPC.velocity = awayDirection * 3f; + } + + // Shoot every 90 frames (1.5 seconds) + if (shootTimer >= 90) + { + shootTimer = 0; + + if (Main.netMode != NetmodeID.MultiplayerClient) + { + Vector2 shootDirection = Vector2.Normalize(target.Center - NPC.Center); + Projectile.NewProjectile(NPC.GetSource_FromAI(), NPC.Center, + shootDirection * 10f, ProjectileID.BulletHighVelocity, + NPC.damage / 2, 1f); + } + } + } + } + + public override void HitEffect(NPC.HitInfo hit) + { + if (NPC.life <= 0) + { + for (int i = 0; i < 12; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Feather); + } + } + } + + public override float SpawnChance(NPCSpawnInfo spawnInfo) + { + return 0f; // Controlled by invasion system + } + } +} diff --git a/Content/NPCs/Enemies/AvalianInvasion/AvalianScout.cs b/Content/NPCs/Enemies/AvalianInvasion/AvalianScout.cs new file mode 100644 index 0000000..618e609 --- /dev/null +++ b/Content/NPCs/Enemies/AvalianInvasion/AvalianScout.cs @@ -0,0 +1,62 @@ +using Terraria; +using Terraria.ID; +using Terraria.ModLoader; +using Microsoft.Xna.Framework; + +namespace ForgottenWawiTech.Content.NPCs.Enemies.AvalianInvasion +{ + public class AvalianScout : ModNPC + { + public override void SetStaticDefaults() + { + Main.npcFrameCount[Type] = 6; // Animation frames + NPCID.Sets.NPCBestiaryDrawModifiers value = new NPCID.Sets.NPCBestiaryDrawModifiers(0) + { + Velocity = 1f + }; + NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, value); + } + + public override void SetDefaults() + { + NPC.width = 32; + NPC.height = 40; + NPC.damage = 45; + NPC.defense = 15; + NPC.lifeMax = 250; + NPC.HitSound = SoundID.NPCHit1; + NPC.DeathSound = SoundID.NPCDeath6; + NPC.value = Item.buyPrice(0, 0, 5, 0); + NPC.knockBackResist = 0.5f; + NPC.aiStyle = NPCAIStyleID.Fighter; // Basic fighter AI + } + + public override void AI() + { + // Add subtle hover/flight behavior + if (NPC.velocity.Y > -2f && NPC.velocity.Y < 2f) + { + NPC.velocity.Y += Main.rand.NextFloat(-0.1f, 0.1f); + } + } + + public override void HitEffect(NPC.HitInfo hit) + { + // Feather particles on hit + if (NPC.life <= 0) + { + for (int i = 0; i < 10; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Feather, + Main.rand.NextFloat(-2f, 2f), Main.rand.NextFloat(-2f, 2f)); + } + } + } + + public override float SpawnChance(NPCSpawnInfo spawnInfo) + { + // Only spawn during Avalian Invasion event + return 0f; // Controlled by invasion system + } + } +} diff --git a/Content/NPCs/Enemies/AvalianInvasion/AvalianTechnician.cs b/Content/NPCs/Enemies/AvalianInvasion/AvalianTechnician.cs new file mode 100644 index 0000000..ea0f445 --- /dev/null +++ b/Content/NPCs/Enemies/AvalianInvasion/AvalianTechnician.cs @@ -0,0 +1,97 @@ +using Terraria; +using Terraria.ID; +using Terraria.ModLoader; +using Microsoft.Xna.Framework; + +namespace ForgottenWawiTech.Content.NPCs.Enemies.AvalianInvasion +{ + public class AvalianTechnician : ModNPC + { + private int laserTimer = 0; + + public override void SetStaticDefaults() + { + Main.npcFrameCount[Type] = 6; + NPCID.Sets.NPCBestiaryDrawModifiers value = new NPCID.Sets.NPCBestiaryDrawModifiers(0) + { + Velocity = 1f + }; + NPCID.Sets.NPCBestiaryDrawOffset.Add(Type, value); + } + + public override void SetDefaults() + { + NPC.width = 36; + NPC.height = 44; + NPC.damage = 40; + NPC.defense = 12; + NPC.lifeMax = 300; + NPC.HitSound = SoundID.NPCHit1; + NPC.DeathSound = SoundID.NPCDeath6; + NPC.value = Item.buyPrice(0, 0, 15, 0); + NPC.knockBackResist = 0.4f; + NPC.aiStyle = NPCAIStyleID.Bat; + } + + public override void AI() + { + Player target = Main.player[NPC.target]; + if (target.active && !target.dead) + { + laserTimer++; + + // Circle around player + float distanceToPlayer = Vector2.Distance(NPC.Center, target.Center); + if (distanceToPlayer > 400f) + { + // Move closer + Vector2 toPlayer = Vector2.Normalize(target.Center - NPC.Center); + NPC.velocity = toPlayer * 4f; + } + else if (distanceToPlayer < 350f) + { + // Orbit + Vector2 toPlayer = target.Center - NPC.Center; + Vector2 perpendicular = new Vector2(-toPlayer.Y, toPlayer.X); + perpendicular.Normalize(); + NPC.velocity = perpendicular * 3f; + } + + // Fire laser every 60 frames (1 second) + if (laserTimer >= 60) + { + laserTimer = 0; + + if (Main.netMode != NetmodeID.MultiplayerClient) + { + Vector2 shootDirection = Vector2.Normalize(target.Center - NPC.Center); + Projectile.NewProjectile(NPC.GetSource_FromAI(), NPC.Center, + shootDirection * 12f, ProjectileID.DeathLaser, + NPC.damage / 2, 1f); + } + } + } + } + + public override void HitEffect(NPC.HitInfo hit) + { + if (NPC.life <= 0) + { + // Tech parts and feathers + for (int i = 0; i < 8; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Feather); + } + for (int i = 0; i < 5; i++) + { + Dust.NewDust(NPC.position, NPC.width, NPC.height, DustID.Electric); + } + } + } + + public override float SpawnChance(NPCSpawnInfo spawnInfo) + { + return 0f; // Controlled by invasion system + } + } +} diff --git a/Content/Players/BeamAttackWandPlayer.cs b/Content/Players/BeamAttackWandPlayer.cs new file mode 100644 index 0000000..efc0808 --- /dev/null +++ b/Content/Players/BeamAttackWandPlayer.cs @@ -0,0 +1,22 @@ +using Terraria.ModLoader; +using Terraria; + +namespace ForgottenWawiTech.Content.Players +{ + // Tracks per-tick emission cooldown for BeamAttackWand while swinging. + public class BeamAttackWandPlayer : ModPlayer + { + public int BeamEmissionTimer = 0; + public float LastBeamRot; // smoothed rotation used to cluster beams + + public override void ResetEffects() + { + // If not holding the wand, allow immediate beam spawn next time it's used and reset rotation smoothing. + if (Player.HeldItem.type != ModContent.ItemType()) + { + BeamEmissionTimer = 0; + LastBeamRot = 0f; + } + } + } +} diff --git a/Content/Projectiles/BeamAttackWandBeam.cs b/Content/Projectiles/BeamAttackWandBeam.cs new file mode 100644 index 0000000..17d1395 --- /dev/null +++ b/Content/Projectiles/BeamAttackWandBeam.cs @@ -0,0 +1,94 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Terraria; +using Terraria.ID; +using Terraria.ModLoader; +using Terraria.GameContent; + +namespace ForgottenWawiTech.Content.Projectiles +{ + // Short-lived beam projectile emitted rapidly by BeamAttackWand. + // Simple straight projectile with minor dust and fade-out. + public class BeamAttackWandBeam : ModProjectile + { + private const int FrameCount = 3; + public override void SetStaticDefaults() + { + Main.projFrames[Type] = FrameCount; // 3 vertical frames + } + + public override void SetDefaults() + { + Projectile.width = 28; // frame width + Projectile.height = 28; // frame height + Projectile.friendly = true; + Projectile.penetrate = 1; // single hit per beam + Projectile.timeLeft = 15; // very short duration + Projectile.DamageType = DamageClass.Melee; + Projectile.tileCollide = true; + Projectile.light = 0.4f; + Projectile.alpha = 40; + } + + public override void AI() + { + // Frame animation: advance every 4 ticks for subtle flicker + Projectile.frameCounter++; + if (Projectile.frameCounter >= 4) + { + Projectile.frameCounter = 0; + Projectile.frame = (Projectile.frame + 1) % FrameCount; + } + + // Slight acceleration forward for a snappy feel + Projectile.velocity *= 1.02f; + + // Simple fade: become more transparent near end + if (Projectile.timeLeft < 6) + Projectile.alpha += 25; + + // Spawn light dust + if (Main.rand.NextBool(3)) + { + int d = Dust.NewDust(Projectile.position, Projectile.width, Projectile.height, DustID.PurpleTorch, + Projectile.velocity.X * 0.2f, Projectile.velocity.Y * 0.2f, 150, default, 0.9f); + Main.dust[d].noGravity = true; + } + } + + public override bool OnTileCollide(Vector2 oldVelocity) + { + // So beams feel crisp: slight bounce spark then die + Projectile.timeLeft = 5; + Projectile.velocity *= 0.4f; + return false; // don't kill immediately; fade + } + + public override bool PreDraw(ref Color lightColor) + { + Texture2D tex = TextureAssets.Projectile[Type].Value; + int frameHeight = tex.Height / FrameCount; + Rectangle frameRect = new(0, frameHeight * Projectile.frame, tex.Width, frameHeight); + Vector2 origin = new(tex.Width / 2f, frameHeight / 2f); + float baseScale = 1f; + float stretchX = 0.6f + Projectile.velocity.Length() / 30f; // elongate horizontally + Color drawColor = Color.White * (1f - Projectile.alpha / 255f); + + // Slight additive trail effect: draw faint previous frame positions + for (int i = 0; i < 3; i++) + { + Vector2 trailPos = Projectile.Center - Projectile.velocity * i * 0.8f; + float trailAlpha = (0.4f - i * 0.12f) * (1f - Projectile.alpha / 255f); + Main.EntitySpriteDraw(tex, trailPos - Main.screenPosition, frameRect, + drawColor * trailAlpha, Projectile.rotation, origin, + new Vector2(stretchX, baseScale), SpriteEffects.None); + } + + // Main frame (bright) + Main.EntitySpriteDraw(tex, Projectile.Center - Main.screenPosition, frameRect, + drawColor, Projectile.rotation, origin, + new Vector2(stretchX, baseScale), SpriteEffects.None); + return false; + } + } +} diff --git a/Content/Projectiles/BeamAttackWandBeam.png b/Content/Projectiles/BeamAttackWandBeam.png new file mode 100644 index 0000000..fb7a26a Binary files /dev/null and b/Content/Projectiles/BeamAttackWandBeam.png differ diff --git a/Content/Projectiles/ChargedBeamAttackWand.png b/Content/Projectiles/ChargedBeamAttackWand.png new file mode 100644 index 0000000..990744f Binary files /dev/null and b/Content/Projectiles/ChargedBeamAttackWand.png differ diff --git a/Localization/en-US_Mods.ForgottenWawiTech.hjson b/Localization/en-US_Mods.ForgottenWawiTech.hjson index 646ea88..1cabb02 100644 --- a/Localization/en-US_Mods.ForgottenWawiTech.hjson +++ b/Localization/en-US_Mods.ForgottenWawiTech.hjson @@ -234,6 +234,15 @@ Items: { Tooltip: Surprisingly large for a trophy. Especially from an otter. DisplayName: Neptune Trophy Item } + + BeamAttackWand: { + Tooltip: + ''' + A wand that fires several balls of energy in quick succession. + You'll get em next time! Beam attack! + ''' + DisplayName: Beam Staff + } } NPCs: { @@ -327,6 +336,7 @@ NPCs: { Projectiles: { ShatteredPrismBeam.DisplayName: Shattered Prism Beam ShatteredPrismHoldout.DisplayName: Shattered Prism Holdout + BeamAttackWandBeam.DisplayName: Beam Attack Wand Beam } Prefixes: {