Files
Wizardry/src/main/java/electroblob/wizardry/item/ItemWand.java
T
WinDanesz fe59a3c377 feat: Add configurable condenser amount multiplier
- Add condenserAmountMultiplier setting with default value 1.0
2025-08-31 23:00:01 +02:00

937 lines
37 KiB
Java

package electroblob.wizardry.item;
import com.google.common.collect.Multimap;
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.client.DrawingUtils;
import electroblob.wizardry.constants.Constants;
import electroblob.wizardry.constants.Element;
import electroblob.wizardry.constants.Tier;
import electroblob.wizardry.data.SpellGlyphData;
import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.entity.living.ISummonedCreature;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.event.SpellCastEvent.Source;
import electroblob.wizardry.packet.PacketCastSpell;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.registry.*;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.*;
import electroblob.wizardry.util.ParticleBuilder.Type;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.SharedMonsterAttributes;
import net.minecraft.entity.ai.attributes.AttributeModifier;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.EntityEquipmentSlot;
import net.minecraft.inventory.Slot;
import net.minecraft.item.EnumAction;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.*;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.RayTraceResult;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.player.AttackEntityEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* This class is (literally) where the magic happens! All of wizardry's wand items are instances of this class. As of
* wizardry 4.2, it is no longer necessary to extend {@code ItemWand} thanks to {@link ISpellCastingItem}, though
* extending {@code ItemWand} may still be more appropriate for items using the same casting implementation.
* <p></p>
* This class handles spell casting as follows:
* <p></p>
* - {@code onItemRightClick} is where non-continuous spells are cast, and it sets the item in use for continuous spells<br>
* - {@code onUsingTick} does the casting for continuous spells<br>
* - {@code onUpdate} deals with the cooldowns for the spells<br>
* <br>
* See {@link ISpellCastingItem} for more detail on the {@code canCast(...)} and {@code cast(...)} methods.<br>
* See {@link WandHelper} for everything related to wand NBT.
*
* @since Wizardry 1.0
*/
@Mod.EventBusSubscriber
public class ItemWand extends Item implements IWorkbenchItem, ISpellCastingItem, IManaStoringItem {
/** The number of spell slots a wand has with no attunement upgrades applied. */
public static int BASE_SPELL_SLOTS;
/** The number of ticks between each time a continuous spell is added to the player's recently-cast spells. */
private static final int CONTINUOUS_TRACKING_INTERVAL = 20;
/** The increase in progression for casting spells of the matching element. */
private static final float ELEMENTAL_PROGRESSION_MODIFIER = 1.2f;
/** The increase in progression for casting an undiscovered spell (can only happen once per spell for each player). */
private static final float DISCOVERY_PROGRESSION_MODIFIER = 5f;
/** The increase in progression for tiers that the player has already reached. */
private static final float SECOND_TIME_PROGRESSION_MODIFIER = 1.5f;
/** The fraction of progression lost when all recently-cast spells are the same as the one being cast. */
private static final float MAX_PROGRESSION_REDUCTION = 0.75f;
public Tier tier;
public Element element;
public ItemWand(Tier tier, Element element){
super();
setMaxStackSize(1);
setCreativeTab(WizardryTabs.GEAR);
this.tier = tier;
this.element = element;
setMaxDamage(this.tier.maxCharge);
WizardryRecipes.addToManaFlaskCharging(this);
// TODO: Hook to allow addon devs to have this override apply to their own animations
addPropertyOverride(new ResourceLocation("pointing"),
(s, w, e) -> e != null && e.getActiveItemStack() == s
&& (s.getItemUseAction() == SpellActions.POINT
|| s.getItemUseAction() == SpellActions.POINT_UP
|| s.getItemUseAction() == SpellActions.POINT_DOWN
|| s.getItemUseAction() == SpellActions.GRAPPLE
|| s.getItemUseAction() == SpellActions.SUMMON) ? 1 : 0);
}
@Override
public Spell getCurrentSpell(ItemStack stack){
return WandHelper.getCurrentSpell(stack);
}
@Override
public Spell getNextSpell(ItemStack stack){
return WandHelper.getNextSpell(stack);
}
@Override
public Spell getPreviousSpell(ItemStack stack){
return WandHelper.getPreviousSpell(stack);
}
@Override
public Spell[] getSpells(ItemStack stack){
return WandHelper.getSpells(stack);
}
@Override
public void selectNextSpell(ItemStack stack){
WandHelper.selectNextSpell(stack);
}
@Override
public void selectPreviousSpell(ItemStack stack){
WandHelper.selectPreviousSpell(stack);
}
@Override
public boolean selectSpell(ItemStack stack, int index){
return WandHelper.selectSpell(stack, index);
}
@Override
public int getCurrentCooldown(ItemStack stack){
return WandHelper.getCurrentCooldown(stack);
}
@Override
public int getCurrentMaxCooldown(ItemStack stack){
return WandHelper.getCurrentMaxCooldown(stack);
}
@Override
public boolean showSpellHUD(EntityPlayer player, ItemStack stack){
return true;
}
@Override
public boolean showTooltip(ItemStack stack){
return true;
}
/** Does nothing, use {@link ItemWand#setMana(ItemStack, int)} to modify wand mana. */
@Override
public void setDamage(ItemStack stack, int damage){
// Overridden to do nothing to stop repair things from 'repairing' the mana in a wand
}
@Override
public void setMana(ItemStack stack, int mana){
// Using super (which can only be done from in here) bypasses the above override
super.setDamage(stack, getManaCapacity(stack) - mana);
}
@Override
public int getMana(ItemStack stack){
return getManaCapacity(stack) - getDamage(stack);
}
@Override
public int getManaCapacity(ItemStack stack){
return this.getMaxDamage(stack);
}
@Override
@SideOnly(Side.CLIENT)
public boolean isFull3D(){
return true;
}
@Override
@SideOnly(Side.CLIENT)
public net.minecraft.client.gui.FontRenderer getFontRenderer(ItemStack stack){
return Wizardry.proxy.getFontRenderer(stack);
}
@Override
public boolean isEnchantable(ItemStack stack){
return false;
}
@Override
public boolean isBookEnchantable(ItemStack stack, ItemStack book){
return false;
}
@Override
public boolean hasEffect(ItemStack stack){
return !Wizardry.settings.legacyWandLevelling && this.tier.level < Tier.MASTER.level
&& WandHelper.getProgression(stack) >= tier.next().getProgression();
}
@Override
public int getRGBDurabilityForDisplay(ItemStack stack){
return DrawingUtils.mix(0xff8bfe, 0x8e2ee4, (float)getDurabilityForDisplay(stack));
}
// Max damage is modifiable with upgrades.
@Override
public int getMaxDamage(ItemStack stack){
// + 0.5f corrects small float errors rounding down
return (int)(super.getMaxDamage(stack) * (1.0f + Constants.STORAGE_INCREASE_PER_LEVEL
* WandHelper.getUpgradeLevel(stack, WizardryItems.storage_upgrade)) + 0.5f);
}
@Override
public void onCreated(ItemStack stack, World worldIn, EntityPlayer playerIn){
setMana(stack, 0); // Wands are empty when first crafted
}
@Override
public void onUpdate(ItemStack stack, World world, Entity entity, int slot, boolean isHeldInMainhand){
boolean isHeld = isHeldInMainhand || entity instanceof EntityLivingBase && ItemStack.areItemStacksEqual(stack, ((EntityLivingBase) entity).getHeldItemOffhand());
// If Wizardry.settings.wandsMustBeHeldToDecrementCooldown is false, the cooldowns will be decremented.
// If Wizardry.settings.wandsMustBeHeldToDecrementCooldown is true and isHeld is true, the cooldowns will also be decremented.
// If Wizardry.settings.wandsMustBeHeldToDecrementCooldown is true and isHeld is false, the cooldowns will not be decremented.
if (!Wizardry.settings.wandsMustBeHeldToDecrementCooldown || isHeld) {
WandHelper.decrementCooldowns(stack);
}
// Decrements wand damage (increases mana) every 1.5 seconds if it has a condenser upgrade
if(!world.isRemote && !this.isManaFull(stack) && world.getTotalWorldTime() % Constants.CONDENSER_TICK_INTERVAL == 0){
// If the upgrade level is 0, this does nothing anyway.
int baseAmount = WandHelper.getUpgradeLevel(stack, WizardryItems.condenser_upgrade);
int amount = (int)(baseAmount * Wizardry.settings.condenserAmountMultiplier);
this.rechargeMana(stack, amount);
}
}
@Override
public Multimap<String, AttributeModifier> getAttributeModifiers(EntityEquipmentSlot slot, ItemStack stack){
Multimap<String, AttributeModifier> multimap = super.getAttributeModifiers(slot, stack);
if(slot == EntityEquipmentSlot.MAINHAND){
int level = WandHelper.getUpgradeLevel(stack, WizardryItems.melee_upgrade);
// This check doesn't affect the damage output, but it does stop a blank line from appearing in the tooltip.
if(level > 0 && !this.isManaEmpty(stack)){
multimap.put(SharedMonsterAttributes.ATTACK_DAMAGE.getName(),
new AttributeModifier(ATTACK_DAMAGE_MODIFIER, "Melee upgrade modifier", 2 * level, 0));
multimap.put(SharedMonsterAttributes.ATTACK_SPEED.getName(), new AttributeModifier(ATTACK_SPEED_MODIFIER, "Melee upgrade modifier", -2.4000000953674316D, 0));
}
}
return multimap;
}
@Override
public boolean hitEntity(ItemStack stack, EntityLivingBase target, EntityLivingBase wielder){
int level = WandHelper.getUpgradeLevel(stack, WizardryItems.melee_upgrade);
int mana = this.getMana(stack);
if(level > 0 && mana > 0) this.consumeMana(stack, level * 4, wielder);
return true;
}
@Override
public boolean canDestroyBlockInCreative(World world, BlockPos pos, ItemStack stack, EntityPlayer player){
return WandHelper.getUpgradeLevel(stack, WizardryItems.melee_upgrade) == 0;
}
// A proper hook was introduced for this in Forge build 14.23.5.2805 - Hallelujah, finally!
// The discussion about this was quite interesting, see the following:
// https://github.com/TeamTwilight/twilightforest/blob/1.12.x/src/main/java/twilightforest/item/ItemTFScepterLifeDrain.java
// https://github.com/MinecraftForge/MinecraftForge/pull/4834
// Among the things mentioned were that it can be 'fixed' by doing the exact same hacks that I did, and that
// returning a result of PASS rather than SUCCESS from onItemRightClick also solves the problem (not sure why
// though, and again it's not a perfect solution)
// Edit: It seems that the hacky fix in previous versions actually introduced a wand duplication bug... oops
@Override
public boolean canContinueUsing(ItemStack oldStack, ItemStack newStack){
// Ignore durability changes
if(ItemStack.areItemsEqualIgnoreDurability(oldStack, newStack)) return true;
return super.canContinueUsing(oldStack, newStack);
}
@Override
public boolean shouldCauseBlockBreakReset(ItemStack oldStack, ItemStack newStack){
// Ignore durability changes
if(ItemStack.areItemsEqualIgnoreDurability(oldStack, newStack)) return false;
return super.shouldCauseBlockBreakReset(oldStack, newStack);
}
@Override
// Only called client-side
// This method is always called on the item in oldStack, meaning that oldStack.getItem() == this
public boolean shouldCauseReequipAnimation(ItemStack oldStack, ItemStack newStack, boolean slotChanged){
// This method does some VERY strange things! Despite its name, it also seems to affect the updating of NBT...
if(!oldStack.isEmpty() || !newStack.isEmpty()){
// We only care about the situation where we specifically want the animation NOT to play.
if(oldStack.getItem() == newStack.getItem() && !slotChanged && oldStack.getItem() instanceof ItemWand
&& newStack.getItem() instanceof ItemWand
&& WandHelper.getCurrentSpell(oldStack) == WandHelper.getCurrentSpell(newStack))
return false;
}
return super.shouldCauseReequipAnimation(oldStack, newStack, slotChanged);
}
@Override
public EnumAction getItemUseAction(ItemStack itemstack){
return WandHelper.getCurrentSpell(itemstack).action;
}
@Override
public int getMaxItemUseDuration(ItemStack stack){
return 72000;
}
@SideOnly(Side.CLIENT)
@Override
public void addInformation(ItemStack stack, World world, List<String> text, net.minecraft.client.util.ITooltipFlag advanced){
EntityPlayer player = net.minecraft.client.Minecraft.getMinecraft().player;
if (player == null) { return; }
// +0.5f is necessary due to the error in the way floats are calculated.
if(element != null) text.add(Wizardry.proxy.translate("item." + Wizardry.MODID + ":wand.buff",
new Style().setColor(TextFormatting.DARK_GRAY),
(int)((tier.level + 1) * Constants.POTENCY_INCREASE_PER_TIER * 100 + 0.5f), element.getDisplayName()));
Spell spell = WandHelper.getCurrentSpell(stack);
boolean discovered = true;
if(Wizardry.settings.discoveryMode && !player.isCreative() && WizardData.get(player) != null
&& !WizardData.get(player).hasSpellBeenDiscovered(spell)){
discovered = false;
}
text.add(Wizardry.proxy.translate("item." + Wizardry.MODID + ":wand.spell", new Style().setColor(TextFormatting.GRAY),
discovered ? spell.getDisplayNameWithFormatting() : "#" + TextFormatting.BLUE + SpellGlyphData.getGlyphName(spell, player.world)));
if(advanced.isAdvanced()){
// Advanced tooltips for debugging
text.add(Wizardry.proxy.translate("item." + Wizardry.MODID + ":wand.mana", new Style().setColor(TextFormatting.BLUE),
this.getMana(stack), this.getManaCapacity(stack)));
text.add(Wizardry.proxy.translate("item." + Wizardry.MODID + ":wand.progression", new Style().setColor(TextFormatting.GRAY),
WandHelper.getProgression(stack), this.tier.level < Tier.MASTER.level ? tier.next().getProgression() : 0));
}
}
@Override
public String getItemStackDisplayName(ItemStack stack){
return (this.element == null ? "" : this.element.getFormattingCode()) + super.getItemStackDisplayName(stack);
}
// Continuous spells use the onUsingItemTick method instead of this one.
/* An important thing to note about this method: it is only called on the server and the client of the player
* holding the item (I call this client-inconsistency). This means if you spawn particles here they will not show up
* on other players' screens. Instead, this must be done via packets. */
@Override
public ActionResult<ItemStack> onItemRightClick(World world, EntityPlayer player, EnumHand hand){
ItemStack stack = player.getHeldItem(hand);
// Alternate right-click function; overrides spell casting.
if(this.selectMinionTarget(player, world)) return new ActionResult<>(EnumActionResult.SUCCESS, stack);
Spell spell = WandHelper.getCurrentSpell(stack);
SpellModifiers modifiers = this.calculateModifiers(stack, player, spell);
if(canCast(stack, spell, player, hand, 0, modifiers)){
// Need to account for the modifier since it could be zero even if the original charge-up wasn't
int chargeup = (int)(spell.getChargeup() * modifiers.get(SpellModifiers.CHARGEUP));
if(spell.isContinuous || chargeup > 0){
// Spells that need the mouse to be held (continuous, charge-up or both)
if(!player.isHandActive()){
player.setActiveHand(hand);
// Store the modifiers for use later
if(WizardData.get(player) != null) WizardData.get(player).itemCastingModifiers = modifiers;
if(chargeup > 0 && world.isRemote) Wizardry.proxy.playChargeupSound(player);
return new ActionResult<>(EnumActionResult.SUCCESS, stack);
}
}else{
// All other (instant) spells
if(cast(stack, spell, player, hand, 0, modifiers)){
return new ActionResult<>(EnumActionResult.SUCCESS, stack);
}
}
}
return new ActionResult<>(EnumActionResult.FAIL, stack);
}
// For continuous spells and spells with a charge-up time. The count argument actually decrements by 1 each tick.
// N.B. The first time this gets called is the tick AFTER onItemRightClick is called, not the same tick
@Override
public void onUsingTick(ItemStack stack, EntityLivingBase user, int count){
if(user instanceof EntityPlayer){
EntityPlayer player = (EntityPlayer)user;
Spell spell = WandHelper.getCurrentSpell(stack);
SpellModifiers modifiers;
if(WizardData.get(player) != null){
modifiers = WizardData.get(player).itemCastingModifiers;
}else{
modifiers = this.calculateModifiers(stack, (EntityPlayer)user, spell); // Fallback to the old way, should never be used
}
int useTick = stack.getMaxItemUseDuration() - count;
int chargeup = (int)(spell.getChargeup() * modifiers.get(SpellModifiers.CHARGEUP));
if(spell.isContinuous){
// Continuous spell charge-up is simple, just don't do anything until it's charged
if(useTick >= chargeup){
// castingTick needs to be relative to when the spell actually started
int castingTick = useTick - chargeup;
// Continuous spells (these must check if they can be cast each tick since the mana changes)
// Don't call canCast when castingTick == 0 because we already did it in onItemRightClick - even
// with charge-up times, because we don't want to trigger events twice
if(castingTick == 0 || canCast(stack, spell, player, player.getActiveHand(), castingTick, modifiers)){
cast(stack, spell, player, player.getActiveHand(), castingTick, modifiers);
}else{
// Stops the casting if it was interrupted, either by events or because the wand ran out of mana
player.stopActiveHand();
}
}
}else{
// Non-continuous spells need to check they actually have a charge-up since ALL spells call setActiveHand
if(chargeup > 0 && useTick == chargeup){
// Once the spell is charged, it's exactly the same as in onItemRightClick
cast(stack, spell, player, player.getActiveHand(), 0, modifiers);
}
}
}
}
@Override
public boolean canCast(ItemStack stack, Spell spell, EntityPlayer caster, EnumHand hand, int castingTick, SpellModifiers modifiers){
// Spells can only be cast if the casting events aren't cancelled...
if(castingTick == 0){
if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Pre(Source.WAND, spell, caster, modifiers))) return false;
}else{
if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(Source.WAND, spell, caster, modifiers, castingTick))) return false;
}
int cost = (int)(spell.getCost() * modifiers.get(SpellModifiers.COST) + 0.1f); // Weird floaty rounding
// As of wizardry 4.2 mana cost is only divided over two intervals each second
if(spell.isContinuous) cost = getDistributedCost(cost, castingTick);
// ...and the wand has enough mana to cast the spell...
return cost <= this.getMana(stack) // This comes first because it changes over time
// ...and the wand is the same tier as the spell or higher...
&& spell.getTier().level <= this.tier.level
// ...and either the spell is not in cooldown or the player is in creative mode
&& (WandHelper.getCurrentCooldown(stack) == 0 || caster.isCreative());
}
@Override
public boolean cast(ItemStack stack, Spell spell, EntityPlayer caster, EnumHand hand, int castingTick, SpellModifiers modifiers){
World world = caster.world;
if(world.isRemote && !spell.isContinuous && spell.requiresPacket()) return false;
if(spell.cast(world, caster, hand, castingTick, modifiers)){
if(castingTick == 0) MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.WAND, spell, caster, modifiers));
if(!world.isRemote){
// Continuous spells never require packets so don't rely on the requiresPacket method to specify it
if(!spell.isContinuous && spell.requiresPacket()){
// Sends a packet to all players in dimension to tell them to spawn particles.
IMessage msg = new PacketCastSpell.Message(caster.getEntityId(), hand, spell, modifiers);
WizardryPacketHandler.net.sendToDimension(msg, world.provider.getDimension());
}
// Mana cost
int cost = (int)(spell.getCost() * modifiers.get(SpellModifiers.COST) + 0.1f); // Weird floaty rounding
// As of wizardry 4.2 mana cost is only divided over two intervals each second
if(spell.isContinuous) cost = getDistributedCost(cost, castingTick);
if(cost > 0) this.consumeMana(stack, cost, caster);
}
caster.setActiveHand(hand);
// Cooldown
if(!spell.isContinuous && !caster.isCreative()){ // Spells only have a cooldown in survival
WandHelper.setCurrentCooldown(stack, (int)(spell.getCooldown() * modifiers.get(WizardryItems.cooldown_upgrade)));
}
// Progression
if(this.tier.level < Tier.MASTER.level && castingTick % CONTINUOUS_TRACKING_INTERVAL == 0){
// We don't care about cost modifiers here, otherwise players would be penalised for wearing robes!
int progression = (int)(spell.getCost() * modifiers.get(SpellModifiers.PROGRESSION));
WandHelper.addProgression(stack, progression);
if(!Wizardry.settings.legacyWandLevelling){ // Don't display the message if legacy wand levelling is enabled
// If the wand just gained enough progression to be upgraded...
Tier nextTier = tier.next();
int excess = WandHelper.getProgression(stack) - nextTier.getProgression();
if(excess >= 0 && excess < progression){
// ...display a message above the player's hotbar
caster.playSound(WizardrySounds.ITEM_WAND_LEVELUP, 1.25f, 1);
WizardryAdvancementTriggers.wand_levelup.triggerFor(caster);
if(!world.isRemote)
caster.sendMessage(new TextComponentTranslation("item." + Wizardry.MODID + ":wand.levelup",
this.getItemStackDisplayName(stack), nextTier.getNameForTranslationFormatted()));
}
}
WizardData.get(caster).trackRecentSpell(spell);
}
return true;
}
return false;
}
@Override
public void onPlayerStoppedUsing(ItemStack stack, World world, EntityLivingBase user, int timeLeft){
if(user instanceof EntityPlayer){
EntityPlayer player = (EntityPlayer)user;
Spell spell = WandHelper.getCurrentSpell(stack);
SpellModifiers modifiers;
if(WizardData.get(player) != null){
modifiers = WizardData.get(player).itemCastingModifiers;
}else{
modifiers = this.calculateModifiers(stack, (EntityPlayer)user, spell); // Fallback to the old way, should never be used
}
int castingTick = stack.getMaxItemUseDuration() - timeLeft; // Might as well include this
int cost = getDistributedCost((int)(spell.getCost() * modifiers.get(SpellModifiers.COST) + 0.1f), castingTick);
// Still need to check there's enough mana or the spell will finish twice, since running out of mana is
// handled separately.
if(spell.isContinuous && spell.getTier().level <= this.tier.level && cost <= this.getMana(stack)){
MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Finish(Source.WAND, spell, player, modifiers, castingTick));
spell.finishCasting(world, player, Double.NaN, Double.NaN, Double.NaN, null, castingTick, modifiers);
if(!player.isCreative()){ // Spells only have a cooldown in survival
WandHelper.setCurrentCooldown(stack, (int)(spell.getCooldown() * modifiers.get(WizardryItems.cooldown_upgrade)));
}
}
}
}
@Override
public boolean itemInteractionForEntity(ItemStack stack, EntityPlayer player, EntityLivingBase entity, EnumHand hand){
if(player.isSneaking() && entity instanceof EntityPlayer && WizardData.get(player) != null){
String string = WizardData.get(player).toggleAlly((EntityPlayer)entity) ? "item." + Wizardry.MODID + ":wand.addally"
: "item." + Wizardry.MODID + ":wand.removeally";
if(!player.world.isRemote) player.sendMessage(new TextComponentTranslation(string, entity.getName()));
return true;
}
return false;
}
/** Distributes the given cost (which should be the per-second cost of a continuous spell) over a second and
* returns the appropriate cost to be applied for the given tick. Currently the cost is distributed over 2
* intervals per second, meaning the returned value is 0 unless {@code castingTick} is a multiple of 10.*/
protected static int getDistributedCost(int cost, int castingTick){
int partialCost;
if(castingTick % 20 == 0){ // Whole number of seconds has elapsed
partialCost = cost / 2 + cost % 2; // Make sure cost adds up to the correct value by adding the remainder here
}else if(castingTick % 10 == 0){ // Something-and-a-half seconds has elapsed
partialCost = cost/2;
}else{ // Some other number of ticks has elapsed
partialCost = 0; // Wands aren't damaged within half-seconds
}
return partialCost;
}
/** Returns a SpellModifiers object with the appropriate modifiers applied for the given ItemStack and Spell. */
// This is now public because artefacts use it
public SpellModifiers calculateModifiers(ItemStack stack, EntityPlayer player, Spell spell){
SpellModifiers modifiers = new SpellModifiers();
// Now we only need to add multipliers if they are not 1.
int level = WandHelper.getUpgradeLevel(stack, WizardryItems.range_upgrade);
if(level > 0)
modifiers.set(WizardryItems.range_upgrade, 1.0f + level * Constants.RANGE_INCREASE_PER_LEVEL, true);
level = WandHelper.getUpgradeLevel(stack, WizardryItems.duration_upgrade);
if(level > 0)
modifiers.set(WizardryItems.duration_upgrade, 1.0f + level * Constants.DURATION_INCREASE_PER_LEVEL, false);
level = WandHelper.getUpgradeLevel(stack, WizardryItems.blast_upgrade);
if(level > 0)
modifiers.set(WizardryItems.blast_upgrade, 1.0f + level * Constants.BLAST_RADIUS_INCREASE_PER_LEVEL, true);
level = WandHelper.getUpgradeLevel(stack, WizardryItems.cooldown_upgrade);
if(level > 0)
modifiers.set(WizardryItems.cooldown_upgrade, 1.0f - level * Constants.COOLDOWN_REDUCTION_PER_LEVEL, true);
float progressionModifier = 1.0f - ((float)WizardData.get(player).countRecentCasts(spell) / WizardData.MAX_RECENT_SPELLS)
* MAX_PROGRESSION_REDUCTION;
if(this.element == spell.getElement()){
modifiers.set(SpellModifiers.POTENCY, 1.0f + (this.tier.level + 1) * Constants.POTENCY_INCREASE_PER_TIER, true);
progressionModifier *= ELEMENTAL_PROGRESSION_MODIFIER;
}
if(WizardData.get(player) != null){
if(!WizardData.get(player).hasSpellBeenDiscovered(spell)){
// Casting an undiscovered spell now grants 5x progression
progressionModifier *= DISCOVERY_PROGRESSION_MODIFIER;
}
if(!WizardData.get(player).hasReachedTier(this.tier.next())){
// 1.5x progression for tiers that have already been reached
progressionModifier *= SECOND_TIME_PROGRESSION_MODIFIER;
}
}
modifiers.set(SpellModifiers.PROGRESSION, progressionModifier, false);
return modifiers;
}
private boolean selectMinionTarget(EntityPlayer player, World world){
RayTraceResult rayTrace = RayTracer.standardEntityRayTrace(world, player, 16, false);
if(rayTrace != null && EntityUtils.isLiving(rayTrace.entityHit)){
EntityLivingBase entity = (EntityLivingBase)rayTrace.entityHit;
// Sets the selected minion's target to the right-clicked entity
if(player.isSneaking() && WizardData.get(player) != null && WizardData.get(player).selectedMinion != null){
ISummonedCreature minion = WizardData.get(player).selectedMinion.get();
if(minion instanceof EntityLiving && minion != entity){
// There is now only the new AI! (which greatly improves things)
((EntityLiving)minion).setAttackTarget(entity);
// Deselects the selected minion
WizardData.get(player).selectedMinion = null;
return true;
}
}
}
return false;
}
// Workbench stuff
@Override
public int getSpellSlotCount(ItemStack stack){
return BASE_SPELL_SLOTS + WandHelper.getUpgradeLevel(stack, WizardryItems.attunement_upgrade);
}
@Override
public ItemStack applyUpgrade(@Nullable EntityPlayer player, ItemStack wand, ItemStack upgrade){
// Upgrades wand if necessary. Damage is copied, preserving remaining durability,
// and also the entire NBT tag compound.
if(upgrade.getItem() == WizardryItems.arcane_tome){
Tier tier = Tier.values()[upgrade.getItemDamage()];
// Checks the wand upgrade is for the tier above the wand's tier, and that either the wand has enough
// progression or the player is in creative mode.
if((player == null || player.isCreative() || Wizardry.settings.legacyWandLevelling
|| WandHelper.getProgression(wand) >= tier.getProgression())
&& tier == this.tier.next() && this.tier != Tier.MASTER){
if(Wizardry.settings.legacyWandLevelling){
// Progression has little meaning with legacy upgrade mechanics so just reset it
// In theory, you can get 'free' progression when upgrading since progression can't be negative,
// so the flipside of that is you lose any excess
WandHelper.setProgression(wand, 0);
}else{
// Carry excess progression over to the new stack
WandHelper.setProgression(wand, WandHelper.getProgression(wand) - tier.getProgression());
}
if(player != null) WizardData.get(player).setTierReached(tier);
ItemStack newWand = new ItemStack(getWand(tier, this.element));
newWand.setTagCompound(wand.getTagCompound());
// This needs to be done after copying the tag compound so the mana capacity for the new wand
// takes storage upgrades into account
// Note the usage of the new wand item and not 'this' to ensure the correct capacity is used
((IManaStoringItem)newWand.getItem()).setMana(newWand, this.getMana(wand));
upgrade.shrink(1);
return newWand;
}
}else if(WandHelper.isWandUpgrade(upgrade.getItem())){
// Special upgrades
Item specialUpgrade = upgrade.getItem();
int maxUpgrades = this.tier.upgradeLimit;
if(this.element == null) maxUpgrades += Constants.NON_ELEMENTAL_UPGRADE_BONUS;
if(WandHelper.getTotalUpgrades(wand) < maxUpgrades
&& WandHelper.getUpgradeLevel(wand, specialUpgrade) < Constants.UPGRADE_STACK_LIMIT){
// Used to preserve existing mana when upgrading storage rather than creating free mana.
int prevMana = this.getMana(wand);
WandHelper.applyUpgrade(wand, specialUpgrade);
// Special behaviours for specific upgrades
if(specialUpgrade == WizardryItems.storage_upgrade){
this.setMana(wand, prevMana);
}else if(specialUpgrade == WizardryItems.attunement_upgrade){
int newSlotCount = BASE_SPELL_SLOTS + WandHelper.getUpgradeLevel(wand,
WizardryItems.attunement_upgrade);
Spell[] spells = WandHelper.getSpells(wand);
Spell[] newSpells = new Spell[newSlotCount];
for(int i = 0; i < newSpells.length; i++){
newSpells[i] = i < spells.length && spells[i] != null ? spells[i] : Spells.none;
}
WandHelper.setSpells(wand, newSpells);
int[] cooldowns = WandHelper.getCooldowns(wand);
int[] newCooldowns = new int[newSlotCount];
if(cooldowns.length > 0){
System.arraycopy(cooldowns, 0, newCooldowns, 0, cooldowns.length);
}
WandHelper.setCooldowns(wand, newCooldowns);
}
upgrade.shrink(1);
if(player != null){
WizardryAdvancementTriggers.special_upgrade.triggerFor(player);
if(WandHelper.getTotalUpgrades(wand) == Tier.MASTER.upgradeLimit){
WizardryAdvancementTriggers.max_out_wand.triggerFor(player);
}
}
}
}
return wand;
}
/**
* Helper method to return the appropriate wand based on tier and element. This replaces the cumbersome wand map in
* {@link WizardryItems} by accessing the item registry dynamically by generating the registry name on the fly.
* <p></p>
* <i><b>This method will only return wands from the base mod.</b> It is unlikely that addons will need it, but it
* has been left public just in case. The intention is that this method is only used where there is no alternative.</i>
*
* @param tier The tier of the wand required.
* @param element The element of the wand required. Null will be converted to {@link Element#MAGIC}.
* @return The wand item which corresponds to the given tier and element, or null if no such item exists.
* @throws NullPointerException if the given tier is null.
*/
// As noted above, in a few SPECIFIC cases this method is necessary (without using a data-driven system, at least,
// which I'm not going to spend the time making in the near future). Wizard trades and gear have been left using the
// WizardryItems version because they need to be replaced with a better system that doesn't use this at all.
public static Item getWand(Tier tier, Element element){
if(tier == null) throw new NullPointerException("The given tier cannot be null.");
if(element == null) element = Element.MAGIC;
String registryName = tier == Tier.NOVICE && element == Element.MAGIC ? "magic" : tier.getUnlocalisedName();
if(element != Element.MAGIC) registryName = registryName + "_" + element.getName();
registryName = registryName + "_wand";
return Item.REGISTRY.getObject(new ResourceLocation(Wizardry.MODID, registryName));
}
@Override
public boolean onApplyButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){
boolean changed = false; // Used for advancements
if(upgrade.getHasStack()){
ItemStack original = centre.getStack().copy();
centre.putStack(this.applyUpgrade(player, centre.getStack(), upgrade.getStack()));
changed = !ItemStack.areItemStacksEqual(centre.getStack(), original);
}
// Reads NBT spell metadata array to variable, edits this, then writes it back to NBT.
// Original spells are preserved; if a slot is left empty the existing spell binding will remain.
// Accounts for spells which cannot be applied because they are above the wand's tier; these spells
// will not bind but the existing spell in that slot will remain and other applicable spells will
// be bound as normal, along with any upgrades and crystals.
Spell[] spells = WandHelper.getSpells(centre.getStack());
if(spells.length <= 0){
// Base value here because if the spell array doesn't exist, the wand can't possibly have attunement upgrades
spells = new Spell[BASE_SPELL_SLOTS];
}
for(int i = 0; i < spells.length; i++){
if(spellBooks[i].getStack() != ItemStack.EMPTY){
Spell spell = Spell.byMetadata(spellBooks[i].getStack().getItemDamage());
// If the wand is powerful enough for the spell, it's not already bound to that slot and it's enabled for wands
if(!(spell.getTier().level > this.tier.level) && spells[i] != spell && spell.isEnabled(SpellProperties.Context.WANDS)){
// Decide if we can bind this multiple times
if (Wizardry.settings.preventBindingSameSpellTwiceToWands && Arrays.stream(spells).anyMatch(s -> s == spell)) {
continue;
}
spells[i] = spell;
changed = true;
// setting to consume books upon use
if (Wizardry.settings.singleUseSpellBooks) {
spellBooks[i].getStack().shrink(1);
}
}
}
}
WandHelper.setSpells(centre.getStack(), spells);
// Charges wand by appropriate amount
if (WandHelper.rechargeManaOnApplyButtonPressed(centre, crystals)) {
changed = true;
}
return changed;
}
@Override
public void onClearButtonPressed(EntityPlayer player, Slot centre, Slot crystals, Slot upgrade, Slot[] spellBooks){
ItemStack stack = centre.getStack();
if (stack.hasTagCompound() && stack.getTagCompound().hasKey(WandHelper.SPELL_ARRAY_KEY)) {
NBTTagCompound nbt = stack.getTagCompound();
int[] spells = nbt.getIntArray(WandHelper.SPELL_ARRAY_KEY);
int expectedSlotCount = BASE_SPELL_SLOTS + WandHelper.getUpgradeLevel(stack,
WizardryItems.attunement_upgrade);
// unbrick broken wands
if (spells.length < expectedSlotCount) {
spells = new int[expectedSlotCount];
}
Arrays.fill(spells, 0);
nbt.setIntArray(WandHelper.SPELL_ARRAY_KEY, spells);
stack.setTagCompound(nbt);
}
}
@Override
public boolean isClearable() { return true; }
// hitEntity is only called server-side, so we'll have to use events
@SubscribeEvent
public static void onAttackEntityEvent(AttackEntityEvent event){
EntityPlayer player = event.getEntityPlayer();
ItemStack stack = player.getHeldItemMainhand(); // Can't melee with offhand items
if(stack.getItem() instanceof IManaStoringItem){
// Nobody said it had to be a wand, as long as it's got a melee upgrade it counts
int level = WandHelper.getUpgradeLevel(stack, WizardryItems.melee_upgrade);
int mana = ((IManaStoringItem)stack.getItem()).getMana(stack);
if(level > 0 && mana > 0){
Random random = player.world.rand;
player.world.playSound(player.posX, player.posY, player.posZ, WizardrySounds.ITEM_WAND_MELEE, SoundCategory.PLAYERS, 0.75f, 1, false);
if(player.world.isRemote){
Vec3d origin = player.getPositionEyes(1);
Vec3d hit = origin.add(player.getLookVec().scale(player.getDistance(event.getTarget())));
// Generate two perpendicular vectors in the plane perpendicular to the look vec
Vec3d vec1 = player.getLookVec().rotatePitch(90);
Vec3d vec2 = player.getLookVec().crossProduct(vec1);
for(int i = 0; i < 15; i++){
ParticleBuilder.create(Type.SPARKLE).pos(hit)
.vel(vec1.scale(random.nextFloat() * 0.3f - 0.15f).add(vec2.scale(random.nextFloat() * 0.3f - 0.15f)))
.clr(1f, 1f, 1f).fade(0.3f, 0.5f, 1)
.time(8 + random.nextInt(4)).spawn(player.world);
}
}
}
}
}
}