Refactor and redo Shattered Prism
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
using Terraria;
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
using Terraria.ModLoader;
|
using Terraria.ModLoader;
|
||||||
|
|
||||||
namespace ForgottenWawiTech.Content.Buffs.Debuffs
|
namespace ForgottenWawiTech.Content.Buffs.Debuffs
|
||||||
@@ -10,6 +11,7 @@ namespace ForgottenWawiTech.Content.Buffs.Debuffs
|
|||||||
Main.debuff[Type] = true;
|
Main.debuff[Type] = true;
|
||||||
Main.pvpBuff[Type] = true;
|
Main.pvpBuff[Type] = true;
|
||||||
Main.buffNoSave[Type] = true;
|
Main.buffNoSave[Type] = true;
|
||||||
|
BuffID.Sets.NurseCannotRemoveDebuff[Type] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Update(Player player, ref int buffIndex)
|
public override void Update(Player player, ref int buffIndex)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
|
||||||
|
namespace ForgottenWawiTech.Content.Buffs.Debuffs
|
||||||
|
{
|
||||||
|
public class Waved : ModBuff
|
||||||
|
{
|
||||||
|
public override void SetStaticDefaults()
|
||||||
|
{
|
||||||
|
Main.debuff[Type] = true;
|
||||||
|
Main.pvpBuff[Type] = false;
|
||||||
|
Main.buffNoSave[Type] = true;
|
||||||
|
BuffID.Sets.NurseCannotRemoveDebuff[Type] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Texture => "Terraria/Images/Buff_" + BuffID.Wet;
|
||||||
|
|
||||||
|
public override void Update(Player player, ref int buffIndex)
|
||||||
|
{
|
||||||
|
player.wingTimeMax = (int)(player.wingTimeMax * 0f); // Disable wing flight
|
||||||
|
player.jumpSpeedBoost += 1.0f; // but double jump height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.Audio;
|
||||||
|
using Terraria.DataStructures;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
using ForgottenWawiTech.Content.Projectiles.FragmentedPrism;
|
||||||
|
using ForgottenWawiTech.Content.Rarities;
|
||||||
|
|
||||||
|
namespace ForgottenWawiTech.Content.Items.Weapons.Magic
|
||||||
|
{
|
||||||
|
public class FragmentedPrism : ModItem
|
||||||
|
{
|
||||||
|
public override void SetStaticDefaults()
|
||||||
|
{
|
||||||
|
Item.ResearchUnlockCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetDefaults()
|
||||||
|
{
|
||||||
|
Item.damage = 200;
|
||||||
|
Item.DamageType = DamageClass.Magic;
|
||||||
|
Item.mana = 36;
|
||||||
|
Item.width = 54;
|
||||||
|
Item.height = 54;
|
||||||
|
Item.useTime = 30;
|
||||||
|
Item.useAnimation = 30;
|
||||||
|
Item.useStyle = ItemUseStyleID.Shoot;
|
||||||
|
Item.noMelee = true;
|
||||||
|
Item.channel = true;
|
||||||
|
Item.noUseGraphic = true;
|
||||||
|
Item.knockBack = 3.5f;
|
||||||
|
Item.value = Item.buyPrice(platinum: 2, gold: 50);
|
||||||
|
Item.rare = ModContent.RarityType<SingularityItem>();
|
||||||
|
Item.UseSound = SoundID.Item15;
|
||||||
|
Item.autoReuse = true;
|
||||||
|
Item.shoot = ModContent.ProjectileType<FragmentedPrismController>();
|
||||||
|
Item.shootSpeed = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanUseItem(Player player) => true;
|
||||||
|
|
||||||
|
public override bool Shoot(Player player, Terraria.DataStructures.EntitySource_ItemUse_WithAmmo source, Vector2 position, Vector2 velocity, int type, int damage, float knockBack) => false;
|
||||||
|
|
||||||
|
public override void HoldItem(Player player)
|
||||||
|
{
|
||||||
|
if (player.whoAmI == Main.myPlayer && player.ownedProjectileCounts[Item.shoot] == 0)
|
||||||
|
{
|
||||||
|
IEntitySource spawnSource = player.GetSource_ItemUse(Item);
|
||||||
|
int damage = player.GetWeaponDamage(Item);
|
||||||
|
float knockBack = player.GetWeaponKnockback(Item, Item.knockBack);
|
||||||
|
Projectile.NewProjectile(spawnSource, player.Center, Vector2.Zero, Item.shoot, damage, knockBack, player.whoAmI);
|
||||||
|
}
|
||||||
|
|
||||||
|
Lighting.AddLight(player.Center, new Vector3(0.08f, 0.16f, 0.26f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1,42 +0,0 @@
|
|||||||
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 = 500;
|
|
||||||
Item.shoot = ModContent.ProjectileType<ShatteredPrismHoldout>();
|
|
||||||
Item.shootSpeed = 30f;
|
|
||||||
Item.color = OverrideColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanUseItem(Player player)
|
|
||||||
{
|
|
||||||
return player.ownedProjectileCounts[ModContent.ProjectileType<ShatteredPrismHoldout>()] <= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void AddRecipes()
|
|
||||||
{
|
|
||||||
Recipe recipe = CreateRecipe();
|
|
||||||
// Only add recipe if Calamity Mod is loaded
|
|
||||||
if (ModLoader.TryGetMod("CalamityMod", out Mod calamityMod))
|
|
||||||
{
|
|
||||||
recipe.AddIngredient(ItemID.LastPrism, 1);
|
|
||||||
recipe.AddIngredient(calamityMod.Find<ModItem>("ShatteredCommunity").Type, 1);
|
|
||||||
recipe.AddIngredient<Content.Items.Materials.RGBAlloy>(10);
|
|
||||||
recipe.AddTile<Content.Tiles.CraftingStations.HighTechMarvel>();
|
|
||||||
recipe.Register();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 504 B |
@@ -39,7 +39,7 @@ namespace ForgottenWawiTech.Content.NPCs.OriginalNeptune
|
|||||||
IDEA:
|
IDEA:
|
||||||
Neptune will mostly hover and fly around at high speeds on a ship similiar to the Respawn Ship from SOT.
|
Neptune will mostly hover and fly around at high speeds on a ship similiar to the Respawn Ship from SOT.
|
||||||
He will fire projectiles at high speeds which cause high AOE damage and inflict strong crushing debuffs.
|
He will fire projectiles at high speeds which cause high AOE damage and inflict strong crushing debuffs.
|
||||||
All players will receive a debuff that nearly nullifies wing time while fighting him.
|
All players will receive a debuff (Content.Buffs.Debuffs.Waved) that nullifies wing time while fighting him.
|
||||||
At 50% health, he will "throw" Mr Raccoon off the ship to fight on the ground with the player. His AI will be listed in his file.
|
At 50% health, he will "throw" Mr Raccoon off the ship to fight on the ground with the player. His AI will be listed in his file.
|
||||||
During this time, Neptune will become mildly less aggressive, however still dangerous and on the offensive.
|
During this time, Neptune will become mildly less aggressive, however still dangerous and on the offensive.
|
||||||
At 25% health, he will double his attack speed and movement speed, becoming a frantic danger to players.
|
At 25% health, he will double his attack speed and movement speed, becoming a frantic danger to players.
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
|
||||||
|
namespace ForgottenWawiTech.Content.Projectiles.FragmentedPrism
|
||||||
|
{
|
||||||
|
public class FragmentedPrismBeam : ModProjectile
|
||||||
|
{
|
||||||
|
public override string Texture => "Terraria/Images/Projectile_440";
|
||||||
|
|
||||||
|
public override void SetDefaults()
|
||||||
|
{
|
||||||
|
Projectile.width = 18;
|
||||||
|
Projectile.height = 18;
|
||||||
|
Projectile.friendly = true;
|
||||||
|
Projectile.DamageType = DamageClass.Magic;
|
||||||
|
Projectile.penetrate = -1;
|
||||||
|
Projectile.tileCollide = false;
|
||||||
|
Projectile.ignoreWater = true;
|
||||||
|
Projectile.extraUpdates = 12;
|
||||||
|
Projectile.timeLeft = 26;
|
||||||
|
Projectile.usesLocalNPCImmunity = true;
|
||||||
|
Projectile.localNPCHitCooldown = 8;
|
||||||
|
Projectile.light = 0.55f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void AI()
|
||||||
|
{
|
||||||
|
if (Projectile.velocity.LengthSquared() < 0.001f)
|
||||||
|
{
|
||||||
|
Projectile.Kill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Projectile.rotation = Projectile.velocity.ToRotation();
|
||||||
|
|
||||||
|
float progress = 1f - Projectile.timeLeft / 26f;
|
||||||
|
Projectile.scale = MathHelper.Lerp(0.85f, 1.35f, progress);
|
||||||
|
Lighting.AddLight(Projectile.Center, 0.2f, 0.35f, 0.55f);
|
||||||
|
|
||||||
|
if (Main.rand.NextBool(3))
|
||||||
|
{
|
||||||
|
Vector2 dustPos = Projectile.Center - Projectile.velocity * Main.rand.NextFloat(0.2f, 0.6f);
|
||||||
|
Dust dust = Dust.NewDustPerfect(dustPos, DustID.DungeonSpirit, Projectile.velocity * -0.15f, 150, default, 1.05f);
|
||||||
|
dust.noGravity = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool? CanHitNPC(NPC target)
|
||||||
|
{
|
||||||
|
return base.CanHitNPC(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.Audio;
|
||||||
|
using Terraria.DataStructures;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
|
||||||
|
namespace ForgottenWawiTech.Content.Projectiles.FragmentedPrism
|
||||||
|
{
|
||||||
|
public class FragmentedPrismController : ModProjectile
|
||||||
|
{
|
||||||
|
public const int ShardCount = 5;
|
||||||
|
public const float ChargeTimePerShard = 24f;
|
||||||
|
private const float ChargeDecayRate = 3f;
|
||||||
|
private const int ShardSpawnDelay = 4;
|
||||||
|
|
||||||
|
public static float MaxChargeTime => ChargeTimePerShard * ShardCount;
|
||||||
|
|
||||||
|
private ref float SpawnTimer => ref Projectile.ai[0];
|
||||||
|
private ref float ChargeTimer => ref Projectile.ai[1];
|
||||||
|
private ref float LastFiringCount => ref Projectile.localAI[0];
|
||||||
|
|
||||||
|
public override string Texture => "Terraria/Images/Projectile_0";
|
||||||
|
|
||||||
|
public override void SetDefaults()
|
||||||
|
{
|
||||||
|
Projectile.width = 10;
|
||||||
|
Projectile.height = 10;
|
||||||
|
Projectile.friendly = false;
|
||||||
|
Projectile.hostile = false;
|
||||||
|
Projectile.penetrate = -1;
|
||||||
|
Projectile.tileCollide = false;
|
||||||
|
Projectile.ignoreWater = true;
|
||||||
|
Projectile.hide = true;
|
||||||
|
Projectile.DamageType = DamageClass.Magic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void AI()
|
||||||
|
{
|
||||||
|
Player player = Main.player[Projectile.owner];
|
||||||
|
int itemType = ModContent.ItemType<Items.Weapons.Magic.FragmentedPrism>();
|
||||||
|
if (!player.active || player.dead || player.HeldItem.type != itemType)
|
||||||
|
{
|
||||||
|
Projectile.Kill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.heldProj = Projectile.whoAmI;
|
||||||
|
if (player.channel || ChargeTimer > 0f)
|
||||||
|
{
|
||||||
|
player.itemAnimation = player.itemTime = 2;
|
||||||
|
player.SetDummyItemTime(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Projectile.Center = player.Center;
|
||||||
|
Projectile.timeLeft = 2;
|
||||||
|
|
||||||
|
if (Main.myPlayer == player.whoAmI)
|
||||||
|
{
|
||||||
|
SpawnTimer++;
|
||||||
|
if (SpawnTimer >= ShardSpawnDelay)
|
||||||
|
{
|
||||||
|
SpawnTimer = 0f;
|
||||||
|
TrySpawnShard(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxCharge = MaxChargeTime;
|
||||||
|
if (player.channel)
|
||||||
|
{
|
||||||
|
ChargeTimer = MathHelper.Clamp(ChargeTimer + 1f, 0f, maxCharge);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ChargeTimer = MathHelper.Clamp(ChargeTimer - ChargeDecayRate, 0f, maxCharge);
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentFiringCount = (int)MathHelper.Clamp(ChargeTimer / ChargeTimePerShard, 0f, ShardCount);
|
||||||
|
if (currentFiringCount > LastFiringCount)
|
||||||
|
{
|
||||||
|
SoundEngine.PlaySound(SoundID.Item117 with { Volume = 0.45f, Pitch = 0.3f }, player.Center);
|
||||||
|
Projectile.netUpdate = true;
|
||||||
|
}
|
||||||
|
else if (currentFiringCount < LastFiringCount)
|
||||||
|
{
|
||||||
|
Projectile.netUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LastFiringCount = currentFiringCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnKill(int timeLeft)
|
||||||
|
{
|
||||||
|
if (Main.player[Projectile.owner] is not Player player)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < Main.maxProjectiles; i++)
|
||||||
|
{
|
||||||
|
Projectile other = Main.projectile[i];
|
||||||
|
if (other.active && other.owner == Projectile.owner && other.type == ModContent.ProjectileType<FragmentedPrismShard>())
|
||||||
|
{
|
||||||
|
other.Kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrySpawnShard(Player player)
|
||||||
|
{
|
||||||
|
int shardType = ModContent.ProjectileType<FragmentedPrismShard>();
|
||||||
|
bool[] occupied = new bool[ShardCount];
|
||||||
|
|
||||||
|
for (int i = 0; i < Main.maxProjectiles; i++)
|
||||||
|
{
|
||||||
|
Projectile shard = Main.projectile[i];
|
||||||
|
if (shard.active && shard.owner == player.whoAmI && shard.type == shardType)
|
||||||
|
{
|
||||||
|
int slot = (int)shard.ai[0];
|
||||||
|
if (slot >= 0 && slot < ShardCount)
|
||||||
|
{
|
||||||
|
occupied[slot] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEntitySource source = player.GetSource_ItemUse_WithPotentialAmmo(player.HeldItem, AmmoID.None);
|
||||||
|
bool spawnedAny = false;
|
||||||
|
for (int index = 0; index < ShardCount; index++)
|
||||||
|
{
|
||||||
|
if (occupied[index])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int proj = Projectile.NewProjectile(source, player.Center, Vector2.Zero, shardType, Projectile.damage, Projectile.knockBack, player.whoAmI, index);
|
||||||
|
if (Main.projectile.IndexInRange(proj))
|
||||||
|
{
|
||||||
|
Main.projectile[proj].originalDamage = Projectile.damage;
|
||||||
|
Main.projectile[proj].ai[1] = MathHelper.TwoPi * (index / (float)ShardCount);
|
||||||
|
}
|
||||||
|
spawnedAny = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spawnedAny)
|
||||||
|
{
|
||||||
|
SoundEngine.PlaySound(SoundID.Item4 with { Volume = 0.35f, Pitch = -0.3f }, player.Center);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.Audio;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
|
||||||
|
namespace ForgottenWawiTech.Content.Projectiles.FragmentedPrism
|
||||||
|
{
|
||||||
|
public class FragmentedPrismShard : ModProjectile
|
||||||
|
{
|
||||||
|
private const float OrbitRadius = 72f;
|
||||||
|
private const float BaseOrbitSpeed = 0.04f;
|
||||||
|
private const int FireInterval = 1;
|
||||||
|
private const int VisualSpawnInterval = 18;
|
||||||
|
|
||||||
|
private ref float ShardIndex => ref Projectile.ai[0];
|
||||||
|
private ref float OrbitAngleOffset => ref Projectile.ai[1];
|
||||||
|
private ref float FireTimer => ref Projectile.localAI[0];
|
||||||
|
private ref float VisualTimer => ref Projectile.localAI[1];
|
||||||
|
|
||||||
|
public override void SetStaticDefaults()
|
||||||
|
{
|
||||||
|
Main.projFrames[Type] = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetDefaults()
|
||||||
|
{
|
||||||
|
Projectile.width = 22;
|
||||||
|
Projectile.height = 22;
|
||||||
|
Projectile.friendly = false;
|
||||||
|
Projectile.hostile = false;
|
||||||
|
Projectile.penetrate = -1;
|
||||||
|
Projectile.tileCollide = false;
|
||||||
|
Projectile.ignoreWater = true;
|
||||||
|
Projectile.timeLeft = 2;
|
||||||
|
Projectile.hide = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool? CanDamage() => false;
|
||||||
|
|
||||||
|
public override void AI()
|
||||||
|
{
|
||||||
|
Player player = Main.player[Projectile.owner];
|
||||||
|
if (!player.active || player.dead)
|
||||||
|
{
|
||||||
|
Projectile.Kill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int itemType = ModContent.ItemType<Items.Weapons.Magic.FragmentedPrism>();
|
||||||
|
if (player.HeldItem.type != itemType)
|
||||||
|
{
|
||||||
|
Projectile.Kill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OrbitAngleOffset == 0f && ShardIndex >= 0f)
|
||||||
|
{
|
||||||
|
OrbitAngleOffset = MathHelper.TwoPi * (ShardIndex / FragmentedPrismController.ShardCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetController(player, out Projectile controller))
|
||||||
|
{
|
||||||
|
Projectile.Kill();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float chargeTimer = controller.ai[1];
|
||||||
|
float chargeProgress = FragmentedPrismController.MaxChargeTime > 0f
|
||||||
|
? MathHelper.Clamp(chargeTimer / FragmentedPrismController.MaxChargeTime, 0f, 1f)
|
||||||
|
: 0f;
|
||||||
|
int firingShardCount = (int)MathHelper.Clamp(chargeTimer / FragmentedPrismController.ChargeTimePerShard, 0f, FragmentedPrismController.ShardCount);
|
||||||
|
bool canFire = ShardIndex < firingShardCount;
|
||||||
|
|
||||||
|
float orbitSpeed = BaseOrbitSpeed + chargeProgress * 0.06f;
|
||||||
|
float orbitRadius = OrbitRadius + chargeProgress * 12f;
|
||||||
|
|
||||||
|
float rotation = (float)(Main.GameUpdateCount * orbitSpeed) + OrbitAngleOffset;
|
||||||
|
Vector2 desiredPosition = player.Center + rotation.ToRotationVector2() * orbitRadius;
|
||||||
|
Projectile.Center = desiredPosition;
|
||||||
|
Vector2 aim = Main.MouseWorld - Projectile.Center;
|
||||||
|
if (aim.LengthSquared() < 0.001f)
|
||||||
|
{
|
||||||
|
aim = Vector2.UnitX * player.direction;
|
||||||
|
}
|
||||||
|
Projectile.rotation = aim.ToRotation() + MathHelper.PiOver2;
|
||||||
|
Projectile.spriteDirection = -1;
|
||||||
|
|
||||||
|
if (canFire && Main.netMode != NetmodeID.Server)
|
||||||
|
{
|
||||||
|
VisualTimer++;
|
||||||
|
if (VisualTimer >= VisualSpawnInterval)
|
||||||
|
{
|
||||||
|
VisualTimer = 0f;
|
||||||
|
SpawnBlackHoleVisual(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
VisualTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animate();
|
||||||
|
|
||||||
|
if (Projectile.owner != Main.myPlayer)
|
||||||
|
{
|
||||||
|
Projectile.timeLeft = 10;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canFire)
|
||||||
|
{
|
||||||
|
FireTimer++;
|
||||||
|
if (FireTimer >= FireInterval)
|
||||||
|
{
|
||||||
|
FireTimer = 0f;
|
||||||
|
FireBeam(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FireTimer = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Projectile.timeLeft = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Animate()
|
||||||
|
{
|
||||||
|
Projectile.frameCounter++;
|
||||||
|
if (Projectile.frameCounter >= 6)
|
||||||
|
{
|
||||||
|
Projectile.frameCounter = 0;
|
||||||
|
Projectile.frame = (Projectile.frame + 1) % Main.projFrames[Type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FireBeam(Player player)
|
||||||
|
{
|
||||||
|
Vector2 aimDirection = Main.MouseWorld - Projectile.Center;
|
||||||
|
if (aimDirection.LengthSquared() < 4f)
|
||||||
|
{
|
||||||
|
aimDirection = Vector2.UnitX * player.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
aimDirection.Normalize();
|
||||||
|
Vector2 velocity = aimDirection * 30f;
|
||||||
|
|
||||||
|
int beamType = ModContent.ProjectileType<FragmentedPrismBeam>();
|
||||||
|
int damage = Projectile.originalDamage > 0 ? Projectile.originalDamage : player.GetWeaponDamage(player.HeldItem);
|
||||||
|
float knockBack = player.GetWeaponKnockback(player.HeldItem, player.HeldItem.knockBack);
|
||||||
|
|
||||||
|
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, velocity, beamType, damage, knockBack, player.whoAmI);
|
||||||
|
SoundEngine.PlaySound(SoundID.Item91 with { Volume = 0.2f, Pitch = 0.55f }, Projectile.Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnBlackHoleVisual(Player player)
|
||||||
|
{
|
||||||
|
if (Projectile.owner != Main.myPlayer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int visualType = ModContent.ProjectileType<FragmentedPrismSingularity>();
|
||||||
|
Vector2 toMouse = Main.MouseWorld - Projectile.Center;
|
||||||
|
Vector2 offset = toMouse.SafeNormalize(Vector2.UnitY) * -16f;
|
||||||
|
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center + offset, Vector2.Zero, visualType, 0, 0f, Projectile.owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetController(Player player, out Projectile controller)
|
||||||
|
{
|
||||||
|
int controllerType = ModContent.ProjectileType<FragmentedPrismController>();
|
||||||
|
for (int i = 0; i < Main.maxProjectiles; i++)
|
||||||
|
{
|
||||||
|
Projectile proj = Main.projectile[i];
|
||||||
|
if (proj.active && proj.owner == player.whoAmI && proj.type == controllerType)
|
||||||
|
{
|
||||||
|
controller = proj;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.9 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Terraria;
|
||||||
|
using Terraria.ID;
|
||||||
|
using Terraria.ModLoader;
|
||||||
|
|
||||||
|
namespace ForgottenWawiTech.Content.Projectiles.FragmentedPrism
|
||||||
|
{
|
||||||
|
public class FragmentedPrismSingularity : ModProjectile
|
||||||
|
{
|
||||||
|
|
||||||
|
public override void SetDefaults()
|
||||||
|
{
|
||||||
|
Projectile.width = 12;
|
||||||
|
Projectile.height = 12;
|
||||||
|
Projectile.friendly = false;
|
||||||
|
Projectile.hostile = false;
|
||||||
|
Projectile.penetrate = -1;
|
||||||
|
Projectile.tileCollide = false;
|
||||||
|
Projectile.ignoreWater = true;
|
||||||
|
Projectile.timeLeft = 24;
|
||||||
|
Projectile.alpha = 80;
|
||||||
|
Projectile.hide = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool? CanDamage() => false;
|
||||||
|
|
||||||
|
public override void AI()
|
||||||
|
{
|
||||||
|
float progress = 1f - Projectile.timeLeft / 24f;
|
||||||
|
Projectile.scale = MathHelper.Lerp(0.8f, 1.2f, progress);
|
||||||
|
Projectile.alpha = (int)MathHelper.Lerp(80f, 220f, progress);
|
||||||
|
Lighting.AddLight(Projectile.Center, 0.05f, 0.1f, 0.25f);
|
||||||
|
|
||||||
|
if (Main.rand.NextBool(4))
|
||||||
|
{
|
||||||
|
Dust dust = Dust.NewDustDirect(Projectile.Center - new Vector2(8f), 16, 16, DustID.BlueCrystalShard, 0f, 0f, 150, default, 0.8f);
|
||||||
|
dust.velocity *= 0.2f;
|
||||||
|
dust.noGravity = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -1,364 +0,0 @@
|
|||||||
using ForgottenWawiTech.Content.Projectiles;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Terraria;
|
|
||||||
using Terraria.Enums;
|
|
||||||
using Terraria.GameContent;
|
|
||||||
using Terraria.GameContent.Shaders;
|
|
||||||
using Terraria.Graphics.Effects;
|
|
||||||
using Terraria.ID;
|
|
||||||
using Terraria.ModLoader;
|
|
||||||
|
|
||||||
namespace ForgottenWawiTech.Content.Projectiles
|
|
||||||
{
|
|
||||||
public class ShatteredPrismBeam : ModProjectile
|
|
||||||
{
|
|
||||||
// A helpful math constant for performing beam angling calculations.
|
|
||||||
private const float PiBeamDivisor = MathHelper.Pi / ShatteredPrismHoldout.NumBeams;
|
|
||||||
|
|
||||||
// How much more damage the beams do when the Prism is fully charged. Damage smoothly scales up to this multiplier.
|
|
||||||
private const float MaxDamageMultiplier = 20f;
|
|
||||||
|
|
||||||
// Beams increase their scale from 0 to this value as the Prism charges up.
|
|
||||||
private const float MaxBeamScale = 1.8f;
|
|
||||||
|
|
||||||
// Beams reduce their spread to zero as the Prism charges up. This controls the maximum spread.
|
|
||||||
private const float MaxBeamSpread = 2f;
|
|
||||||
|
|
||||||
// The maximum possible range of the beam. Don't set this too high or it will cause significant lag.
|
|
||||||
private const float MaxBeamLength = 2400f;
|
|
||||||
|
|
||||||
// The width of the beam in pixels for the purposes of tile collision.
|
|
||||||
// This should generally be left at 1, otherwise the beam tends to stop early when touching tiles.
|
|
||||||
private const float BeamTileCollisionWidth = 1f;
|
|
||||||
|
|
||||||
// The width of the beam in pixels for the purposes of entity hitbox collision.
|
|
||||||
// This gets scaled with the beam's scale value, so as the beam visually grows its hitbox gets wider as well.
|
|
||||||
private const float BeamHitboxCollisionWidth = 22f;
|
|
||||||
|
|
||||||
// The number of sample points to use when performing a collision hitscan for the beam.
|
|
||||||
// More points theoretically leads to a higher quality result, but can cause more lag. 3 tends to be enough.
|
|
||||||
private const int NumSamplePoints = 3;
|
|
||||||
|
|
||||||
// How quickly the beam adjusts to sudden changes in length.
|
|
||||||
// Every frame, the beam replaces this ratio of its current length with its intended length.
|
|
||||||
// Generally you shouldn't need to change this.
|
|
||||||
// Setting it too low will make the beam lazily pass through walls before being blocked by them.
|
|
||||||
private const float BeamLengthChangeFactor = 0.75f;
|
|
||||||
|
|
||||||
// The charge percentage required on the host prism for the beam to begin visual effects (e.g. impact dust).
|
|
||||||
private const float VisualEffectThreshold = 0.1f;
|
|
||||||
|
|
||||||
// Each Last Prism beam draws two lasers separately: an inner beam and an outer beam. This controls their opacity.
|
|
||||||
private const float OuterBeamOpacityMultiplier = 0.75f;
|
|
||||||
private const float InnerBeamOpacityMultiplier = 0.1f;
|
|
||||||
|
|
||||||
// The maximum brightness of the light emitted by the beams. Brightness scales from 0 to this value as the Prism's charge increases.
|
|
||||||
private const float BeamLightBrightness = 0f;
|
|
||||||
|
|
||||||
// These variables control the beam's potential coloration.
|
|
||||||
// As a value, hue ranges from 0f to 1f, both of which are pure red. The laser beams vary from 0.57 to 0.75, which winds up being a blue-to-purple gradient.
|
|
||||||
// Saturation ranges from 0f to 1f and controls how greyed out the color is. 0 is fully grayscale, 1 is vibrant, intense color.
|
|
||||||
// Lightness ranges from 0f to 1f and controls how dark or light the color is. 0 is pitch black. 1 is pure white.
|
|
||||||
private const float BeamColorHue = 0f;
|
|
||||||
private const float BeamHueVariance = 0f;
|
|
||||||
private const float BeamColorSaturation = 0f;
|
|
||||||
private const float BeamColorLightness = 0f;
|
|
||||||
|
|
||||||
// This property encloses the internal AI variable Projectile.ai[0]. It makes the code easier to read.
|
|
||||||
private float BeamID {
|
|
||||||
get => Projectile.ai[0];
|
|
||||||
set => Projectile.ai[0] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This property encloses the internal AI variable Projectile.ai[1].
|
|
||||||
private float HostPrismIndex {
|
|
||||||
get => Projectile.ai[1];
|
|
||||||
set => Projectile.ai[1] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This property encloses the internal AI variable Projectile.localAI[1].
|
|
||||||
// Normally, localAI is not synced over the network. This beam manually syncs this variable using SendExtraAI and ReceiveExtraAI.
|
|
||||||
private float BeamLength {
|
|
||||||
get => Projectile.localAI[1];
|
|
||||||
set => Projectile.localAI[1] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetDefaults() {
|
|
||||||
Projectile.width = 18;
|
|
||||||
Projectile.height = 18;
|
|
||||||
Projectile.DamageType = DamageClass.Magic;
|
|
||||||
Projectile.penetrate = -1;
|
|
||||||
Projectile.alpha = 255;
|
|
||||||
// The beam itself still stops on tiles, but its invisible "source" Projectile ignores them.
|
|
||||||
// This prevents the beams from vanishing if the player shoves the Prism into a wall.
|
|
||||||
Projectile.tileCollide = false;
|
|
||||||
|
|
||||||
// Using local NPC immunity allows each beam to strike independently from one another.
|
|
||||||
Projectile.usesLocalNPCImmunity = true;
|
|
||||||
Projectile.localNPCHitCooldown = 10;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send beam length over the network to prevent hitbox-affecting and thus cascading desyncs in multiplayer.
|
|
||||||
public override void SendExtraAI(BinaryWriter writer) => writer.Write(BeamLength);
|
|
||||||
public override void ReceiveExtraAI(BinaryReader reader) => BeamLength = reader.ReadSingle();
|
|
||||||
|
|
||||||
public override void AI() {
|
|
||||||
// If something has gone wrong with either the beam or the host Prism, destroy the beam.
|
|
||||||
Projectile hostPrism = Main.projectile[(int)HostPrismIndex];
|
|
||||||
if (Projectile.type != ModContent.ProjectileType<ShatteredPrismBeam>() || !hostPrism.active || hostPrism.type != ModContent.ProjectileType<ShatteredPrismHoldout>()) {
|
|
||||||
Projectile.Kill();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab some variables from the host Prism.
|
|
||||||
Vector2 hostPrismDir = Vector2.Normalize(hostPrism.velocity);
|
|
||||||
float chargeRatio = MathHelper.Clamp(hostPrism.ai[0] / ShatteredPrismHoldout.MaxCharge, 0f, 1f);
|
|
||||||
|
|
||||||
// Update the beam's damage every frame based on charge and the host Prism's damage.
|
|
||||||
Projectile.damage = (int)(hostPrism.damage * GetDamageMultiplier(chargeRatio));
|
|
||||||
|
|
||||||
// The beam cannot strike enemies until the host Prism is at a certain charge level.
|
|
||||||
Projectile.friendly = hostPrism.ai[0] > ShatteredPrismHoldout.DamageStart;
|
|
||||||
|
|
||||||
// This offset is used to make each individual beam orient differently based on its Beam ID.
|
|
||||||
float beamIdOffset = BeamID - ShatteredPrismHoldout.NumBeams / 2f + 0.5f;
|
|
||||||
float beamSpread;
|
|
||||||
float spinRate;
|
|
||||||
float beamStartSidewaysOffset;
|
|
||||||
float beamStartForwardsOffset;
|
|
||||||
|
|
||||||
// Variables scale smoothly while the host Prism is charging up.
|
|
||||||
if (chargeRatio < 1f) {
|
|
||||||
Projectile.scale = MathHelper.Lerp(0f, MaxBeamScale, chargeRatio);
|
|
||||||
beamSpread = MathHelper.Lerp(MaxBeamSpread, 0f, chargeRatio);
|
|
||||||
beamStartSidewaysOffset = MathHelper.Lerp(20f, 6f, chargeRatio);
|
|
||||||
beamStartForwardsOffset = MathHelper.Lerp(-21f, -17f, chargeRatio);
|
|
||||||
|
|
||||||
// For the first 2/3 of charge time, the opacity scales up from 0% to 40%.
|
|
||||||
// Spin rate increases slowly during this time.
|
|
||||||
if (chargeRatio <= 0.66f) {
|
|
||||||
float phaseRatio = chargeRatio * 1.5f;
|
|
||||||
Projectile.Opacity = MathHelper.Lerp(0f, 0.4f, phaseRatio);
|
|
||||||
spinRate = MathHelper.Lerp(20f, 16f, phaseRatio);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the last 1/3 of charge time, the opacity scales up from 40% to 100%.
|
|
||||||
// Spin rate increases dramatically during this time.
|
|
||||||
else {
|
|
||||||
float phaseRatio = (chargeRatio - 0.66f) * 3f;
|
|
||||||
Projectile.Opacity = MathHelper.Lerp(0.4f, 1f, phaseRatio);
|
|
||||||
spinRate = MathHelper.Lerp(16f, 6f, phaseRatio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the host Prism is already at max charge, don't calculate anything. Just use the max values.
|
|
||||||
else {
|
|
||||||
Projectile.scale = MaxBeamScale;
|
|
||||||
Projectile.Opacity = 1f;
|
|
||||||
beamSpread = 0f;
|
|
||||||
spinRate = 6f;
|
|
||||||
beamStartSidewaysOffset = 6f;
|
|
||||||
beamStartForwardsOffset = -17f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The amount to which the angle changes reduces over time so that the beams look like they are focusing.
|
|
||||||
float deviationAngle = (hostPrism.ai[0] + beamIdOffset * spinRate) / (spinRate * ShatteredPrismHoldout.NumBeams) * MathHelper.TwoPi;
|
|
||||||
|
|
||||||
// This trigonometry calculates where the beam is supposed to be pointing.
|
|
||||||
Vector2 unitRot = Vector2.UnitY.RotatedBy(deviationAngle);
|
|
||||||
Vector2 yVec = new Vector2(4f, beamStartSidewaysOffset);
|
|
||||||
float hostPrismAngle = hostPrism.velocity.ToRotation();
|
|
||||||
Vector2 beamSpanVector = (unitRot * yVec).RotatedBy(hostPrismAngle);
|
|
||||||
float sinusoidYOffset = unitRot.Y * PiBeamDivisor * beamSpread;
|
|
||||||
|
|
||||||
// Calculate the beam's emanating position. Start with the Prism's center.
|
|
||||||
Projectile.Center = hostPrism.Center;
|
|
||||||
// Add a fixed offset to align with the Prism's sprite sheet.
|
|
||||||
Projectile.position += hostPrismDir * 16f + new Vector2(0f, -hostPrism.gfxOffY);
|
|
||||||
// Add the forwards offset, measured in pixels.
|
|
||||||
Projectile.position += hostPrismDir * beamStartForwardsOffset;
|
|
||||||
// Add the sideways offset vector, which is calculated for the current angle of the beam and scales with the beam's sideways offset.
|
|
||||||
Projectile.position += beamSpanVector;
|
|
||||||
|
|
||||||
// Set the beam's velocity to point towards its current spread direction and sanity check it. It should have magnitude 1.
|
|
||||||
Projectile.velocity = hostPrismDir.RotatedBy(sinusoidYOffset);
|
|
||||||
if (Projectile.velocity.HasNaNs() || Projectile.velocity == Vector2.Zero) {
|
|
||||||
Projectile.velocity = -Vector2.UnitY;
|
|
||||||
}
|
|
||||||
Projectile.rotation = Projectile.velocity.ToRotation();
|
|
||||||
|
|
||||||
// Update the beam's length by performing a hitscan collision check.
|
|
||||||
float hitscanBeamLength = PerformBeamHitscan(hostPrism, chargeRatio >= 1f);
|
|
||||||
BeamLength = MathHelper.Lerp(BeamLength, hitscanBeamLength, BeamLengthChangeFactor);
|
|
||||||
|
|
||||||
// This Vector2 stores the beam's hitbox statistics. X = beam length. Y = beam width.
|
|
||||||
Vector2 beamDims = new Vector2(Projectile.velocity.Length() * BeamLength, Projectile.width * Projectile.scale);
|
|
||||||
|
|
||||||
// Only produce dust and cause water ripples if the beam is above a certain charge level.
|
|
||||||
Color beamColor = GetOuterBeamColor();
|
|
||||||
if (chargeRatio >= VisualEffectThreshold) {
|
|
||||||
ProduceBeamDust(beamColor);
|
|
||||||
|
|
||||||
// If the game is rendering (i.e. isn't a dedicated server), make the beam disturb water.
|
|
||||||
if (Main.netMode != NetmodeID.Server) {
|
|
||||||
ProduceWaterRipples(beamDims);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the beam cast light along its length. The brightness of the light scales with the charge.
|
|
||||||
// v3_1 is an unnamed decompiled variable which is the color of the light cast by DelegateMethods.CastLight.
|
|
||||||
DelegateMethods.v3_1 = beamColor.ToVector3() * BeamLightBrightness * chargeRatio;
|
|
||||||
Utils.PlotTileLine(Projectile.Center, Projectile.Center + Projectile.velocity * BeamLength, beamDims.Y, new Utils.TileActionAttempt(DelegateMethods.CastLight));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uses a simple polynomial (x^3) to get sudden but smooth damage increase near the end of the charge-up period.
|
|
||||||
private float GetDamageMultiplier(float chargeRatio) {
|
|
||||||
float f = chargeRatio * chargeRatio * chargeRatio;
|
|
||||||
return MathHelper.Lerp(1f, MaxDamageMultiplier, f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private float PerformBeamHitscan(Projectile prism, bool fullCharge) {
|
|
||||||
// By default, the hitscan interpolation starts at the Projectile's center.
|
|
||||||
// If the host Prism is fully charged, the interpolation starts at the Prism's center instead.
|
|
||||||
Vector2 samplingPoint = Projectile.Center;
|
|
||||||
if (fullCharge) {
|
|
||||||
samplingPoint = prism.Center;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overriding that, if the player shoves the Prism into or through a wall, the interpolation starts at the player's center.
|
|
||||||
// This last part prevents the player from projecting beams through walls under any circumstances.
|
|
||||||
Player player = Main.player[Projectile.owner];
|
|
||||||
if (!Collision.CanHitLine(player.Center, 0, 0, prism.Center, 0, 0)) {
|
|
||||||
samplingPoint = player.Center;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform a laser scan to calculate the correct length of the beam.
|
|
||||||
// Alternatively, if you want the beam to ignore tiles, just set it to be the max beam length with the following line.
|
|
||||||
// return MaxBeamLength;
|
|
||||||
float[] laserScanResults = new float[NumSamplePoints];
|
|
||||||
Collision.LaserScan(samplingPoint, Projectile.velocity, 0 * Projectile.scale, MaxBeamLength, laserScanResults);
|
|
||||||
float averageLengthSample = 0f;
|
|
||||||
for (int i = 0; i < laserScanResults.Length; ++i) {
|
|
||||||
averageLengthSample += laserScanResults[i];
|
|
||||||
}
|
|
||||||
averageLengthSample /= NumSamplePoints;
|
|
||||||
|
|
||||||
return averageLengthSample;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines whether the specified target hitbox is intersecting with the beam.
|
|
||||||
public override bool? Colliding(Rectangle projHitbox, Rectangle targetHitbox) {
|
|
||||||
// If the target is touching the beam's hitbox (which is a small rectangle vaguely overlapping the host Prism), that's good enough.
|
|
||||||
if (projHitbox.Intersects(targetHitbox)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, perform an AABB line collision check to check the whole beam.
|
|
||||||
float _ = float.NaN;
|
|
||||||
Vector2 beamEndPos = Projectile.Center + Projectile.velocity * BeamLength;
|
|
||||||
return Collision.CheckAABBvLineCollision(targetHitbox.TopLeft(), targetHitbox.Size(), Projectile.Center, beamEndPos, BeamHitboxCollisionWidth * Projectile.scale, ref _);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool PreDraw(ref Color lightColor) {
|
|
||||||
// If the beam doesn't have a defined direction, don't draw anything.
|
|
||||||
if (Projectile.velocity == Vector2.Zero) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Texture2D texture = TextureAssets.Projectile[Type].Value;
|
|
||||||
Vector2 centerFloored = Projectile.Center.Floor() + Projectile.velocity * Projectile.scale * 10.5f;
|
|
||||||
Vector2 drawScale = new Vector2(Projectile.scale);
|
|
||||||
|
|
||||||
// Reduce the beam length proportional to its square area to reduce block penetration.
|
|
||||||
float visualBeamLength = BeamLength - 14.5f * Projectile.scale * Projectile.scale;
|
|
||||||
|
|
||||||
DelegateMethods.f_1 = 1f; // f_1 is an unnamed decompiled variable whose function is unknown. Leave it at 1.
|
|
||||||
Vector2 startPosition = centerFloored - Main.screenPosition;
|
|
||||||
Vector2 endPosition = startPosition + Projectile.velocity * visualBeamLength;
|
|
||||||
|
|
||||||
// Draw the outer beam.
|
|
||||||
DrawBeam(Main.spriteBatch, texture, startPosition, endPosition, drawScale, GetOuterBeamColor() * OuterBeamOpacityMultiplier * Projectile.Opacity);
|
|
||||||
|
|
||||||
// Draw the inner beam, which is half size.
|
|
||||||
drawScale *= 0.5f;
|
|
||||||
DrawBeam(Main.spriteBatch, texture, startPosition, endPosition, drawScale, GetInnerBeamColor() * InnerBeamOpacityMultiplier * Projectile.Opacity);
|
|
||||||
|
|
||||||
// Returning false prevents Terraria from trying to draw the Projectile itself.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawBeam(SpriteBatch spriteBatch, Texture2D texture, Vector2 startPosition, Vector2 endPosition, Vector2 drawScale, Color beamColor) {
|
|
||||||
Utils.LaserLineFraming lineFraming = new Utils.LaserLineFraming(DelegateMethods.RainbowLaserDraw);
|
|
||||||
|
|
||||||
// c_1 is an unnamed decompiled variable which is the render color of the beam drawn by DelegateMethods.RainbowLaserDraw.
|
|
||||||
DelegateMethods.c_1 = beamColor;
|
|
||||||
Utils.DrawLaser(spriteBatch, texture, startPosition, endPosition, drawScale, lineFraming);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color GetOuterBeamColor() {
|
|
||||||
// This hue calculation produces a unique color for each beam based on its Beam ID.
|
|
||||||
float hue = (BeamID / ShatteredPrismHoldout.NumBeams) % BeamHueVariance + BeamColorHue;
|
|
||||||
|
|
||||||
// Main.hslToRgb converts Hue, Saturation, Lightness into a Color for general purpose use.
|
|
||||||
Color c = Main.hslToRgb(hue, BeamColorSaturation, BeamColorLightness);
|
|
||||||
|
|
||||||
// Manually reduce the opacity of the color so beams can overlap without completely overwriting each other.
|
|
||||||
c.A = 64;
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Color GetInnerBeamColor() => Color.Black;
|
|
||||||
|
|
||||||
private void ProduceBeamDust(Color beamColor) {
|
|
||||||
// Create one dust per frame a small distance from where the beam ends.
|
|
||||||
const int type = 15;
|
|
||||||
Vector2 endPosition = Projectile.Center + Projectile.velocity * (BeamLength - 14.5f * Projectile.scale);
|
|
||||||
|
|
||||||
// Main.rand.NextBool is used to give a 50/50 chance for the angle to point to the left or right.
|
|
||||||
// This gives the dust a 50/50 chance to fly off on either side of the beam.
|
|
||||||
float angle = Projectile.rotation + (Main.rand.NextBool() ? 1f : -1f) * MathHelper.PiOver2;
|
|
||||||
float startDistance = Main.rand.NextFloat(1f, 1.8f);
|
|
||||||
float scale = Main.rand.NextFloat(0.7f, 1.1f);
|
|
||||||
Vector2 velocity = angle.ToRotationVector2() * startDistance;
|
|
||||||
Dust dust = Dust.NewDustDirect(endPosition, 0, 0, type, velocity.X, velocity.Y, 0, beamColor, scale);
|
|
||||||
dust.color = beamColor;
|
|
||||||
dust.noGravity = true;
|
|
||||||
|
|
||||||
// If the beam is currently large, make the dust faster and larger to match.
|
|
||||||
if (Projectile.scale > 1f) {
|
|
||||||
dust.velocity *= Projectile.scale;
|
|
||||||
dust.scale *= Projectile.scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProduceWaterRipples(Vector2 beamDims) {
|
|
||||||
WaterShaderData shaderData = (WaterShaderData)Filters.Scene["WaterDistortion"].GetShader();
|
|
||||||
|
|
||||||
// A universal time-based sinusoid which updates extremely rapidly. GlobalTime is 0 to 3600, measured in seconds.
|
|
||||||
float waveSine = 0.1f * (float)Math.Sin(Main.GlobalTimeWrappedHourly * 20f);
|
|
||||||
Vector2 ripplePos = Projectile.position + new Vector2(beamDims.X * 0.5f, 0f).RotatedBy(Projectile.rotation);
|
|
||||||
|
|
||||||
// WaveData is encoded as a Color. Not really sure why.
|
|
||||||
Color waveData = new Color(0.5f, 0.1f * Math.Sign(waveSine) + 0.5f, 0f, 1f) * Math.Abs(waveSine);
|
|
||||||
shaderData.QueueRipple(ripplePos, waveData, beamDims, RippleShape.Square, Projectile.rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Automatically iterates through every tile the laser is overlapping to cut grass at all those locations.
|
|
||||||
public override void CutTiles() {
|
|
||||||
// tilecut_0 is an unnamed decompiled variable which tells CutTiles how the tiles are being cut (in this case, via a Projectile).
|
|
||||||
DelegateMethods.tilecut_0 = TileCuttingContext.AttackProjectile;
|
|
||||||
Utils.TileActionAttempt cut = new Utils.TileActionAttempt(DelegateMethods.CutTiles);
|
|
||||||
Vector2 beamStartPos = Projectile.Center;
|
|
||||||
Vector2 beamEndPos = beamStartPos + Projectile.velocity * BeamLength;
|
|
||||||
|
|
||||||
// PlotTileLine is a function which performs the specified action to all tiles along a drawn line, with a specified width.
|
|
||||||
// In this case, it is cutting all tiles which can be destroyed by Projectiles, for example grass or pots.
|
|
||||||
Utils.PlotTileLine(beamStartPos, beamEndPos, Projectile.width * Projectile.scale, cut);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
using ForgottenWawiTech.Content.Items.Weapons.Magic;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using Microsoft.Xna.Framework.Graphics;
|
|
||||||
using Terraria;
|
|
||||||
using Terraria.Audio;
|
|
||||||
using Terraria.GameContent;
|
|
||||||
using Terraria.ID;
|
|
||||||
using Terraria.ModLoader;
|
|
||||||
|
|
||||||
namespace ForgottenWawiTech.Content.Projectiles
|
|
||||||
{
|
|
||||||
public class ShatteredPrismHoldout : ModProjectile
|
|
||||||
{
|
|
||||||
public override string Texture => "Terraria/Images/Projectile_" + ProjectileID.LastPrism;
|
|
||||||
|
|
||||||
// The vanilla Last Prism is an animated item with 5 frames of animation. We copy that here.
|
|
||||||
private const int NumAnimationFrames = 5;
|
|
||||||
|
|
||||||
// This controls how many individual beams are fired by the Prism.
|
|
||||||
public const int NumBeams = 10;
|
|
||||||
|
|
||||||
// This value controls how many frames it takes for the Prism to reach "max charge". 60 frames = 1 second.
|
|
||||||
public const float MaxCharge = 180f;
|
|
||||||
|
|
||||||
// This value controls how many frames it takes for the beams to begin dealing damage. Before then they can't hit anything.
|
|
||||||
public const float DamageStart = 30f;
|
|
||||||
|
|
||||||
// This value controls how sluggish the Prism turns while being used. Vanilla Last Prism is 0.08f.
|
|
||||||
// Higher values make the Prism turn faster.
|
|
||||||
private const float AimResponsiveness = 0.5f;
|
|
||||||
|
|
||||||
// This value controls how frequently the Prism emits sound once it's firing.
|
|
||||||
private const int SoundInterval = 20;
|
|
||||||
|
|
||||||
// These values place caps on the mana consumption rate of the Prism.
|
|
||||||
// When first used, the Prism consumes mana once every MaxManaConsumptionDelay frames.
|
|
||||||
// Every time mana is consumed, the pace becomes one frame faster, meaning mana consumption smoothly increases.
|
|
||||||
// When capped out, the Prism consumes mana once every MinManaConsumptionDelay frames.
|
|
||||||
private const float MaxManaConsumptionDelay = 15f;
|
|
||||||
private const float MinManaConsumptionDelay = 5f;
|
|
||||||
|
|
||||||
// This property encloses the internal AI variable Projectile.ai[0]. It makes the code easier to read.
|
|
||||||
private float FrameCounter {
|
|
||||||
get => Projectile.ai[0];
|
|
||||||
set => Projectile.ai[0] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This property encloses the internal AI variable Projectile.ai[1].
|
|
||||||
private float NextManaFrame {
|
|
||||||
get => Projectile.ai[1];
|
|
||||||
set => Projectile.ai[1] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This property encloses the internal AI variable Projectile.localAI[0].
|
|
||||||
// localAI is not automatically synced over the network, but that does not cause any problems in this case.
|
|
||||||
private float ManaConsumptionRate {
|
|
||||||
get => Projectile.localAI[0];
|
|
||||||
set => Projectile.localAI[0] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetStaticDefaults() {
|
|
||||||
Main.projFrames[Projectile.type] = NumAnimationFrames;
|
|
||||||
|
|
||||||
// Signals to Terraria that this Projectile requires a unique identifier beyond its index in the Projectile array.
|
|
||||||
// This prevents the issue with the vanilla Last Prism where the beams are invisible in multiplayer.
|
|
||||||
ProjectileID.Sets.NeedsUUID[Projectile.type] = true;
|
|
||||||
|
|
||||||
// Prevents jitter when stepping up and down blocks and half blocks
|
|
||||||
ProjectileID.Sets.HeldProjDoesNotUsePlayerGfxOffY[Type] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetDefaults() {
|
|
||||||
// Use CloneDefaults to clone all basic Projectile statistics from the vanilla Last Prism.
|
|
||||||
Projectile.CloneDefaults(ProjectileID.LastPrism);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Held projectiles should set this to false otherwise they will deal contact damage.
|
|
||||||
public override bool? CanDamage() => false;
|
|
||||||
|
|
||||||
public override void AI() {
|
|
||||||
Player player = Main.player[Projectile.owner];
|
|
||||||
Vector2 rrp = player.RotatedRelativePoint(player.MountedCenter, true);
|
|
||||||
|
|
||||||
// Update the Prism's damage every frame so that it is dynamically affected by Mana Sickness.
|
|
||||||
UpdateDamageForManaSickness(player);
|
|
||||||
|
|
||||||
// Update the frame counter.
|
|
||||||
FrameCounter += 1f;
|
|
||||||
|
|
||||||
// Update Projectile visuals and sound.
|
|
||||||
UpdateAnimation();
|
|
||||||
PlaySounds();
|
|
||||||
|
|
||||||
// Update the Prism's position in the world and relevant variables of the player holding it.
|
|
||||||
UpdatePlayerVisuals(player, rrp);
|
|
||||||
|
|
||||||
// Update the Prism's behavior: project beams on frame 1, consume mana, and despawn if out of mana.
|
|
||||||
if (Projectile.owner == Main.myPlayer) {
|
|
||||||
// Slightly re-aim the Prism every frame so that it gradually sweeps to point towards the mouse.
|
|
||||||
UpdateAim(rrp, player.HeldItem.shootSpeed);
|
|
||||||
|
|
||||||
// player.CheckMana returns true if the mana cost can be paid. Since the second argument is true, the mana is actually consumed.
|
|
||||||
// If mana shouldn't consumed this frame, the || operator short-circuits its evaluation player.CheckMana never executes.
|
|
||||||
bool manaIsAvailable = !ShouldConsumeMana() || player.CheckMana(player.HeldItem.mana, true, false);
|
|
||||||
|
|
||||||
// The Prism immediately stops functioning if the player is Cursed (player.noItems) or "Crowd Controlled", e.g. the Frozen debuff.
|
|
||||||
// player.channel indicates whether the player is still holding down the mouse button to use the item.
|
|
||||||
bool stillInUse = player.channel && manaIsAvailable && !player.noItems && !player.CCed;
|
|
||||||
|
|
||||||
// Spawn in the Prism's lasers on the first frame if the player is capable of using the item.
|
|
||||||
if (stillInUse && FrameCounter == 1f) {
|
|
||||||
FireBeams();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the Prism cannot continue to be used, then destroy it immediately.
|
|
||||||
else if (!stillInUse) {
|
|
||||||
Projectile.Kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This ensures that the Prism never times out while in use.
|
|
||||||
Projectile.timeLeft = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateDamageForManaSickness(Player player) {
|
|
||||||
Projectile.damage = (int)player.GetDamage(DamageClass.Magic).ApplyTo(player.HeldItem.damage);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAnimation() {
|
|
||||||
Projectile.frameCounter++;
|
|
||||||
|
|
||||||
// As the Prism charges up and focuses the beams, its animation plays faster.
|
|
||||||
int framesPerAnimationUpdate = FrameCounter >= MaxCharge ? 2 : FrameCounter >= (MaxCharge * 0.66f) ? 3 : 4;
|
|
||||||
|
|
||||||
// If necessary, change which specific frame of the animation is displayed.
|
|
||||||
if (Projectile.frameCounter >= framesPerAnimationUpdate) {
|
|
||||||
Projectile.frameCounter = 0;
|
|
||||||
if (++Projectile.frame >= NumAnimationFrames) {
|
|
||||||
Projectile.frame = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PlaySounds() {
|
|
||||||
// The Prism makes sound intermittently while in use, using the vanilla Projectile variable soundDelay.
|
|
||||||
if (Projectile.soundDelay <= 0) {
|
|
||||||
Projectile.soundDelay = SoundInterval;
|
|
||||||
|
|
||||||
// On the very first frame, the sound playing is skipped. This way it doesn't overlap the starting hiss sound.
|
|
||||||
if (FrameCounter > 1f) {
|
|
||||||
SoundEngine.PlaySound(SoundID.Item15, Projectile.position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePlayerVisuals(Player player, Vector2 playerHandPos) {
|
|
||||||
// Place the Prism directly into the player's hand at all times.
|
|
||||||
Projectile.Center = playerHandPos;
|
|
||||||
// The beams emit from the tip of the Prism, not the side. As such, rotate the sprite by pi/2 (90 degrees).
|
|
||||||
Projectile.rotation = Projectile.velocity.ToRotation() + MathHelper.PiOver2;
|
|
||||||
Projectile.spriteDirection = Projectile.direction;
|
|
||||||
|
|
||||||
// The Prism is a holdout Projectile, so change the player's variables to reflect that.
|
|
||||||
// Constantly resetting player.itemTime and player.itemAnimation prevents the player from switching items or doing anything else.
|
|
||||||
player.ChangeDir(Projectile.direction);
|
|
||||||
player.heldProj = Projectile.whoAmI;
|
|
||||||
player.itemTime = 2;
|
|
||||||
player.itemAnimation = 2;
|
|
||||||
|
|
||||||
// If you do not multiply by Projectile.direction, the player's hand will point the wrong direction while facing left.
|
|
||||||
player.itemRotation = (Projectile.velocity * Projectile.direction).ToRotation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ShouldConsumeMana() {
|
|
||||||
// If the mana consumption timer hasn't been initialized yet, initialize it and consume mana on frame 1.
|
|
||||||
if (ManaConsumptionRate == 0f) {
|
|
||||||
NextManaFrame = ManaConsumptionRate = MaxManaConsumptionDelay;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should mana be consumed this frame?
|
|
||||||
bool consume = FrameCounter == NextManaFrame;
|
|
||||||
|
|
||||||
// If mana is being consumed this frame, update the rate of mana consumption and write down the next frame mana will be consumed.
|
|
||||||
if (consume) {
|
|
||||||
// MathHelper.Clamp(X,A,B) guarantees that A <= X <= B. If X is outside the range, it will be set to A or B accordingly.
|
|
||||||
ManaConsumptionRate = MathHelper.Clamp(ManaConsumptionRate - 1f, MinManaConsumptionDelay, MaxManaConsumptionDelay);
|
|
||||||
NextManaFrame += ManaConsumptionRate;
|
|
||||||
}
|
|
||||||
return consume;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAim(Vector2 source, float speed) {
|
|
||||||
// Get the player's current aiming direction as a normalized vector.
|
|
||||||
Vector2 aim = Vector2.Normalize(Main.MouseWorld - source);
|
|
||||||
if (aim.HasNaNs()) {
|
|
||||||
aim = -Vector2.UnitY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change a portion of the Prism's current velocity so that it points to the mouse. This gives smooth movement over time.
|
|
||||||
aim = Vector2.Normalize(Vector2.Lerp(Vector2.Normalize(Projectile.velocity), aim, AimResponsiveness));
|
|
||||||
aim *= speed;
|
|
||||||
|
|
||||||
if (aim != Projectile.velocity) {
|
|
||||||
Projectile.netUpdate = true;
|
|
||||||
}
|
|
||||||
Projectile.velocity = aim;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FireBeams() {
|
|
||||||
// If for some reason the beam velocity can't be correctly normalized, set it to a default value.
|
|
||||||
Vector2 beamVelocity = Vector2.Normalize(Projectile.velocity);
|
|
||||||
if (beamVelocity.HasNaNs()) {
|
|
||||||
beamVelocity = -Vector2.UnitY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This UUID will be the same between all players in multiplayer, ensuring that the beams are properly anchored on the Prism on everyone's screen.
|
|
||||||
int uuid = Projectile.GetByUUID(Projectile.owner, Projectile.whoAmI);
|
|
||||||
|
|
||||||
int damage = Projectile.damage;
|
|
||||||
float knockback = Projectile.knockBack;
|
|
||||||
for (int b = 0; b < NumBeams; ++b) {
|
|
||||||
Projectile.NewProjectile(Projectile.GetSource_FromThis(), Projectile.Center, beamVelocity, ModContent.ProjectileType<ShatteredPrismBeam>(), damage, knockback, Projectile.owner, b, uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// After creating the beams, mark the Prism as having an important network event. This will make Terraria sync its data to other players ASAP.
|
|
||||||
Projectile.netUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Because the Prism is a holdout Projectile and stays glued to its user, it needs custom drawcode.
|
|
||||||
public override bool PreDraw(ref Color lightColor) {
|
|
||||||
SpriteEffects effects = Projectile.spriteDirection == -1 ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
|
|
||||||
Texture2D texture = TextureAssets.Projectile[Type].Value;
|
|
||||||
int frameHeight = texture.Height / Main.projFrames[Projectile.type];
|
|
||||||
int spriteSheetOffset = frameHeight * Projectile.frame;
|
|
||||||
Vector2 sheetInsertPosition = (Projectile.Center + Vector2.UnitY * Projectile.gfxOffY - Main.screenPosition).Floor();
|
|
||||||
|
|
||||||
// The Prism is always at full brightness, regardless of the surrounding light. This is equivalent to it being its own glowmask.
|
|
||||||
// It is drawn in a non-white color to distinguish it from the vanilla Last Prism.
|
|
||||||
Color drawColor = ShatteredPrism.OverrideColor;
|
|
||||||
Main.EntitySpriteDraw(texture, sheetInsertPosition, new Rectangle?(new Rectangle(0, spriteSheetOffset, texture.Width, frameHeight)), drawColor, Projectile.rotation, new Vector2(texture.Width / 2f, frameHeight / 2f), Projectile.scale, effects, 0f);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -114,6 +114,16 @@ Items: {
|
|||||||
'''
|
'''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FragmentedPrism: {
|
||||||
|
DisplayName: Fragmented Prism
|
||||||
|
Tooltip:
|
||||||
|
'''
|
||||||
|
Channels five orbiting shards that unleash beams at your cursor.
|
||||||
|
Stack the streams for brutal single-target damage or fan them out for coverage.
|
||||||
|
Each shard leaves a cosmetic singularity in its wake.
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
|
||||||
DevInfiniteMana: {
|
DevInfiniteMana: {
|
||||||
DisplayName: Infinite Mana
|
DisplayName: Infinite Mana
|
||||||
Tooltip:
|
Tooltip:
|
||||||
@@ -1078,6 +1088,10 @@ Projectiles: {
|
|||||||
TridentOfTheSunkenShot.DisplayName: Trident Of The Sunken Shot
|
TridentOfTheSunkenShot.DisplayName: Trident Of The Sunken Shot
|
||||||
TridentOfTheSunkenHoldout.DisplayName: Trident Of The Sunken Holdout
|
TridentOfTheSunkenHoldout.DisplayName: Trident Of The Sunken Holdout
|
||||||
TridentOfTheSunkenBlast.DisplayName: Trident Of The Sunken Blast
|
TridentOfTheSunkenBlast.DisplayName: Trident Of The Sunken Blast
|
||||||
|
FragmentedPrismBeam.DisplayName: Fragmented Prism Beam
|
||||||
|
FragmentedPrismController.DisplayName: Fragmented Prism Controller
|
||||||
|
FragmentedPrismShard.DisplayName: Fragmented Prism Shard
|
||||||
|
FragmentedPrismSingularity.DisplayName: Fragmented Prism Singularity
|
||||||
}
|
}
|
||||||
|
|
||||||
Prefixes: {
|
Prefixes: {
|
||||||
@@ -1095,4 +1109,9 @@ Buffs: {
|
|||||||
DisplayName: Thermal Mark
|
DisplayName: Thermal Mark
|
||||||
Description: You are tagged by Avali thermal drones, making you vulnerable.
|
Description: You are tagged by Avali thermal drones, making you vulnerable.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Waved: {
|
||||||
|
Description: Mods.ForgottenWawiTech.Buffs.Waved.Description
|
||||||
|
DisplayName: Waved
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ I personally think only the Avalian Trader should be considered until we have a
|
|||||||
- A circular contained singularity that functions as an axe, hammer and pickaxe (on toggle). Maybe some glowing effects to show the singularity contained within?
|
- A circular contained singularity that functions as an axe, hammer and pickaxe (on toggle). Maybe some glowing effects to show the singularity contained within?
|
||||||
- [ ] Singularity Multitool
|
- [ ] Singularity Multitool
|
||||||
- A high-tech multitool infused with singularity energy, capable of performing various functions such as mining, chopping, and hammering with enhanced efficiency.
|
- A high-tech multitool infused with singularity energy, capable of performing various functions such as mining, chopping, and hammering with enhanced efficiency.
|
||||||
|
- [ ] Fragmented Prism
|
||||||
|
- [x] Item icon for the fractured prism focus.
|
||||||
|
- [x] Orbiting shard projectile sprite (`FragmentedPrismShard`).
|
||||||
|
- [ ] Beam visual sprite (`FragmentedPrismBeam`).
|
||||||
|
- [x] Cosmetic singularity sprite (`FragmentedPrismSingularity`).
|
||||||
- [ ] Trident Of The Sunken
|
- [ ] Trident Of The Sunken
|
||||||
- [x] Item icon for the charged trident staff.
|
- [x] Item icon for the charged trident staff.
|
||||||
- [x] Holdout sprite (`TridentOfTheSunkenHoldout`).
|
- [x] Holdout sprite (`TridentOfTheSunkenHoldout`).
|
||||||
|
|||||||
Reference in New Issue
Block a user