add beam wand (thanks lexy)

This commit is contained in:
2025-11-14 22:36:20 +00:00
parent 1219413040
commit f44703376f
12 changed files with 604 additions and 0 deletions
@@ -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<Weapons.Magic.BeamAttackWand>(), 3));
}
}
}
}
@@ -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<BeamAttackWandBeam>();
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<BeamAttackWandPlayer>();
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;
}
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -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
}
}
}
@@ -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
}
}
}
@@ -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
}
}
}
@@ -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
}
}
}
+22
View File
@@ -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<Items.Weapons.Magic.BeamAttackWand>())
{
BeamEmissionTimer = 0;
LastBeamRot = 0f;
}
}
}
}
+94
View File
@@ -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;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB