();
+
+ Property property;
+
+ config.addCustomCategoryComment(COMPATIBILITY_CATEGORY, "Settings that affect how wizardry interacts with other mods. In multiplayer, the server/LAN host settings will apply.");
+
+ property = config.get(COMPATIBILITY_CATEGORY, "damageSourceBlacklist", new String[]{},
+ "List of damage source string identifiers to be ignored when re-applying damage. Case-sensitive. A message will be logged if wizardry detects a damage source that should be added to this list. Otherwise, don't change unless instructed to do so.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".damage_source_blacklist");
+ property.setRequiresWorldRestart(true);
+ damageSourceBlacklist = property.getStringList();
+ propOrder.add(property.getName());
+
+ property = config.get(COMPATIBILITY_CATEGORY, "compatibilityWarnings", true,
+ "Whether to print compatibility warnings to the console. Set to false if excessive messages are being printed.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".compatibility_warnings");
+ Wizardry.proxy.setToNamedBooleanEntry(property);
+ compatibilityWarnings = property.getBoolean();
+ propOrder.add(property.getName());
+
+ property = config.get(COMPATIBILITY_CATEGORY, "baublesIntegration", true,
+ "If Baubles is installed, controls whether Baubles integration features are enabled. If this is disabled, wizardry will always behave as if Baubles is not installed.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".baubles_integration");
+ property.setRequiresMcRestart(true);
+ Wizardry.proxy.setToNamedBooleanEntry(property);
+ baublesIntegration = property.getBoolean();
+ propOrder.add(property.getName());
+
+// property = config.get(COMPATIBILITY_CATEGORY, "jeiIntegration", true,
+// "If JEI (Just Enough Items) is installed, controls whether JEI integration features are enabled. If this is disabled, wizardry will always behave as if JEI is not installed.");
+// property.setLanguageKey("config." + Wizardry.MODID + ".jei_integration");
+// property.setRequiresMcRestart(true);
+// Wizardry.proxy.setToNamedBooleanEntry(property);
+// jeiIntegration = property.getBoolean();
+// propOrder.add(property.getName());
+
+ property = config.get(COMPATIBILITY_CATEGORY, "antiqueAtlasIntegration", true,
+ "If Antique Atlas is installed, controls whether Antique Atlas integration features are enabled. If this is disabled, wizardry will always behave as if Antique Atlas is not installed.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".antique_atlas_integration");
+ property.setRequiresMcRestart(true);
+ Wizardry.proxy.setToNamedBooleanEntry(property);
+ antiqueAtlasIntegration = property.getBoolean();
+ propOrder.add(property.getName());
+
+ property = config.get(COMPATIBILITY_CATEGORY, "autoPlaceTowerMarkers", true,
+ "Controls whether wizardry automatically places antique atlas markers at the locations of wizard towers.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".auto_place_tower_markers");
+ property.setRequiresMcRestart(true);
+ Wizardry.proxy.setToNamedBooleanEntry(property);
+ autoTowerMarkers = property.getBoolean();
+ propOrder.add(property.getName());
+
+ property = config.get(COMPATIBILITY_CATEGORY, "autoPlaceObeliskMarkers", true,
+ "Controls whether wizardry automatically places antique atlas markers at the locations of obelisks.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".auto_place_obelisk_markers");
+ property.setRequiresMcRestart(true);
+ Wizardry.proxy.setToNamedBooleanEntry(property);
+ autoObeliskMarkers = property.getBoolean();
+ propOrder.add(property.getName());
+
+ property = config.get(COMPATIBILITY_CATEGORY, "autoPlaceShrineMarkers", true,
+ "Controls whether wizardry automatically places antique atlas markers at the locations of shrines.");
+ property.setLanguageKey("config." + Wizardry.MODID + ".auto_place_shrine_markers");
+ property.setRequiresMcRestart(true);
+ Wizardry.proxy.setToNamedBooleanEntry(property);
+ autoShrineMarkers = property.getBoolean();
+ propOrder.add(property.getName());
+
+ config.setCategoryPropertyOrder(COMPATIBILITY_CATEGORY, propOrder);
+
+ }
+
+ /** Retrieves a string list from the given property and converts it to an array of {@link ResourceLocation}s. */
+ public static ResourceLocation[] getResourceLocationList(Property property){
+ return toResourceLocations(property.getStringList());
+ }
+
+ /** Converts the given strings to an array of {@link ResourceLocation}s */
+ public static ResourceLocation[] toResourceLocations(String... strings){
+ return Arrays.stream(strings).map(s -> new ResourceLocation(s.toLowerCase(Locale.ROOT).trim())).toArray(ResourceLocation[]::new);
+ }
}
diff --git a/src/main/java/electroblob/wizardry/WizardData.java b/src/main/java/electroblob/wizardry/WizardData.java
deleted file mode 100644
index 313822b4..00000000
--- a/src/main/java/electroblob/wizardry/WizardData.java
+++ /dev/null
@@ -1,715 +0,0 @@
-package electroblob.wizardry;
-
-import java.lang.ref.WeakReference;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.UUID;
-
-import electroblob.wizardry.constants.Element;
-import electroblob.wizardry.enchantment.Imbuement;
-import electroblob.wizardry.entity.EntityShield;
-import electroblob.wizardry.entity.living.ISummonedCreature;
-import electroblob.wizardry.event.SpellCastEvent;
-import electroblob.wizardry.event.SpellCastEvent.Source;
-import electroblob.wizardry.packet.PacketCastContinuousSpell;
-import electroblob.wizardry.packet.PacketPlayerSync;
-import electroblob.wizardry.packet.PacketTransportation;
-import electroblob.wizardry.packet.WizardryPacketHandler;
-import electroblob.wizardry.registry.Spells;
-import electroblob.wizardry.registry.WizardryAdvancementTriggers;
-import electroblob.wizardry.spell.None;
-import electroblob.wizardry.spell.Spell;
-import electroblob.wizardry.util.MagicDamage;
-import electroblob.wizardry.util.MagicDamage.DamageType;
-import electroblob.wizardry.util.SpellModifiers;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.enchantment.Enchantment;
-import net.minecraft.enchantment.EnchantmentHelper;
-import net.minecraft.entity.Entity;
-import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.init.Items;
-import net.minecraft.init.MobEffects;
-import net.minecraft.init.SoundEvents;
-import net.minecraft.item.ItemEnchantedBook;
-import net.minecraft.item.ItemStack;
-import net.minecraft.nbt.*;
-import net.minecraft.potion.PotionEffect;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.EnumHand;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.math.BlockPos;
-import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.capabilities.CapabilityInject;
-import net.minecraftforge.common.capabilities.ICapabilitySerializable;
-import net.minecraftforge.common.util.Constants.NBT;
-import net.minecraftforge.common.util.INBTSerializable;
-import net.minecraftforge.event.AttachCapabilitiesEvent;
-import net.minecraftforge.event.entity.EntityJoinWorldEvent;
-import net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent;
-import net.minecraftforge.event.entity.player.PlayerEvent;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
-import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
-
-/**
- * Capability-based replacement for the old ExtendedPlayer class from 1.7.10. This has been reworked to leave minimum
- * external changes (for my own sanity, mainly!). Turns out the only major difference between an internal capability and
- * an IEEP is a couple of redundant classes and a different way of registering it.
- *
- * Forge seems to have separate classes to hold the Capability<...> instance ('key') and methods for getting the
- * capability, but in my opinion there are already too many classes to deal with, so I'm not adding any more than are
- * necessary, meaning those constants and values are kept here instead.
- *
- * @since Wizardry 2.1
- * @author Electroblob
- */
-// On the plus side, having to rethink this class allowed me to clean it up a lot.
-@Mod.EventBusSubscriber
-public class WizardData implements INBTSerializable {
-
- /** Static instance of what I like to refer to as the capability key. Private because, well, it's internal! */
- // This annotation does some crazy Forge magic behind the scenes and assigns this field a value.
- @CapabilityInject(WizardData.class)
- private static final Capability WIZARD_DATA_CAPABILITY = null;
-
- /** The player this WizardData instance belongs to. */
- private final EntityPlayer player;
-
- // This one is still necessary, because I can't override the equip animation for items that aren't from Wizardry.
- private Map imbuementDurations;
-
- public boolean hasSpiritWolf;
- public boolean hasSpiritHorse;
-
- /**
- * Whether this player is currently casting a continuous spell via commands. Not saved over world reload and reset
- * on player death.
- */
- private Spell currentlyCasting;
- /**
- * The time for which this player has been casting a continuous spell via commands. Increments by 1 each tick. Not
- * saved over world reload and reset on player death.
- */
- private int castingTick;
- /**
- * SpellModifiers object for the current continuous spell cast via commands. Not saved over world reload and reset
- * on player death.
- */
- private SpellModifiers spellModifiers;
- /** Coordinates for the saved transportation stone circle location. Will be null if no location is saved. */
- private BlockPos stoneCircleLocation;
- /** Dimension id which the saved stone circle is in. */
- private int stoneCircleDimension;
- /** Time left until the player teleports under the effect of transportation */
- private int tpCountdown;
-
- /** Coordinates for the saved clairvoyance location. Will be null if no location is saved. */
- private BlockPos clairvoyanceLocation;
- /** Dimension id which the saved clairvoyance point is in. */
- private int clairvoyanceDimension;
-
- public EntityShield shield;
-
- public WeakReference selectedMinion;
-
- /**
- * Set of this player's discovered spells. Do not write to this list directly , use
- * {@link WizardData#discoverSpell(Spell)} instead.
- */
- public Set spellsDiscovered;
-
- private Set allies;
- /**
- * List of usernames of this player's allies. May not be accurate 100% of the time. This is here so that a player
- * can view the usernames of their allies even when those allies are not online. Do not use this for any other
- * purpose than displaying the names!
- */
- public Set allyNames;
-
- private Set soulboundCreatures;
-
- public WizardData(){
- this(null); // Nullary constructor for the registration method factory parameter
- }
-
- public WizardData(EntityPlayer player){
- this.player = player;
- this.imbuementDurations = new HashMap();
- this.spellsDiscovered = new HashSet();
- // All players can recognise magic missile. This is not done using discoverSpell because that seems to cause
- // a crash on load occasionally (probably something to do with achievements being initalised)
- this.spellsDiscovered.add(Spells.magic_missile);
- this.hasSpiritWolf = false;
- this.hasSpiritHorse = false;
- this.currentlyCasting = Spells.none;
- this.spellModifiers = new SpellModifiers();
- this.castingTick = 0;
- this.stoneCircleDimension = 0;
- this.clairvoyanceDimension = 0;
- this.setTpCountdown(0);
- this.allies = new HashSet();
- this.allyNames = new HashSet();
- this.soulboundCreatures = new HashSet();
- }
-
- public boolean hasSpellBeenDiscovered(Spell spell){
- return spellsDiscovered.contains(spell) || spell instanceof None;
- }
-
- /**
- * Adds the given spell to the list of discovered spells for this player. Automatically takes into account whether
- * the spell has been discovered. Use this method rather than adding directly to the list because it handles
- * achievements.
- *
- * @param spell The spell to be discovered
- * @return True if the spell had not already been discovered; false otherwise.
- */
- public boolean discoverSpell(Spell spell){
-
- if(spellsDiscovered == null){
- spellsDiscovered = new HashSet();
- }
- // The 'none' spell cannot be discovered
- if(spell instanceof None) return false;
- // Tries to add the spell to the list of discovered spells, and returns false if it was already present
- if(!spellsDiscovered.add(spell)) return false;
- // If the spell had not already been discovered, achievements can be triggered and the method returns true
- if(spellsDiscovered.containsAll(Spell.getSpells(Spell::isEnabled))){
- WizardryAdvancementTriggers.all_spells.triggerFor(this.player);
- }
-
- for(Element element : Element.values()){
- if(element != Element.MAGIC
- && spellsDiscovered.containsAll(Spell.getSpells(new Spell.TierElementFilter(null, element)))){
- WizardryAdvancementTriggers.element_master.triggerFor(this.player);
- }
- }
-
- return true;
- }
-
- /** Sets the player's saved transportation stone location and dimension. */
- public void setStoneCircleLocation(BlockPos pos, int dimensionID){
- this.stoneCircleLocation = pos;
- this.stoneCircleDimension = dimensionID;
- }
-
- /** Returns the coordinates of the associated player's saved transportation stone circle. */
- public BlockPos getStoneCircleLocation(){
- return stoneCircleLocation;
- }
-
- /** Returns the dimension ID of the associated player's saved transportation stone circle. */
- public int getStoneCircleDimension(){
- return stoneCircleDimension;
- }
-
- public int getTpCountdown(){
- return tpCountdown;
- }
-
- public void setTpCountdown(int tpCountdown){
- this.tpCountdown = tpCountdown;
- }
-
- /** Sets the player's saved clairvoyance location. */
- public void setClairvoyancePoint(BlockPos pos, int dimensionID){
- this.clairvoyanceLocation = pos;
- this.clairvoyanceDimension = dimensionID;
- }
-
- /** Returns the coordinates for the saved clairvoyance location. Will be null if no location is saved. */
- public BlockPos getClairvoyanceLocation(){
- return clairvoyanceLocation;
- }
-
- /** Returns the dimension ID for the saved clairvoyance location. Will be null if no location is saved. */
- public int getClairvoyanceDimension(){
- return clairvoyanceDimension;
- }
-
- /**
- * Overwrites the imbuement duration associated with the given imubement for this player, or creates it if there was
- * none previously.
- *
- * @throws IllegalArgumentException if the given {@link Enchantment} is not an {@link Imbuement}.
- */
- public void setImbuementDuration(Enchantment enchantment, int duration){
- // It is best to throw an exception here, because otherwise the error would either go unnoticed (if
- // non-imbuements
- // were ignored) or cause a ClassCastException later (if non-imbuements were allowed to be added).
- if(enchantment instanceof Imbuement){
- this.imbuementDurations.put((Imbuement)enchantment, duration);
- }else{
- throw new IllegalArgumentException(
- "Attempted to set an imbuement duration for something that isn't an Imbuement! (This exception has been thrown now to prevent a ClassCastException from occurring later.)");
- }
- }
-
- /**
- * Returns the imbuement duration associated with the given imbuement for this player, or 0 if it does not exist.
- */
- @SuppressWarnings("unlikely-arg-type")
- public int getImbuementDuration(Enchantment enchantment){
- // Need to check that i is not null, otherwise it throws an NPE when Java auto-unboxes it.
- // What's nice here is that the map simply accepts objects as keys, so there's no need to cast or throw
- // exceptions.
- Integer i = this.imbuementDurations.get(enchantment);
- // If i is null, returns 0; otherwise returns i, auto-unboxed to an int.
- return i == null ? 0 : i;
- }
-
- /**
- * Decrements the duration for each conjured item by 1, and removes from the map any that are 0 or less or that the
- * player no longer has. Also deletes the item from the player's inventory if it runs out of time.
- */
- private void updateImbuedItems(){
-
- Set activeImbuements = new HashSet();
-
- // For each item in the player's inventory
- for(ItemStack stack : player.inventory.mainInventory){
- if(stack.isItemEnchanted()){
-
- NBTTagList enchantmentList = stack.getItem() == Items.ENCHANTED_BOOK ?
- ItemEnchantedBook.getEnchantments(stack) : stack.getEnchantmentTagList();
-
- Iterator iterator =enchantmentList.iterator();
- // For each of the item's enchantments
- while(iterator.hasNext()){
- NBTTagCompound enchantmentTag = (NBTTagCompound) iterator.next();
- Enchantment enchantment = Enchantment.getEnchantmentByID(enchantmentTag.getShort("id"));
- // Ignores the enchantment unless it is an imbuement
- if(enchantment instanceof Imbuement){
- int duration = this.getImbuementDuration(enchantment);
- // If the imbuement is still active:
- if(duration > 0){
- // Decrements the timer
- this.imbuementDurations.put((Imbuement)enchantment, duration - 1);
- // Adds this imbuement to the set of imbuements that need to be kept
- activeImbuements.add((Imbuement)enchantment);
- // Otherwise:
- }else{
- // Removes the enchantment from the item
- iterator.remove();
- }
- }
- }
- }
- }
- // Removes all imbuements from the map that are no longer active
- this.imbuementDurations.keySet().retainAll(activeImbuements);
- }
-
- /**
- * Adds the given player to the list of allies belonging to the associated player, or removes the player if they are
- * already in the list of allies. Returns true if the player was added, false if they were removed.
- */
- public boolean toggleAlly(EntityPlayer player){
- if(this.isPlayerAlly(player)){
- this.allies.remove(player.getUniqueID());
- // The remove method uses .equals() rather than == so this will work fine.
- this.allyNames.remove(player.getName());
- return false;
- }else{
- this.allies.add(player.getUniqueID());
- this.allyNames.add(player.getName());
- return true;
- }
- }
-
- /** Returns whether the given player is in this player's list of allies, or is on the same team as this player. */
- public boolean isPlayerAlly(EntityPlayer player){
- return this.allies.contains(player.getUniqueID()) || this.player.isOnSameTeam(player);
- }
-
- /** Adds the given entity to this player's list of soulbound creatures, and returns whether it succeeded. */
- public boolean soulbind(EntityLivingBase target){
- return this.soulboundCreatures.add(target.getUniqueID());
- }
-
- /** Returns whether the given entity has been soulbound to this player. */
- public boolean isCreatureSoulbound(EntityPlayer target){
- return this.soulboundCreatures.contains(target.getUniqueID());
- }
-
- /**
- * Damages all creatures soulbound to this player by the given amount, and removes from the list any that no longer
- * exist.
- */
- public void damageAllSoulboundCreatures(float damage){
-
- for(Iterator iterator = this.soulboundCreatures.iterator(); iterator.hasNext();){
-
- Entity entity = WizardryUtilities.getEntityByUUID(this.player.world, iterator.next());
-
- if(entity == null) iterator.remove();
-
- if(entity instanceof EntityLivingBase){
- // Retaliatory effect
- if(entity.attackEntityFrom(MagicDamage.causeDirectMagicDamage(this.player, DamageType.MAGIC, true),
- damage)){
- // Sound only plays if the damage succeeds
- player.playSound(SoundEvents.ENTITY_WITHER_HURT, 1.0F, player.world.rand.nextFloat() * 0.2F + 1.0F);
- }
- }
- }
- }
-
- /** Starts casting the given spell with the given modifiers. */
- public void startCastingContinuousSpell(Spell spell, SpellModifiers modifiers){
-
- this.currentlyCasting = spell;
- this.spellModifiers = modifiers;
-
- if(!this.player.world.isRemote){
- PacketCastContinuousSpell.Message message = new PacketCastContinuousSpell.Message(this.player.getEntityId(),
- spell.id(), this.spellModifiers);
- WizardryPacketHandler.net.sendToDimension(message, this.player.world.provider.getDimension());
- }
- }
-
- /** Stops casting the current spell. */
- public void stopCastingContinuousSpell(){
-
- this.currentlyCasting = Spells.none;
- this.castingTick = 0;
- this.spellModifiers.reset();
-
- if(!this.player.world.isRemote){
- PacketCastContinuousSpell.Message message = new PacketCastContinuousSpell.Message(this.player.getEntityId(),
- Spells.none.id(), this.spellModifiers);
- WizardryPacketHandler.net.sendToDimension(message, this.player.world.provider.getDimension());
- }
- }
-
- /** Casts the current continuous spell, fires relevant events and updates the castingTick field. */
- public void updateContinuousSpellCasting(){
-
- if(this.currentlyCasting != null && this.currentlyCasting.isContinuous){
-
- if(MinecraftForge.EVENT_BUS.post(
- new SpellCastEvent.Tick(player, currentlyCasting, spellModifiers, Source.COMMAND, castingTick))){
- this.stopCastingContinuousSpell();
- return;
- }
-
- if(this.currentlyCasting.cast(player.world, player, EnumHand.MAIN_HAND, castingTick, this.spellModifiers)
- && this.castingTick == 0){
- // On the first tick casting a continuous spell via commands, SpellCastEvent.Post is fired.
- MinecraftForge.EVENT_BUS
- .post(new SpellCastEvent.Post(player, currentlyCasting, spellModifiers, Source.COMMAND));
- }
-
- castingTick++;
-
- }else{
- // Why is this here? Surely castingTick will always be 0 if currentlyCasting is null?
- this.castingTick = 0;
- }
- }
-
- /** Returns whether this player is currently casting a continuous spell via commands. */
- public boolean isCasting(){
- return this.currentlyCasting != null && this.currentlyCasting != Spells.none;
- }
-
- /**
- * Returns the continuous spell this player is currently casting via commands, or the 'none' spell if they aren't
- * casting anything.
- */
- public Spell currentlyCasting(){
- return currentlyCasting;
- }
-
- /** Called each time the associated player is updated. */
- private void update(){
-
- if(this.selectedMinion != null && this.selectedMinion.get() == null) this.selectedMinion = null;
-
- // This new system removes a lot of repetitive event handler code and inflexible variables which had duplicate
- // functions, just for different enchantments.
- updateImbuedItems();
-
- if(!player.world.isRemote){
- if(getTpCountdown() == 1){
- player.setPositionAndUpdate(this.stoneCircleLocation.getX() + 0.5, this.stoneCircleLocation.getY(),
- this.stoneCircleLocation.getZ() + 0.5);
- player.addPotionEffect(new PotionEffect(MobEffects.BLINDNESS, 50, 0));
- IMessage msg = new PacketTransportation.Message(player.getEntityId());
- WizardryPacketHandler.net.sendToDimension(msg, player.world.provider.getDimension());
- }
-
- if(getTpCountdown() > 0){
- setTpCountdown(getTpCountdown() - 1);
- }
- }
-
- updateContinuousSpellCasting();
- }
-
- /**
- * Returns the WizardData instance for the specified player.
- */
- public static final WizardData get(EntityPlayer player){
- return player.getCapability(WIZARD_DATA_CAPABILITY, null);
- }
-
- /**
- * Called from the event handler each time the associated player entity is cloned, i.e. on respawn or when
- * travelling to a different dimension. Used to copy over any variables that should persist over player death. This
- * is the inverse of the old onPlayerDeath method, which reset the variables that shouldn't persist.
- *
- * @param data The old WizardData whose variables are to be copied over.
- * @param respawn True if the player died and is respawning, false if they are just travelling between dimensions.
- */
- public void copyFrom(WizardData data, boolean respawn){
- // TODO: What happens with spirit wolf and spirit horse?
- this.hasSpiritHorse = data.hasSpiritHorse;
- this.hasSpiritWolf = data.hasSpiritWolf;
- this.allies = data.allies;
- this.allyNames = data.allyNames;
- this.clairvoyanceDimension = data.clairvoyanceDimension;
- this.clairvoyanceLocation = data.clairvoyanceLocation;
- this.selectedMinion = data.selectedMinion;
- // Curse of soulbinding is lifted when the caster dies, but not when they switch dimensions.
- if(!respawn) this.soulboundCreatures = data.soulboundCreatures;
- this.spellsDiscovered = data.spellsDiscovered;
- this.stoneCircleDimension = data.stoneCircleDimension;
- this.stoneCircleLocation = data.stoneCircleLocation;
-
- // Imbuements are lost on death so their durations do not persist.
- // Command spell casting is reset on death so the associated variables do not persist.
- // tpCountdown is reset both when the player dies and when they switch dimensions.
-
- }
-
- /** Sends a packet to this player's client to synchronise necessary information. Only called server side. */
- public void sync(){
- if(this.player instanceof EntityPlayerMP){
- int id = -1;
- if(this.selectedMinion != null && this.selectedMinion.get() instanceof Entity)
- id = ((Entity)this.selectedMinion.get()).getEntityId();
- IMessage msg = new PacketPlayerSync.Message(this.spellsDiscovered, id);
- WizardryPacketHandler.net.sendTo(msg, (EntityPlayerMP)this.player);
- }
- }
-
- @Override
- public NBTTagCompound serializeNBT(){
-
- NBTTagCompound properties = new NBTTagCompound();
-
- // ...so Java 8 allows you to do stuff like this:
- properties.setTag("imbuements", WizardryUtilities.mapToNBT(this.imbuementDurations,
- imbuement -> new NBTTagInt(Enchantment.getEnchantmentID((Enchantment)imbuement)), NBTTagInt::new));
-
- properties.setBoolean("hasSpiritWolf", this.hasSpiritWolf);
- properties.setBoolean("hasSpiritHorse", this.hasSpiritHorse);
-
- if(this.stoneCircleLocation != null)
- properties.setLong("stoneCircleLocation", this.stoneCircleLocation.toLong());
- properties.setInteger("stoneCircleDimension", this.stoneCircleDimension);
- properties.setInteger("tpCountdown", this.tpCountdown);
-
- if(this.clairvoyanceLocation != null)
- properties.setLong("clairvoyanceLocation", this.clairvoyanceLocation.toLong());
- properties.setInteger("clairvoyanceDimension", this.getClairvoyanceDimension());
-
- // THIS is why I wrote the list/map <-> NBT methods. Look how neat this is!
- properties.setTag("allies", WizardryUtilities.listToNBT(this.allies, WizardryUtilities::UUIDtoTagCompound));
- properties.setTag("allyNames", WizardryUtilities.listToNBT(this.allyNames, NBTTagString::new));
- properties.setTag("soulboundCreatures",
- WizardryUtilities.listToNBT(this.soulboundCreatures, WizardryUtilities::UUIDtoTagCompound));
-
- // Might be worth converting this over to WizardryUtilities.listToNBT.
- int[] spells = new int[this.spellsDiscovered.size()];
- int i = 0;
- for(Spell spell : this.spellsDiscovered){
- spells[i] = spell.id();
- i++;
- }
- properties.setIntArray("discoveredSpells", spells);
-
- return properties;
- }
-
- @Override
- public void deserializeNBT(NBTTagCompound nbt){
-
- if(nbt != null){
-
- this.imbuementDurations = WizardryUtilities.NBTToMap(nbt.getTagList("imbuements", NBT.TAG_COMPOUND),
- (NBTTagInt tag) -> (Imbuement)Enchantment.getEnchantmentByID(tag.getInt()), NBTTagInt::getInt);
-
- this.hasSpiritWolf = nbt.getBoolean("hasSpiritWolf");
- this.hasSpiritHorse = nbt.getBoolean("hasSpiritHorse");
-
- this.stoneCircleLocation = BlockPos.fromLong(nbt.getLong("stoneCircleLocation"));
- this.stoneCircleDimension = nbt.getInteger("stoneCircleDimension");
- this.tpCountdown = nbt.getInteger("tpCountdown");
-
- this.clairvoyanceLocation = BlockPos.fromLong(nbt.getLong("clairvoyanceLocation"));
- this.clairvoyanceDimension = nbt.getInteger("clairvoyanceDimension");
-
- this.allies = new HashSet(WizardryUtilities.NBTToList(nbt.getTagList("allies", NBT.TAG_COMPOUND),
- WizardryUtilities::tagCompoundToUUID));
-
- this.allyNames = new HashSet(
- WizardryUtilities.NBTToList(nbt.getTagList("allyNames", NBT.TAG_STRING), NBTTagString::getString));
-
- this.soulboundCreatures = new HashSet(WizardryUtilities.NBTToList(
- nbt.getTagList("soulboundCreatures", NBT.TAG_COMPOUND), WizardryUtilities::tagCompoundToUUID));
-
- this.spellsDiscovered = new HashSet();
- for(int id : nbt.getIntArray("discoveredSpells")){
- spellsDiscovered.add(Spell.get(id));
- }
- }
- }
-
- // Event handlers
-
- @SubscribeEvent
- // The type parameter here has to be Entity, not EntityPlayer, or the event won't get fired.
- public static void onCapabilityLoad(AttachCapabilitiesEvent event){
-
- if(event.getObject() instanceof EntityPlayer)
- event.addCapability(new ResourceLocation(Wizardry.MODID, "WizardData"),
- new WizardData.Provider((EntityPlayer)event.getObject()));
-
- // This demonstrates why capabilities are badly structured: The following code compiles, but what it does is put
- // a player into a CapabilityDispatcher, which is in turn stored in that very same player, which makes no sense
- // at all!
- // event.addCapability(new ResourceLocation(Wizardry.MODID, "WizardData"), event.getObject());
- }
-
- @SubscribeEvent
- public static void onPlayerCloneEvent(PlayerEvent.Clone event){
-
- WizardData newData = WizardData.get(event.getEntityPlayer());
- WizardData oldData = WizardData.get(event.getOriginal());
-
- newData.copyFrom(oldData, event.isWasDeath());
- }
-
- @SubscribeEvent
- public static void onEntityJoinWorld(EntityJoinWorldEvent event){
- if(!event.getEntity().world.isRemote && event.getEntity() instanceof EntityPlayerMP){
- // Synchronises wizard data after loading.
- WizardData data = WizardData.get((EntityPlayer)event.getEntity());
- if(data != null) data.sync();
- }
- }
-
- @SubscribeEvent
- public static void onLivingUpdateEvent(LivingUpdateEvent event){
-
- if(event.getEntityLiving() instanceof EntityPlayer){
-
- EntityPlayer player = (EntityPlayer)event.getEntityLiving();
-
- if(WizardData.get(player) != null){
- WizardData.get(player).update();
- }
- }
- }
-
- /**
- * This is a nested class for a few reasons: firstly, it makes sense because instances of this and WizardData go
- * hand-in-hand; secondly, it's too short to be worth a separate file; and thirdly (and most importantly) it allows
- * me to access WIZARD_DATA_CAPABILITY while keeping it private.
- */
- public static class Provider implements ICapabilitySerializable {
-
- private final WizardData data;
-
- public Provider(EntityPlayer player){
- data = new WizardData(player);
- }
-
- @Override
- public boolean hasCapability(Capability> capability, EnumFacing facing){
- return capability == WIZARD_DATA_CAPABILITY;
- }
-
- @Override
- public T getCapability(Capability capability, EnumFacing facing){
-
- if(capability == WIZARD_DATA_CAPABILITY){
- return WIZARD_DATA_CAPABILITY.cast(data);
- }
-
- return null;
- }
-
- @Override
- public NBTTagCompound serializeNBT(){
- return data.serializeNBT();
- }
-
- @Override
- public void deserializeNBT(NBTTagCompound nbt){
- data.deserializeNBT(nbt);
- }
-
- }
-
- // Ended up deleting IWizardData because it was unnecessary. This is the comment that was at the start of it:
-
- /* I'm not going to lie, I will never find the capabilities system even remotely intuitive so this is a bare-minimum
- * approach just to get things working (four classes where one would have done?!) At one point I considered simply
- * wrapping my old IEEP inside a single-field capability, but I eventually decided I would at least *try* to do it
- * properly.
- *
- * "...without having to directly implement many interfaces." - Forge Docs. I still can't see what's wrong with
- * implementing many interfaces; surely that's what Java interfaces are designed for?
- *
- * Other things I find annoying: - IStorage. It's completely redundant in the majority of cases, and I don't
- * understand why we need yet another separate class. - Making an interface, only to implement it once and once
- * only. This completely defeats the point of interfaces. - The EnumFacing parameter, which is again redundant for
- * everything that isn't a tile entity. So much for a clean, neat system.
- *
- * What Forge has effectively done is conflated two different functions: attaching data to stuff and cross-mod
- * integration/soft dependencies. I think this is bad design; it would have been better to keep the two features
- * separate.
- *
- * Here's my current understanding of how the capability system works: - You make an interface which defines the
- * things your capability can do (this class). I will call this the TEMPLATE. - You implement that interface with
- * your default implementation (WizardData). This is the closest analog to your old IEEP implementation class. THIS
- * CLASS STORES ALL THE VARIABLES, and hence has one instance for each instance of whatever it is attached to. I
- * will call this the DATA. - The DATA class implements INBTSerializable (assuming you want it to be saved, which is
- * nearly always the case) - Despite its name, Capability does NOT represent a capability itself. Instead, it
- * acts as a sort of identifier/key, the idea being that you can access a particular instance of your DATA given the
- * key (which tells forge that you want a capability of type TEMPLATE) and the object you want the DATA for. This is
- * what Entity.getCapability(...) does.
- *
- * To really understand what's going on though, you need to sift through Forge's verbose data structures and find
- * where capabilities are actually hooked into vanilla: - Anything that implements ICapabilityProvider will have a
- * private CapabilityDispatcher field. This holds other ICapabilityProviders. (I know. This inheritance pattern DOES
- * NOT MAKE SENSE, because these could, in theory, be OTHER ENTITIES!) - This field is assigned a value through
- * Forge's event factory, which, as we are all familiar with, calls all the methods marked with @SubscribeEvent.
- * These methods add individual ICapabilityProviders to a Map stored in the event, which the event factory then
- * wraps in a CapabilityDispatcher (which is itself an ICapabilityProvider) for the object that called it. - In your
- * event handler, you return a custom ICapabilityProvider which is effectively bolted on to the player, and
- * duplicates the ICapabilityProvider methods so you can hook into them and return an instance of your DATA class. -
- * Where before there was a simple collection of IEEPs stored in the player, there is now a tree of
- * ICapabilityProviders:
- *
- * - Entity/TileEntity/ItemStack - Vanilla ICapabilityProviders, mostly IItemHandlers, stored as fields. -
- * CapabilityDispatcher, stored as a field. - Custom ICapabilityProviders - Custom CapabilityDispatchers - ...
- *
- * Most importantly, EACH PLAYER HOLDS THEIR OWN INSTANCE OF THIS TREE.
- *
- * When a capability is retrieved, the following process happens: 1. For the Entity/TileEntity/ItemStack instance,
- * ICapabilityProvider.getCapability(...) is called. 2. The request propogates through the tree and finds the
- * requested capability. */
-
-}
diff --git a/src/main/java/electroblob/wizardry/Wizardry.java b/src/main/java/electroblob/wizardry/Wizardry.java
index 2f6dbb6e..1eeb4806 100644
--- a/src/main/java/electroblob/wizardry/Wizardry.java
+++ b/src/main/java/electroblob/wizardry/Wizardry.java
@@ -4,18 +4,19 @@ import electroblob.wizardry.command.CommandCastSpell;
import electroblob.wizardry.command.CommandDiscoverSpell;
import electroblob.wizardry.command.CommandSetAlly;
import electroblob.wizardry.command.CommandViewAllies;
+import electroblob.wizardry.data.DispenserCastingData;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.integration.antiqueatlas.WizardryAntiqueAtlasIntegration;
+import electroblob.wizardry.integration.baubles.WizardryBaublesIntegration;
+import electroblob.wizardry.misc.Forfeit;
import electroblob.wizardry.packet.WizardryPacketHandler;
-import electroblob.wizardry.registry.WizardryAdvancementTriggers;
-import electroblob.wizardry.registry.WizardryItems;
-import electroblob.wizardry.registry.WizardryRegistry;
-import electroblob.wizardry.registry.WizardryTabs;
+import electroblob.wizardry.registry.*;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.util.CustomSoundCategory;
+import electroblob.wizardry.util.SpellProperties;
+import electroblob.wizardry.worldgen.*;
import net.minecraft.item.Item;
-import net.minecraft.nbt.NBTBase;
-import net.minecraft.util.EnumFacing;
import net.minecraftforge.common.MinecraftForge;
-import net.minecraftforge.common.capabilities.Capability;
-import net.minecraftforge.common.capabilities.Capability.IStorage;
-import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.event.RegistryEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
@@ -30,6 +31,16 @@ import net.minecraftforge.fml.common.network.NetworkRegistry;
import net.minecraftforge.fml.common.registry.GameRegistry;
import org.apache.logging.log4j.Logger;
+/**
+ * "Electroblob's Wizardry adds an RPG-like system of spells to Minecraft, with the aim of being as playable as
+ * possible. No crazy constructs, no perk trees, no complex recipes - simply find spell books, cast spells, and master
+ * the arcane! - But you knew that, right?"
+ *
+ * Main mod class for Wizardry. Contains the logger and settings instances, along with all the other stuff that's normally
+ * in a main mod class.
+ * @author Electroblob
+ * @since Wizardry 1.0
+ */
@Mod(modid = Wizardry.MODID, name = Wizardry.NAME, version = Wizardry.VERSION, guiFactory = "electroblob.wizardry.WizardryGuiFactory")
public class Wizardry {
@@ -39,48 +50,38 @@ public class Wizardry {
public static final String NAME = "Electroblob's Wizardry";
/**
* The version number for this version of wizardry. The following system is used for version numbers:
- *
+ *
* [major Minecraft version].[major mod version].[minor mod version/patch]
- *
+ *
* The major mod version is consistent across Minecraft versions, i.e. Wizardry 1.1 has the same
* features as Wizardry 2.1, but they are for different versions of Minecraft and have separate minor versioning.
* 1.x.x represents Minecraft 1.7.x versions, 2.x.x represents Minecraft 1.10.x versions, 3.x.x represents Minecraft
* 1.11.x versions, and so on.
*/
- public static final String VERSION = "4.1.4";
+ public static final String VERSION = "4.2.0";
- // IDEA: Improve the algorithm that finds a place to summon creatures to take walls into account.
- // IDEA: Replace all uses of Math.cos and Math.sin with MathHelper versions
// IDEA: Triggering of inbuilt Forge events in relevant places?
+ // IDEA: Abstract the vanilla particles behind the particle builder
- /* Minor bugs that need fixing at some point:
- * - Player skin hat layer shows through wizard hats - Wizard armour breaks rather than just running out of mana (I
- * can't seem to replicate this bug, but I have had it happen to me before...)
- * - Shift-clicking a stack of special upgrades when in the arcane workbench causes the whole stack to be
- * transferred when it should be just one (this is a bug with vanilla as well - try putting a stack of bottles into
- * a brewing stand). I have at least made it so only one gets used now, so it has no impact on the game.
- * - When a spell is on cooldown, you can't break blocks when holding a wand. */
-
- // TODO: So somehow I have managed to overlook the fact that health is actually a float. What this means is that
- // I can make the healing spells use damage multipliers - hurrah!
-
- // TODO: Switch from IInventory to IItemHandler (Or don't. It's only useful for automation really.)
// TODO: Have particles obey Minecraft's particle setting where appropriate
// (see https://github.com/RootsTeam/Embers/blob/master/src/main/java/teamroots/embers/particle/ParticleUtil.java)
-
- // NOTE: Add melee upgrades to loot tables when they are added.
+ // TODO: TileEntityArcaneWorkbench needs looking at, esp. regarding inventory and markDirty
+ // TODO: See what can be done with the illager spell sounds (and go over sounds in general)
+ // TODO: Fix possession
+ // TODO: Fix charge
/** Static instance of the {@link Settings} object for Wizardry. */
public static final Settings settings = new Settings();
- /** Static instance of the {@link Logger} object for Wizardry. */
+ /** Static instance of the {@link Logger} object for Wizardry.
+ *
+ * Logging conventions for wizardry (only these levels are used currently):
+ *
+ * - ERROR : Anything that threw an exception; may or may not crash the game.
+ * - WARN : Anything that isn't supposed to happen during normal operation, but didn't throw an exception.
+ * - INFO : Anything that might happen during normal mod operation that the user needs to know about. */
public static Logger logger;
- // private static Pattern entityNamePattern;
-
- // EventManager
- WizardryWorldGenerator generator = new WizardryWorldGenerator();
-
// The instance of wizardry that Forge uses.
@Instance(Wizardry.MODID)
public static Wizardry instance;
@@ -93,39 +94,28 @@ public class Wizardry {
public void preInit(FMLPreInitializationEvent event){
logger = event.getModLog();
+
+ proxy.registerResourceReloadListeners();
settings.initConfig(event);
- // Yes - by the looks of it, having an interface is completely unnecessary in this case.
- CapabilityManager.INSTANCE.register(WizardData.class, new IStorage(){
- // These methods are only called by Capability.writeNBT() or Capability.readNBT(), which in turn are
- // NEVER CALLED. Unless I'm missing some reflective invocation, that means this entire class serves only
- // to allow capabilities to be saved and loaded manually. What that would be useful for I don't know.
- // (If an API forces most users to write redundant code for no reason, it's not user friendly, is it?)
- // ... well, that's my rant for today!
- @Override
- public NBTBase writeNBT(Capability capability, WizardData instance, EnumFacing side){
- return null;
- }
-
- @Override
- public void readNBT(Capability capability, WizardData instance, EnumFacing side, NBTBase nbt){
- }
- }, WizardData::new);
-
- WizardryRegistry.registerTileEntities();
-
- WizardryRegistry.registerEntities();
- // The check for the generateLoot setting is now done within this method.
- WizardryRegistry.registerLoot();
+ // Capabilities
+ WizardData.register();
+ DispenserCastingData.register();
+ // Register things that don't have registries
+ WizardryBlocks.registerTileEntities();
+ WizardryLoot.register();
WizardryAdvancementTriggers.register();
+ Forfeit.register();
- // Moved to preInit, because apparently it has to be here now.
+ // Client-side stuff (via proxies)
proxy.registerRenderers();
- // It seems this also has to be here
proxy.registerKeyBindings();
+ WizardryBaublesIntegration.init();
+ WizardryAntiqueAtlasIntegration.init();
+
}
@EventHandler
@@ -133,26 +123,41 @@ public class Wizardry {
settings.initConfigExtras();
- // Event Handlers
- GameRegistry.registerWorldGenerator(generator, 0);
- MinecraftForge.EVENT_BUS.register(instance);
- proxy.registerSpellHUD(); // This can't easily be converted to use the new @Mod.EventBusSubscriber system
+ // World generators
+ // Weight is a misnomer, it's actually the priority (where lower numbers get generated first)
+ // Literally nothing on typical 'weight' values here, there isn't even an upper limit
+ // Examples I've managed to find:
+ // - Tinker's construct slime islands use 25
+ GameRegistry.registerWorldGenerator(new WorldGenCrystalOre(), 0);
+ GameRegistry.registerWorldGenerator(new WorldGenCrystalFlower(), 50);
+ GameRegistry.registerWorldGenerator(new WorldGenWizardTower(), 20);
+ GameRegistry.registerWorldGenerator(new WorldGenObelisk(), 20);
+ GameRegistry.registerWorldGenerator(new WorldGenShrine(), 20);
+
+ // This is for the config change and missing mappings events
+ MinecraftForge.EVENT_BUS.register(instance); // Since there's already an instance we might as well use it
+
NetworkRegistry.INSTANCE.registerGuiHandler(this, new WizardryGuiHandler());
WizardryPacketHandler.initPackets();
+ // Post-registry extras
+ WizardryItems.populateWandMap();
+ WizardryItems.populateArmourMap();
+ WizardryItems.registerDispenseBehaviours();
+ Spell.registry.forEach(Spell::init);
+ SpellProperties.init();
+
+ // Client-side stuff (via proxies)
proxy.initGuiBits();
+ proxy.registerParticles();
+ proxy.registerSoundEventListener();
+
+ WizardrySounds.SPELLS = CustomSoundCategory.add(Wizardry.MODID + ":spells");
}
@EventHandler
public void postInit(FMLPostInitializationEvent event){
- // This needs to be here or it won't necessarily include all the mods' entities.
- // TODO: Re-implement this when the Forge bug is fixed.
- /* Doesn't seem to be doing anything... String entityNames = ""; for(Object name :
- * EntityList.classToStringMapping.values()){ if(name instanceof String){ entityNames = entityNames + name +
- * '|'; } } // Cuts off the last '|' entityNames = entityNames.substring(0, entityNames.length()-1);
- * entityNamePattern = Pattern.compile(entityNames); */
proxy.initialiseLayers();
- WizardryTabs.sort();
}
@EventHandler
@@ -174,23 +179,30 @@ public class Wizardry {
// 2.1 changed some item ids, so this fixes them for existing worlds
// Nobody updates minecraft versions when using mods, but I may as well leave this here just in case.
@SubscribeEvent
- public static void onMissingMappingEvent(RegistryEvent.MissingMappings- event){
+ public static void onMissingItemMappingEvent(RegistryEvent.MissingMappings
- event){
// Just get, not getAll, since the mod id didn't change!
for(RegistryEvent.MissingMappings.Mapping
- mapping : event.getAllMappings()){
if(mapping.key.getNamespace().equals(Wizardry.MODID)){
- Item replacement = null;
+ Item replacement;
switch(mapping.key.getPath()){
case "wand_basic": replacement = WizardryItems.magic_wand; break;
- case "wand_basic_fire": replacement = WizardryItems.basic_fire_wand; break;
- case "wand_basic_ice": replacement = WizardryItems.basic_ice_wand; break;
- case "wand_basic_lightning": replacement = WizardryItems.basic_lightning_wand; break;
- case "wand_basic_necromancy": replacement = WizardryItems.basic_necromancy_wand; break;
- case "wand_basic_earth": replacement = WizardryItems.basic_earth_wand; break;
- case "wand_basic_sorcery": replacement = WizardryItems.basic_sorcery_wand; break;
- case "wand_basic_healing": replacement = WizardryItems.basic_healing_wand; break;
+ case "wand_basic_fire": replacement = WizardryItems.novice_fire_wand; break;
+ case "wand_basic_ice": replacement = WizardryItems.novice_ice_wand; break;
+ case "wand_basic_lightning": replacement = WizardryItems.novice_lightning_wand; break;
+ case "wand_basic_necromancy": replacement = WizardryItems.novice_necromancy_wand; break;
+ case "wand_basic_earth": replacement = WizardryItems.novice_earth_wand; break;
+ case "wand_basic_sorcery": replacement = WizardryItems.novice_sorcery_wand; break;
+ case "wand_basic_healing": replacement = WizardryItems.novice_healing_wand; break;
+ case "basic_fire_wand": replacement = WizardryItems.novice_fire_wand; break;
+ case "basic_ice_wand": replacement = WizardryItems.novice_ice_wand; break;
+ case "basic_lightning_wand": replacement = WizardryItems.novice_lightning_wand; break;
+ case "basic_necromancy_wand": replacement = WizardryItems.novice_necromancy_wand; break;
+ case "basic_earth_wand": replacement = WizardryItems.novice_earth_wand; break;
+ case "basic_sorcery_wand": replacement = WizardryItems.novice_sorcery_wand; break;
+ case "basic_healing_wand": replacement = WizardryItems.novice_healing_wand; break;
case "wand_apprentice": replacement = WizardryItems.apprentice_wand; break;
case "wand_apprentice_fire": replacement = WizardryItems.apprentice_fire_wand; break;
case "wand_apprentice_ice": replacement = WizardryItems.apprentice_ice_wand; break;
@@ -233,4 +245,13 @@ public class Wizardry {
}
}
+ @SubscribeEvent
+ public static void onMissingSpellMappingEvent(RegistryEvent.MissingMappings
event){
+ for(RegistryEvent.MissingMappings.Mapping mapping : event.getAllMappings()){
+ if(mapping.key.getNamespace().equals(Wizardry.MODID)){
+ if(mapping.key.getPath().equals("firestorm")) mapping.remap(Spells.fire_breath);
+ }
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/WizardryEventHandler.java b/src/main/java/electroblob/wizardry/WizardryEventHandler.java
index 871787d6..08cb5ac9 100644
--- a/src/main/java/electroblob/wizardry/WizardryEventHandler.java
+++ b/src/main/java/electroblob/wizardry/WizardryEventHandler.java
@@ -1,115 +1,137 @@
package electroblob.wizardry;
import electroblob.wizardry.constants.Constants;
-import electroblob.wizardry.entity.EntityArc;
-import electroblob.wizardry.entity.living.EntityEvilWizard;
+import electroblob.wizardry.data.SpellEmitterData;
+import electroblob.wizardry.data.SpellGlyphData;
+import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.entity.living.ISpellCaster;
-import electroblob.wizardry.entity.living.ISummonedCreature;
import electroblob.wizardry.event.DiscoverSpellEvent;
import electroblob.wizardry.event.SpellCastEvent;
-import electroblob.wizardry.item.ItemWand;
-import electroblob.wizardry.registry.Spells;
-import electroblob.wizardry.registry.WizardryAdvancementTriggers;
-import electroblob.wizardry.registry.WizardryEnchantments;
-import electroblob.wizardry.registry.WizardryItems;
-import electroblob.wizardry.registry.WizardryPotions;
-import electroblob.wizardry.registry.WizardrySounds;
+import electroblob.wizardry.integration.DamageSafetyChecker;
+import electroblob.wizardry.item.IManaStoringItem;
+import electroblob.wizardry.item.ItemArtefact;
+import electroblob.wizardry.packet.PacketSyncAdvancements;
+import electroblob.wizardry.packet.WizardryPacketHandler;
+import electroblob.wizardry.registry.*;
import electroblob.wizardry.spell.FreezingWeapon;
+import electroblob.wizardry.spell.ImbueWeapon;
import electroblob.wizardry.spell.Spell;
-import electroblob.wizardry.util.IElementalDamage;
-import electroblob.wizardry.util.MagicDamage;
+import electroblob.wizardry.util.*;
import electroblob.wizardry.util.MagicDamage.DamageType;
-import electroblob.wizardry.util.SpellModifiers;
-import electroblob.wizardry.util.WandHelper;
-import electroblob.wizardry.util.WizardryParticleType;
-import electroblob.wizardry.util.WizardryUtilities;
+import electroblob.wizardry.util.ParticleBuilder.Type;
+import net.minecraft.advancements.Advancement;
import net.minecraft.enchantment.EnchantmentHelper;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
-import net.minecraft.entity.item.EntityItem;
-import net.minecraft.entity.monster.IMob;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.entity.projectile.EntityArrow;
-import net.minecraft.init.SoundEvents;
import net.minecraft.item.ItemStack;
-import net.minecraft.item.ItemSword;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.EnumHand;
-import net.minecraft.util.EnumParticleTypes;
import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.Vec3d;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.world.World;
-import net.minecraft.world.storage.loot.LootEntry;
-import net.minecraft.world.storage.loot.LootEntryTable;
-import net.minecraft.world.storage.loot.LootPool;
-import net.minecraft.world.storage.loot.RandomValueRange;
-import net.minecraft.world.storage.loot.conditions.LootCondition;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.util.FakePlayer;
-import net.minecraftforge.event.LootTableLoadEvent;
-import net.minecraftforge.event.entity.living.LivingAttackEvent;
-import net.minecraftforge.event.entity.living.LivingDeathEvent;
-import net.minecraftforge.event.entity.living.LivingDropsEvent;
+import net.minecraftforge.event.entity.PlaySoundAtEntityEvent;
+import net.minecraftforge.event.entity.living.*;
import net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent;
-import net.minecraftforge.event.entity.living.LivingHurtEvent;
+import net.minecraftforge.event.entity.player.AdvancementEvent;
+import net.minecraftforge.fml.common.Loader;
import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.EventPriority;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent;
+import java.util.ArrayList;
+
/**
- * As of Wizardry 2.1, most of the code in this class has been relocated somewhere sensible, leaving only a few
- * miscellaneous things that don't make much sense anywhere else, or that are better kept together. Previously, this was
- * a gigantic class with about half of the entire mod's logic in it!
+ * General-purpose event handler for things that don't fit anywhere else or groups of related behaviours that are better
+ * kept together. As of Wizardry 2.1, most of the code in this class has been relocated somewhere sensible, leaving only
+ * a few miscellaneous things that don't make much sense anywhere else, or that are better kept together (previously,
+ * this was a gigantic class with about half of the entire mod's logic in it!)
*
* @author Electroblob
* @since Wizardry 1.0
*/
+// The general rules for where event-based logic goes are:
+// - If the logic relates only to vanilla things or is general (e.g. gameplay settings) it lives in here
+// - If there is an obvious class for the thing relevant to the logic being performed, it goes in there for the sake
+// of modularity/separation-of-concerns (e.g. armour cost reductions go in ItemWizardArmour)
+// - If the thing has an instance but not a separate class (e.g. most custom potions), it goes in a related class
+// where applicable (e.g. a spell class), or if not it goes in here
+// - If several things share a significant amount of logic, to avoid duplicate code and potentially improve efficiency
+// they should be kept together either in a class relevant to all of them (probably a common superclass) or in here
+// - Client-side logic goes in WizardryClientEventHandler or another relevant client-side class
@Mod.EventBusSubscriber
public final class WizardryEventHandler {
- // IDEA: Config option allowing users to specify loot locations
- private static final String[] LOOT_INJECTION_LOCATIONS = {"minecraft:chests/simple_dungeon",
- "minecraft:chests/abandoned_mineshaft", "minecraft:chests/desert_pyramid", "minecraft:chests/jungle_temple",
- "minecraft:chests/stronghold_corridor", "minecraft:chests/stronghold_crossing",
- "minecraft:chests/stronghold_library", "minecraft:chests/igloo_chest", "minecraft:chests/woodland_mansion",
- "minecraft:chests/end_city_treasure"};
-
- @SubscribeEvent
- public static void onLootTableLoadEvent(LootTableLoadEvent event){
- if(Wizardry.settings.generateLoot){
- for(String location : LOOT_INJECTION_LOCATIONS){
- if(event.getName().toString().matches(location)){
- event.getTable().addPool(getAdditive(Wizardry.MODID + ":chests/dungeon_additions"));
- }
- }
- }
- }
-
- private static LootPool getAdditive(String entryName){
- return new LootPool(new LootEntry[]{getAdditiveEntry(entryName, 1)}, new LootCondition[0],
- new RandomValueRange(1), new RandomValueRange(0, 1), Wizardry.MODID + "_additive_pool");
- }
-
- private static LootEntryTable getAdditiveEntry(String name, int weight){
- return new LootEntryTable(new ResourceLocation(name), weight, 0, new LootCondition[0],
- Wizardry.MODID + "_additive_entry");
- }
+ private WizardryEventHandler(){} // No instances!
@SubscribeEvent
public static void onPlayerLoggedInEvent(PlayerLoggedInEvent event){
- // When a player logs in, they are sent the glyph data and the server's settings.
+ // When a player logs in, they are sent the glyph data, server settings and spell properties.
if(event.player instanceof EntityPlayerMP){
SpellGlyphData.get(event.player.world).sync((EntityPlayerMP)event.player);
+ SpellEmitterData.get(event.player.world).sync((EntityPlayerMP)event.player);
Wizardry.settings.sync((EntityPlayerMP)event.player);
+ syncAdvancements((EntityPlayerMP)event.player, false);
+ }
+ Spell.syncProperties(event.player);
+ }
+
+ @SubscribeEvent(priority = EventPriority.HIGH)
+ public static void onPlaySoundAtEntityEvent(PlaySoundAtEntityEvent event){
+ // Muffle (there's no spell class for it so it's here instead)
+ if(event.getEntity() instanceof EntityLivingBase
+ && ((EntityLivingBase)event.getEntity()).isPotionActive(WizardryPotions.muffle)){
+ event.setCanceled(true);
}
}
@SubscribeEvent
+ public static void onAdvancementEvent(AdvancementEvent event){
+ // Forge has no hook for revoked advancements :(
+ // Guess we'll just have to make do
+ // Also, this seems to get fired on player login, so to prevent the toasts from appearing every login the
+ // only way I can see to do it is by testing the player has been around long enough.
+ if(event.getEntityPlayer() instanceof EntityPlayerMP && event.getEntityPlayer().ticksExisted > 0){
+ syncAdvancements((EntityPlayerMP)event.getEntityPlayer(), true);
+ }
+ }
+
+ private static void syncAdvancements(EntityPlayerMP player, boolean showToasts){
+
+ Wizardry.logger.info("Synchronising advancements for " + player.getName());
+
+ ArrayList advancements = new ArrayList<>();
+
+ for(Advancement advancement : player.getServer().getAdvancementManager().getAdvancements()){
+ if(player.getAdvancements().getProgress(advancement).isDone()) advancements.add(advancement.getId());
+ }
+
+ WizardryPacketHandler.net.sendTo(new PacketSyncAdvancements.Message(showToasts, advancements.toArray(new ResourceLocation[0])), player);
+ }
+
+ @SubscribeEvent(priority = EventPriority.HIGH) // Disabling of specific spells comes after arcane jammer but before everything else
public static void onSpellCastPreEvent(SpellCastEvent.Pre event){
+
+ boolean enabled = true;
+
+ switch(event.getSource()){
+ case WAND: enabled = event.getSpell().isEnabled(SpellProperties.Context.WANDS); break;
+ case SCROLL: enabled = event.getSpell().isEnabled(SpellProperties.Context.SCROLL); break;
+ case COMMAND: enabled = event.getSpell().isEnabled(SpellProperties.Context.COMMANDS); break;
+ case NPC: enabled = event.getSpell().isEnabled(SpellProperties.Context.NPCS); break;
+ case DISPENSER: enabled = event.getSpell().isEnabled(SpellProperties.Context.DISPENSERS); break;
+ case OTHER: enabled = event.getSpell().isEnabled(); break; // Any enabled context will do for this one
+ }
+
// If a spell is disabled in the config, it will not work.
- if(!event.getSpell().isEnabled()){
- if(!event.getEntityLiving().world.isRemote) event.getEntity().sendMessage(
+ if(!enabled){
+ if(event.getCaster() != null && !event.getCaster().world.isRemote) event.getCaster().sendMessage(
new TextComponentTranslation("spell.disabled", event.getSpell().getNameForTranslationFormatted()));
event.setCanceled(true);
}
@@ -118,32 +140,36 @@ public final class WizardryEventHandler {
@SubscribeEvent
public static void onSpellCastPostEvent(SpellCastEvent.Post event){
- // Spell discovery (only players can discover spells, obviously)
- if(event.getEntity() instanceof EntityPlayer){
+ if(event.getCaster() instanceof EntityPlayer){
- EntityPlayer player = (EntityPlayer)event.getEntity();
+ EntityPlayer player = (EntityPlayer)event.getCaster();
+ // Advancement triggers
+ if(player instanceof EntityPlayerMP){
+ WizardryAdvancementTriggers.cast_spell.trigger((EntityPlayerMP)player, event.getSpell(), player.getHeldItem(player.getActiveHand()));
+ }
+
+ // Spell discovery (only players can discover spells, obviously)
WizardData data = WizardData.get(player);
if(data != null){
// Data is updated on both sides (This line was added client-side to fix a bug back in 1.1.3, so now
// it's in common code, which is nice!)
// Short-circuiting AND means that discoverSpell is only called if the event isn't cancelled.
- if(!MinecraftForge.EVENT_BUS
- .post(new DiscoverSpellEvent(player, event.getSpell(), DiscoverSpellEvent.Source.CASTING))
+ if(!MinecraftForge.EVENT_BUS.post(new DiscoverSpellEvent(player, event.getSpell(), DiscoverSpellEvent.Source.CASTING))
&& data.discoverSpell(event.getSpell())){
// If the spell wasn't already discovered, other stuff happens:
if(event.getSource() == SpellCastEvent.Source.COMMAND){
// If the spell didn't send a packet itself, the extended player needs to be synced so the
// spell discovery updates on the client.
- if(!event.getSpell().doesSpellRequirePacket()) data.sync();
+ if(!event.getSpell().requiresPacket()) data.sync();
- }else if(!event.getEntity().world.isRemote && !player.capabilities.isCreativeMode
+ }else if(!event.getCaster().world.isRemote && !player.isCreative()
&& Wizardry.settings.discoveryMode){
// Sound and text only happen server-side, in survival, with discovery mode on, and only when
// the spell wasn't cast using commands.
- WizardryUtilities.playSoundAtPlayer(player, SoundEvents.ENTITY_PLAYER_LEVELUP, 1.25f, 1);
+ WizardryUtilities.playSoundAtPlayer(player, WizardrySounds.MISC_DISCOVER_SPELL, 1.25f, 1);
player.sendMessage(new TextComponentTranslation("spell.discover",
event.getSpell().getNameForTranslationFormatted()));
}
@@ -152,33 +178,59 @@ public final class WizardryEventHandler {
}
}
+ @SubscribeEvent(priority = EventPriority.LOW)
+ public static void onDiscoverSpellEvent(DiscoverSpellEvent event){
+ if(event.getEntityPlayer() instanceof EntityPlayerMP){
+ WizardryAdvancementTriggers.discover_spell.trigger((EntityPlayerMP)event.getEntityPlayer(), event.getSpell(), event.getSource());
+ }
+ }
+
+ @SubscribeEvent
+ public static void onLivingSetAttackTargetEvent(LivingSetAttackTargetEvent event){
+
+ if(event.getTarget() != null && event.getEntityLiving() instanceof EntityLiving
+ && event.getTarget().isPotionActive(WizardryPotions.muffle)){
+
+ Vec3d vec = event.getTarget().getPositionEyes(1).subtract(event.getEntity().getPositionEyes(1));
+ // Find the angle between the direction the mob is looking and the direction the player is in
+ // Angle between a and b = acos((a.b) / (|a|*|b|))
+ double angle = Math.acos(vec.dotProduct(event.getEntity().getLookVec()) / vec.length());
+ System.out.println(angle);
+ // If the player is not within the 144-degree arc in front of the mob, it won't detect them
+ if(angle > 0.4 * Math.PI){
+ ((EntityLiving)event.getEntityLiving()).setAttackTarget(null);
+ }
+ }
+ }
+
/* There is a subtle but important difference between LivingAttackEvent and LivingHurtEvent - LivingAttackEvent
* fires immediately when attackEntityFrom is called, whereas LivingHurtEvent only fires if the attack actually
* succeeded, i.e. if the entity in question takes damage (though the event is fired before that so you can cancel
- * the damage). Things are processed in the following order: * LivingAttackEvent * - Invulnerability -
- * Already-dead-ness - Fire resistance - Helmets vs. falling things - Hurt resistant time - Invulnerability (again)
- * * LivingHurtEvent * - Armour - Potions - Health is finally changed Of course, there are no guarantees that other
+ * the damage). Things are processed in the following order:
+ *
+ * * LivingAttackEvent *
+ * - Invulnerability
+ * - Already-dead-ness
+ * - Fire resistance
+ * - Helmets vs. falling things
+ * - Hurt resistant time
+ * - Invulnerability (again)
+ * * LivingHurtEvent *
+ * - Armour
+ * - Potions
+ * * LivingDamageEvent *
+ * - Health is finally changed
+ *
+ * Of course, there are no guarantees that other
* mods hooking into these two events will be called before or after yours, but you can have some degree of control
* by choosing which event to use. EDIT: Actually, there are. Firstly, you can set a priority in the @SubscribeEvent
* annotation which defines how early (higher priority) or late (lower priority) the method is called. Methods with
* the same priority are sorted alphabetically by mod id (so it's safe to assume wizardry would be fairly late on!).
* I wonder if there are any conventions for what sort of things take what priority...? */
- @SubscribeEvent
+ @SubscribeEvent(priority = EventPriority.LOW) // Low priority in case the event gets cancelled at default priority
public static void onLivingAttackEvent(LivingAttackEvent event){
- // Prevents any damage to allies from magic if friendly fire is disabled
- if(!Wizardry.settings.friendlyFire && event.getSource() != null
- && event.getSource().getTrueSource() instanceof EntityPlayer && event.getEntity() instanceof EntityPlayer
- && event.getSource() instanceof IElementalDamage){
- if(WizardryUtilities.isPlayerAlly((EntityPlayer)event.getSource().getTrueSource(),
- (EntityPlayer)event.getEntity())){
- event.setCanceled(true);
- // This needs to be here, since if the event is cancelled nothing else needs to happen.
- return;
- }
- }
-
// Retaliatory effects
// These are better off here because the revenge effects are pretty similar, and I'd rather keep the (lengthy)
// if statement in one place.
@@ -189,48 +241,43 @@ public final class WizardryEventHandler {
EntityLivingBase attacker = (EntityLivingBase)event.getSource().getTrueSource();
World world = event.getEntityLiving().world;
- // Fireskin
- if(event.getEntityLiving().isPotionActive(WizardryPotions.fireskin)
- && !MagicDamage.isEntityImmune(DamageType.FIRE, event.getEntityLiving()))
- attacker.setFire(5);
+ if(attacker.getDistance(event.getEntityLiving()) < 10){
- // Ice Shroud
- if(event.getEntityLiving().isPotionActive(WizardryPotions.ice_shroud)
- && !MagicDamage.isEntityImmune(DamageType.FROST, event.getEntityLiving())
- && !(event.getEntityLiving() instanceof FakePlayer))
- attacker.addPotionEffect(new PotionEffect(WizardryPotions.frost, 100, 0));
+ // Fireskin
+ if(event.getEntityLiving().isPotionActive(WizardryPotions.fireskin)
+ && !MagicDamage.isEntityImmune(DamageType.FIRE, event.getEntityLiving()))
+ attacker.setFire(Spells.fire_breath.getProperty(Spell.BURN_DURATION).intValue());
- // Static Aura
- if(event.getEntityLiving().isPotionActive(WizardryPotions.static_aura)){
+ // Ice Shroud
+ if(event.getEntityLiving().isPotionActive(WizardryPotions.ice_shroud)
+ && !MagicDamage.isEntityImmune(DamageType.FROST, event.getEntityLiving())
+ && !(attacker instanceof FakePlayer)) // Fake players cause problems
+ attacker.addPotionEffect(new PotionEffect(WizardryPotions.frost,
+ Spells.ice_shroud.getProperty(Spell.EFFECT_DURATION).intValue(),
+ Spells.ice_shroud.getProperty(Spell.EFFECT_STRENGTH).intValue()));
- if(!world.isRemote){
- EntityArc arc = new EntityArc(world);
- arc.setEndpointCoords(event.getEntityLiving().posX, event.getEntityLiving().posY + 1,
- event.getEntityLiving().posZ, attacker.posX, attacker.posY + attacker.height / 2,
- attacker.posZ);
- world.spawnEntity(arc);
- }else{
- for(int i = 0; i < 8; i++){
- Wizardry.proxy.spawnParticle(WizardryParticleType.SPARK, world,
- attacker.posX + world.rand.nextFloat() - 0.5, attacker.getEntityBoundingBox().minY
- + attacker.height / 2 + world.rand.nextFloat() * 2 - 1,
- attacker.posZ + world.rand.nextFloat() - 0.5, 0, 0, 0, 3);
- world.spawnParticle(EnumParticleTypes.SMOKE_LARGE, attacker.posX + world.rand.nextFloat() - 0.5,
- attacker.getEntityBoundingBox().minY + attacker.height / 2 + world.rand.nextFloat() * 2
- - 1,
- attacker.posZ + world.rand.nextFloat() - 0.5, 0, 0, 0);
+ // Static Aura
+ if(event.getEntityLiving().isPotionActive(WizardryPotions.static_aura)){
+
+ if(world.isRemote){
+
+ ParticleBuilder.create(Type.LIGHTNING).entity(event.getEntity()).pos(0, event.getEntity().height / 2, 0)
+ .target(attacker).spawn(world);
+
+ ParticleBuilder.spawnShockParticles(world, attacker.posX,
+ attacker.getEntityBoundingBox().minY + attacker.height / 2, attacker.posZ);
}
- }
- attacker.attackEntityFrom(
- MagicDamage.causeDirectMagicDamage(event.getEntityLiving(), DamageType.SHOCK, true), 4.0f);
- attacker.playSound(WizardrySounds.SPELL_SPARK, 1.0F, world.rand.nextFloat() * 0.4F + 1.5F);
+ DamageSafetyChecker.attackEntitySafely(attacker, MagicDamage.causeDirectMagicDamage(event.getEntityLiving(),
+ DamageType.SHOCK, true), Spells.static_aura.getProperty(Spell.DAMAGE).floatValue(), event.getSource().getDamageType());
+ attacker.playSound(WizardrySounds.SPELL_STATIC_AURA_RETALIATE, 1.0F, world.rand.nextFloat() * 0.4F + 1.5F);
+ }
}
}
}
- @SubscribeEvent
+ @SubscribeEvent(priority = EventPriority.LOW) // Again, we don't want these effects if the event is cancelled
public static void onLivingHurtEvent(LivingHurtEvent event){
// Flaming and freezing swords
@@ -239,7 +286,7 @@ public final class WizardryEventHandler {
EntityLivingBase attacker = (EntityLivingBase)event.getSource().getTrueSource();
// Players can only ever attack with their main hand, so this is the right method to use here.
- if(!attacker.getHeldItemMainhand().isEmpty() && attacker.getHeldItemMainhand().getItem() instanceof ItemSword){
+ if(!attacker.getHeldItemMainhand().isEmpty() && ImbueWeapon.isSword(attacker.getHeldItemMainhand().getItem())){
int level = EnchantmentHelper.getEnchantmentLevel(WizardryEnchantments.flaming_weapon,
attacker.getHeldItemMainhand());
@@ -280,12 +327,13 @@ public final class WizardryEventHandler {
@SubscribeEvent
public static void onLivingUpdateEvent(LivingUpdateEvent event){
- if(event.getEntityLiving() instanceof EntityPlayer){
-
- EntityPlayer player = (EntityPlayer)event.getEntityLiving();
-
- if(player.world.isRemote) hackilyFixContinuousSpellCasting(player);
- }
+ // Experimental animation feature
+// if(event.getEntityLiving().isHandActive() && event.getEntityLiving().getActiveItemStack().getItemUseAction() == WizardryUtilities.POINT){
+// event.getEntityLiving().isSwingInProgress = true;
+// event.getEntityLiving().swingProgress = 1f;
+// event.getEntityLiving().prevSwingProgress = 1;
+// event.getEntityLiving().swingingHand = event.getEntityLiving().getActiveHand();
+// }
if(event.getEntityLiving().world.isRemote){
@@ -296,13 +344,14 @@ public final class WizardryEventHandler {
Spell spell = ((ISpellCaster)event.getEntity()).getContinuousSpell();
SpellModifiers modifiers = ((ISpellCaster)event.getEntity()).getModifiers();
- if(spell != null && spell != Spells.none){
+ if(spell != null && spell != Spells.none){ // IntelliJ is wrong, do NOT remove the null check!
- if(!MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(event.getEntityLiving(), spell, modifiers,
- SpellCastEvent.Source.NPC, 0))){
+ if(!MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(SpellCastEvent.Source.NPC, spell, event.getEntityLiving(),
+ modifiers, 0))){
spell.cast(event.getEntity().world, (EntityLiving)event.getEntity(), EnumHand.MAIN_HAND, 0,
// TODO: This implementation of modifiers relies on them being accessible client-side.
+ // Right now that doesn't matter because NPCs don't use modifiers, but they might in future
((EntityLiving)event.getEntity()).getAttackTarget(), modifiers);
}
}
@@ -310,7 +359,7 @@ public final class WizardryEventHandler {
}
}
- @SubscribeEvent
+ @SubscribeEvent(priority = EventPriority.LOWEST) // No siphoning if the event is cancelled, that could be exploited...
public static void onLivingDeathEvent(LivingDeathEvent event){
if(event.getSource().getTrueSource() instanceof EntityPlayer){
@@ -319,64 +368,85 @@ public final class WizardryEventHandler {
for(ItemStack stack : WizardryUtilities.getPrioritisedHotbarAndOffhand(player)){
- if(stack.getItem() instanceof ItemWand && stack.isItemDamaged()
+ if(stack.getItem() instanceof IManaStoringItem && !((IManaStoringItem)stack.getItem()).isManaFull(stack)
&& WandHelper.getUpgradeLevel(stack, WizardryItems.siphon_upgrade) > 0){
- int damage = stack.getItemDamage()
- - Constants.SIPHON_MANA_PER_LEVEL
+
+ int mana = Constants.SIPHON_MANA_PER_LEVEL
* WandHelper.getUpgradeLevel(stack, WizardryItems.siphon_upgrade)
- - player.world.rand.nextInt(Constants.SIPHON_MANA_PER_LEVEL);
- if(damage < 0) damage = 0;
- stack.setItemDamage(damage);
- break;
+ + player.world.rand.nextInt(Constants.SIPHON_MANA_PER_LEVEL);
+
+ if(ItemArtefact.isArtefactActive(player, WizardryItems.ring_siphoning)) mana *= 1.3f;
+
+ ((IManaStoringItem)stack.getItem()).rechargeMana(stack, mana);
+
+ break; // Only recharge one item per kill
+ }
+ }
+ }
+ }
+
+ // These two are lifted from EntityLivingBase#travel
+ private static final double LIVING_ENTITY_GRAVITY = 0.08;
+ private static final double LIVING_ENTITY_DRAG = 0.98;
+
+ private static final double LIVING_ENTITY_TERMINAL_VELOCITY = 3.92; // From Minecraft Wiki!
+
+ private static final double LOG_LIVING_ENTITY_DRAG = Math.log(LIVING_ENTITY_DRAG);
+ private static final double FALL_TICKS_ERROR_CORRECTION = 0.500841776608447;
+
+ @SubscribeEvent // Priority doesn't matter here, we're only setting event fields so if it's cancelled it won't matter
+ public static void onLivingFallEvent(LivingFallEvent event){
+ // Why is fall damage based on distance fallen? Why? Who on earth came up with that? It makes no sense whatsoever!
+ if(Wizardry.settings.replaceVanillaFallDamage && !Loader.isModLoaded("speedbasedfalldamage")){
+ // We want to keep the fall damage EXACTLY THE SAME for free, uninterrupted falls, but fix the weirdness
+ // caused when something else changes the entity's velocity
+ // All living entities have a gravity of 0.08b/t^2
+ // Therefore it would be simple to say v^2 = u^2 + 2gs gives the equivalent fall distance as motionY^2 / 0.16
+ // However, Minecraft also has a drag of 0.02 * the velocity, so we actually expect a slightly different value
+ // Much maths later...
+
+ double v = event.getEntity().motionY;
+ // Players are weird, their velocity somehow resets on the server just before this event fires so
+ // instead we're storing the y velocity from the previous tick in WizardData and retrieving it here
+ // Of course, if another mod screws things up and sets a player's velocity client-side only then this won't
+ // work ...but it's better than having clients calculate their own fall damage
+ if(event.getEntity() instanceof EntityPlayer){
+ WizardData data = WizardData.get((EntityPlayer)event.getEntity());
+ if(data != null){
+ v = data.prevMotionY;
}
}
- if(event.getEntityLiving() == player && event.getSource() instanceof IElementalDamage){
- WizardryAdvancementTriggers.self_destruct.triggerFor(player);
+ // At terminal velocity, there's no way of finding fall distance from velocity, and the entity is probably dead anyway!
+ if(v > -3.9){
+
+ // Just to make the code more readable, java will replace them all with numbers anyway
+ double g = LIVING_ENTITY_GRAVITY;
+ double f = LIVING_ENTITY_DRAG;
+ double lnf = LOG_LIVING_ENTITY_DRAG;
+ double tv = LIVING_ENTITY_TERMINAL_VELOCITY;
+
+ // Work backwards from y velocity to get fall time
+ // logs are probably slow but it's not like this gets calculated every tick, and we only need one log
+ double t = Math.log(((-v - tv) * lnf) / g) / lnf; // Log the number over log the base
+
+ // Because time is in discrete ticks, the above equation for t results in a constant error of
+ // +0.500841776608447, so I guess we can just subtract it... if it works it works I guess!
+ t -= FALL_TICKS_ERROR_CORRECTION; // Don't cast to int or perform any rounding
+
+ // Now work forwards from t to find the effective fall distance, i.e. the distance the entity would
+ // have to freefall to reach the same velocity
+ // Don't ask me where the 196 is from, again, it just works!
+ double y = (g * Math.pow(f, t)) / (lnf*lnf) + tv * (t) - 196;
+
+ // DEBUG
+// if(event.getEntity() instanceof EntityPlayer){
+// Wizardry.logger.info("Replaced fall distance {} with effective distance {} based on entity velocity", event.getDistance(), y);
+// }
+
+ event.setDistance((float)y);
}
}
}
- @SubscribeEvent
- public static void onLivingDropsEvent(LivingDropsEvent event){
- // TODO: Really, this should be in a loot table (mob_additions), however I can't seem to find a way of
- // automatically adding it to all subclasses of IMob.
- // Evil wizards drop spell books themselves
- if(event.getEntityLiving() instanceof IMob && !(event.getEntityLiving() instanceof EntityEvilWizard)
- // TODO: Backport when you backport the new summoned creature system.
- && !(event.getEntityLiving() instanceof ISummonedCreature)
- && event.getSource().getTrueSource() instanceof EntityPlayer && Wizardry.settings.spellBookDropChance > 0){
-
- // This does exactly what the entity drop method does, but with a different random number so that the
- // spell book doesn't always drop with other rare drops.
- int rareDropNumber = event.getEntity().world.rand.nextInt(200) - event.getLootingLevel();
- if(rareDropNumber < Wizardry.settings.spellBookDropChance){
- // Drops a spell book
- int id = WizardryUtilities.getStandardWeightedRandomSpellId(event.getEntity().world.rand);
-
- event.getDrops()
- .add(new EntityItem(event.getEntityLiving().world, event.getEntityLiving().posX,
- event.getEntityLiving().posY, event.getEntityLiving().posZ,
- new ItemStack(WizardryItems.spell_book, 1, id)));
- }
- }
- }
-
- // Private helper methods
- // ================================================================================================================
-
- /**
- * Detects inconsistencies between player.getActiveItemStack and the actual itemstack and forces them to be equal.
- * Fixes issue #25.
- *
- * @param player
- */
- private static void hackilyFixContinuousSpellCasting(EntityPlayer player){
- if(player.isHandActive() && player.getHeldItem(player.getActiveHand()).getItem() instanceof ItemWand
- && WandHelper.getCurrentSpell(player.getHeldItem(player.getActiveHand())).isContinuous){
- if(player.getActiveItemStack() != player.getHeldItem(player.getActiveHand())){
- player.setHeldItem(player.getActiveHand(), player.getActiveItemStack());
- }
- }
- }
}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/WizardryGuiFactory.java b/src/main/java/electroblob/wizardry/WizardryGuiFactory.java
index 19951840..a6ef1190 100644
--- a/src/main/java/electroblob/wizardry/WizardryGuiFactory.java
+++ b/src/main/java/electroblob/wizardry/WizardryGuiFactory.java
@@ -1,12 +1,12 @@
package electroblob.wizardry;
-import java.util.Set;
-
-import electroblob.wizardry.client.GuiConfigWizardry;
+import electroblob.wizardry.client.gui.config.GuiConfigWizardry;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraftforge.fml.client.IModGuiFactory;
+import java.util.Set;
+
public class WizardryGuiFactory implements IModGuiFactory {
@Override
diff --git a/src/main/java/electroblob/wizardry/WizardryGuiHandler.java b/src/main/java/electroblob/wizardry/WizardryGuiHandler.java
index f3dffaa6..140ed107 100644
--- a/src/main/java/electroblob/wizardry/WizardryGuiHandler.java
+++ b/src/main/java/electroblob/wizardry/WizardryGuiHandler.java
@@ -40,20 +40,20 @@ public class WizardryGuiHandler implements IGuiHandler {
if(id == ARCANE_WORKBENCH){
TileEntity tileEntity = world.getTileEntity(new BlockPos(x, y, z));
if(tileEntity instanceof TileEntityArcaneWorkbench){
- return new electroblob.wizardry.client.GuiArcaneWorkbench(player.inventory,
+ return new electroblob.wizardry.client.gui.GuiArcaneWorkbench(player.inventory,
(TileEntityArcaneWorkbench)tileEntity);
}
}else if(id == WIZARD_HANDBOOK && (player.getHeldItemMainhand().getItem() instanceof ItemWizardHandbook
|| player.getHeldItemOffhand().getItem() instanceof ItemWizardHandbook)){
- return new electroblob.wizardry.client.GuiWizardHandbook();
+ return new electroblob.wizardry.client.gui.handbook.GuiWizardHandbook();
}else if(id == SPELL_BOOK){
if(player.getHeldItemMainhand().getItem() instanceof ItemSpellBook){
- return new electroblob.wizardry.client.GuiSpellBook(Spell.get(player.getHeldItemMainhand().getItemDamage()));
+ return new electroblob.wizardry.client.gui.GuiSpellBook(Spell.byMetadata(player.getHeldItemMainhand().getItemDamage()));
}else if(player.getHeldItemOffhand().getItem() instanceof ItemSpellBook){
- return new electroblob.wizardry.client.GuiSpellBook(Spell.get(player.getHeldItemOffhand().getItemDamage()));
+ return new electroblob.wizardry.client.gui.GuiSpellBook(Spell.byMetadata(player.getHeldItemOffhand().getItemDamage()));
}
}else if(id == PORTABLE_CRAFTING){
- return new electroblob.wizardry.client.GuiPortableCrafting(player.inventory, world, new BlockPos(x, y, z));
+ return new electroblob.wizardry.client.gui.GuiPortableCrafting(player.inventory, world, new BlockPos(x, y, z));
}
return null;
}
diff --git a/src/main/java/electroblob/wizardry/WizardryWorldGenerator.java b/src/main/java/electroblob/wizardry/WizardryWorldGenerator.java
deleted file mode 100644
index 4283a41e..00000000
--- a/src/main/java/electroblob/wizardry/WizardryWorldGenerator.java
+++ /dev/null
@@ -1,949 +0,0 @@
-package electroblob.wizardry;
-
-import java.util.HashSet;
-import java.util.Random;
-import java.util.Set;
-
-import org.apache.commons.lang3.ArrayUtils;
-
-import electroblob.wizardry.entity.living.EntityEvilWizard;
-import electroblob.wizardry.entity.living.EntityWizard;
-import electroblob.wizardry.registry.WizardryBlocks;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.block.BlockChest;
-import net.minecraft.block.BlockColored;
-import net.minecraft.block.BlockLeaves;
-import net.minecraft.block.BlockLiquid;
-import net.minecraft.block.BlockPlanks;
-import net.minecraft.block.BlockSlab;
-import net.minecraft.block.BlockSlab.EnumBlockHalf;
-import net.minecraft.block.BlockTorch;
-import net.minecraft.block.state.IBlockState;
-import net.minecraft.init.Biomes;
-import net.minecraft.init.Blocks;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.item.EnumDyeColor;
-import net.minecraft.item.ItemDoor;
-import net.minecraft.util.EnumFacing;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.util.math.BlockPos;
-import net.minecraft.world.World;
-import net.minecraft.world.WorldServer;
-import net.minecraft.world.biome.Biome;
-import net.minecraft.world.chunk.IChunkProvider;
-import net.minecraft.world.gen.IChunkGenerator;
-import net.minecraft.world.gen.feature.WorldGenMinable;
-import net.minecraft.world.storage.loot.LootContext;
-import net.minecraft.world.storage.loot.LootTable;
-import net.minecraftforge.common.BiomeDictionary;
-import net.minecraftforge.common.IPlantable;
-import net.minecraftforge.fml.common.IWorldGenerator;
-
-public class WizardryWorldGenerator implements IWorldGenerator {
-
- /** The string identifier for wizard tower chests, used in ChestGenHooks. */
- public static final String WIZARD_TOWER = Wizardry.MODID + "wizardTower";
-
- @Override
- public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator,
- IChunkProvider chunkProvider){
-
- for(int id : Wizardry.settings.oreDimensions){
- if(id == world.provider.getDimension()) this.addOreSpawn(WizardryBlocks.crystal_ore.getDefaultState(),
- world, random, chunkX * 16, chunkZ * 16, 16, 16, 5, 7, 5, 30);
- }
-
- for(int id : Wizardry.settings.flowerDimensions){
- if(id == world.provider.getDimension()) this.generatePlant(WizardryBlocks.crystal_flower.getDefaultState(),
- world, random, chunkX * 16, chunkZ * 16, 2, 20);
- }
-
- if(world.getWorldInfo().isMapFeaturesEnabled()){
- for(int id : Wizardry.settings.towerDimensions){
- if(id == world.provider.getDimension())
- this.generateWizardTower(world, random, chunkX * 16, chunkZ * 16);
- }
- }
- }
-
- /**
- * Adds an Ore Spawn to Minecraft. Simply register all Ores to spawn with this method in your Generation method in
- * your IWorldGeneration extending Class
- *
- * @param The Block to spawn
- * @param The World to spawn in
- * @param A Random object for retrieving random positions within the world to spawn the Block
- * @param An int for passing the X-Coordinate for the Generation method
- * @param An int for passing the Z-Coordinate for the Generation method
- * @param An int for setting the maximum X-Coordinate values for spawning on the X-Axis on a Per-Chunk basis
- * @param An int for setting the maximum Z-Coordinate values for spawning on the Z-Axis on a Per-Chunk basis
- * @param An int for setting the maximum size of a vein
- * @param An int for the Number of chances available for the Block to spawn per-chunk
- * @param An int for the minimum Y-Coordinate height at which this block may spawn
- * @param An int for the maximum Y-Coordinate height at which this block may spawn
- **/
- public void addOreSpawn(IBlockState state, World world, Random random, int blockXPos, int blockZPos, int maxX,
- int maxZ, int maxVeinSize, int chancesToSpawn, int minY, int maxY){
- // int maxPossY = minY + (maxY - 1);
- assert maxY > minY : "The maximum Y must be greater than the Minimum Y";
- assert maxX > 0 && maxX <= 16 : "addOreSpawn: The Maximum X must be greater than 0 and less than 16";
- assert minY > 0 : "addOreSpawn: The Minimum Y must be greater than 0";
- assert maxY < 256 && maxY > 0 : "addOreSpawn: The Maximum Y must be less than 256 but greater than 0";
- assert maxZ > 0 && maxZ <= 16 : "addOreSpawn: The Maximum Z must be greater than 0 and less than 16";
-
- int diffBtwnMinMaxY = maxY - minY;
- for(int x = 0; x < chancesToSpawn; x++){
- int posX = blockXPos + random.nextInt(maxX);
- int posY = minY + random.nextInt(diffBtwnMinMaxY);
- int posZ = blockZPos + random.nextInt(maxZ);
- (new WorldGenMinable(state, maxVeinSize)).generate(world, random, new BlockPos(posX, posY, posZ));
- }
- }
-
- /**
- * Generates the specified plant randomly throughout the world.
- *
- * @param block The plant block
- * @param world The world
- * @param random A random instance
- * @param x The x coord of the first block in the chunk
- * @param z The y coord of the first block in the chunk
- * @param chancesToSpawn Number of chances to spawn a flower patch
- * @param groupSize The number of times to try generating a flower per flower patch spawn
- */
- public void generatePlant(IBlockState state, World world, Random random, int x, int z, int chancesToSpawn,
- int groupSize){
-
- for(int i = 0; i < chancesToSpawn; i++){
- int randPosX = x + random.nextInt(16);
- int randPosY = random.nextInt(256);
- int randPosZ = z + random.nextInt(16);
- for(int l = 0; l < groupSize; ++l){
- int i1 = randPosX + random.nextInt(8) - random.nextInt(8);
- int j1 = randPosY + random.nextInt(4) - random.nextInt(4);
- int k1 = randPosZ + random.nextInt(8) - random.nextInt(8);
-
- BlockPos pos = new BlockPos(i1, j1, k1);
-
- if(world.isBlockLoaded(pos) && world.isAirBlock(pos) && (!world.provider.isNether() || j1 < 127)
- && state.getBlock().canPlaceBlockOnSide(world, pos, EnumFacing.UP)){
-
- world.setBlockState(pos, state, 2);
- }
- }
- }
- }
-
- /**
- * Generates wizard towers randomly throughout the world.
- */
- public void generateWizardTower(World world, Random random, int chunkX, int chunkZ){
-
- // Allows the config file to set the rarity value to 0 to disable tower generation completely.
- if(Wizardry.settings.towerRarity == 0) return;
-
- // Compensates for the lack of space in forests. Math.max is required since treeless biomes have treesPerChunk =
- // -999
- // double treeFactor = 70 - Math.max((double)world.getBiomeGenForCoords(chunkX,
- // chunkZ).theBiomeDecorator.treesPerChunk, 0) * 1.5d;
-
- // Multiplied by 70 to (roughly) retain the old rarity scale
- if(random.nextInt((int)(Wizardry.settings.towerRarity * 70)) == 0){
-
- BlockPos origin = new BlockPos(chunkX + random.nextInt(16), 0, chunkZ + random.nextInt(16));
-
- // Despite what its name suggests, this method does not return the position of a liquid. It is in fact
- // exactly what is needed here since it is used for placing villages and stuff, and doesn't include leaves
- // or other foliage.
- origin = origin.up(world.getTopSolidOrLiquidBlock(origin).getY() - 1);
-
- int[][][] towerBlueprint = towerBlueprintSmall;
-
- switch(random.nextInt(4)){
- case 0:
- towerBlueprint = towerBlueprintSmall;
- break;
- case 1:
- towerBlueprint = towerBlueprintMedium;
- break;
- case 2:
- towerBlueprint = towerBlueprintTall;
- break;
- case 3:
- towerBlueprint = towerBlueprintDouble;
- break;
- }
-
- // 0 = West, 1 = North, 2 = East, 3 = South (The way you would face when walking out of the door)
- EnumFacing orientation = EnumFacing.byHorizontalIndex(random.nextInt(4));
- boolean flip = random.nextBoolean();
-
- if(checkSpaceForTower(world, origin, towerBlueprint, orientation, flip)){
-
- // == Setup ==
-
- boolean evilWizard = random.nextInt(5) == 0;
-
- IBlockState wallMaterial = Blocks.COBBLESTONE.getDefaultState();
- BlockPlanks.EnumType woodType = BlockPlanks.EnumType.OAK;
-
- Biome biome = world.getBiome(origin);
-
- // The order of these is somewhat important in that biomes can be many types, so the last of these
- // checks
- // that is true gets priority. Generally speaking, the later the check, the more specific it is.
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.DENSE))
- wallMaterial = Blocks.MOSSY_COBBLESTONE.getDefaultState();
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.SWAMP))
- wallMaterial = Blocks.MOSSY_COBBLESTONE.getDefaultState();
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.SANDY))
- wallMaterial = Blocks.SANDSTONE.getDefaultState();
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.NETHER))
- wallMaterial = Blocks.NETHER_BRICK.getDefaultState();
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.MOUNTAIN))
- wallMaterial = Blocks.STONEBRICK.getDefaultState();
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.MESA))
- wallMaterial = Blocks.HARDENED_CLAY.getDefaultState();
-
- // Unfortunately, I can't check all the wood types with the biome dictionary
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.CONIFEROUS))
- woodType = BlockPlanks.EnumType.SPRUCE;
- if(biome == Biomes.BIRCH_FOREST || biome == Biomes.BIRCH_FOREST_HILLS)
- woodType = BlockPlanks.EnumType.BIRCH;
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.JUNGLE)) woodType = BlockPlanks.EnumType.JUNGLE;
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.SAVANNA)) woodType = BlockPlanks.EnumType.ACACIA;
- // Not technically a tree type, but I think it fits quite well anyway
- if(BiomeDictionary.hasType(biome, BiomeDictionary.Type.SPOOKY))
- woodType = BlockPlanks.EnumType.DARK_OAK;
-
- IBlockState[] blockStateList = new IBlockState[]{null, Blocks.AIR.getDefaultState(),
- Blocks.PLANKS.getDefaultState().withProperty(BlockPlanks.VARIANT, woodType),
- Blocks.BOOKSHELF.getDefaultState(),
- Blocks.STAINED_HARDENED_CLAY.getDefaultState().withProperty(BlockColored.COLOR,
- EnumDyeColor.values()[random.nextInt(EnumDyeColor.values().length)]),
- Blocks.WOODEN_SLAB.getDefaultState().withProperty(BlockSlab.HALF, EnumBlockHalf.BOTTOM),
- Blocks.WOODEN_SLAB.getDefaultState().withProperty(BlockSlab.HALF, EnumBlockHalf.TOP),
- wallMaterial, Blocks.GLASS_PANE.getDefaultState(), Blocks.OAK_DOOR.getDefaultState(),
- WizardryBlocks.arcane_workbench.getDefaultState(), Blocks.TORCH.getDefaultState(),
- evilWizard ? Blocks.CHEST.getDefaultState() : Blocks.BOOKSHELF.getDefaultState()};
-
- Set blocksPlaced = new HashSet();
-
- // == Foundations ==
-
- // Fills in foundations. This is done first so the door always has something to be placed on.
- boolean flag = true;
-
- // BlockPos is immutable, so I'm not sure if simply saying pos1 = pos will be sufficient.
- BlockPos layerCentre = new BlockPos(origin);
-
- // Stop when the bottom of the world is reached
- while(flag && layerCentre.getY() > 0){
-
- flag = false;
-
- for(BlockPos offset : foundationLayer){
- if(!world.isBlockNormalCube(layerCentre.add(offset), false)){
- world.setBlockState(layerCentre.add(offset), wallMaterial);
- blocksPlaced.add(layerCentre.add(offset));
- // Keeps going as long as something was filled in.
- flag = true;
- }
- }
-
- layerCentre = layerCentre.down();
- }
-
- // == Main Structure ==
-
- // It is assumed that the width of the blueprint is the same all the way up, and that the layers are
- // square.
- int width = towerBlueprint[0].length - 1;
-
- // x, y and z are the position the block is being put in.
- // x1, y and z1 are the position in the blueprint which determines which block is being placed.
-
- int x1 = 0, z1 = 0;
-
- for(int y = 0; y < towerBlueprint.length; y++){
- for(int z = 0; z < towerBlueprint[y].length; z++){
- for(int x = 0; x < towerBlueprint[y][z].length; x++){
-
- BlockPos pos = origin.add(x - width / 2, y, z - width / 2);
-
- switch(orientation){
- case WEST:
- x1 = flip ? width - x : x;
- z1 = z;
- break;
- case NORTH:
- x1 = z;
- z1 = flip ? x : width - x;
- break;
- case EAST:
- x1 = flip ? x : width - x;
- z1 = width - z;
- break;
- case SOUTH:
- x1 = width - z;
- z1 = flip ? width - x : x;
- break;
- default:
- break;
- }
-
- if(blockStateList[towerBlueprint[y][z1][x1]] != null
- && blockStateList[towerBlueprint[y][z1][x1]].getBlock() != Blocks.TORCH
- && blockStateList[towerBlueprint[y][z1][x1]].getBlock() != Blocks.CHEST){
-
- if(blockStateList[towerBlueprint[y][z1][x1]].getBlock() == Blocks.OAK_DOOR){
- // Rotates the door depending on whether flip is true.
- ItemDoor.placeDoor(
- world, pos, flip
- ? EnumFacing.byHorizontalIndex(3 - orientation.getHorizontalIndex())
- .getOpposite()
- : orientation.rotateYCCW(),
- Blocks.OAK_DOOR, false);
- }else{
- world.setBlockState(pos, blockStateList[towerBlueprint[y][z1][x1]], 2);
- }
-
- blocksPlaced.add(pos);
-
- }
- }
- }
- }
-
- // == Extras ==
-
- // Torches are done afterwards so they don't fall off
- // Chests are also done afterwards, because for some reason the chest decides which way round to face
- // itself, after it has been placed, based on the surrounding blocks... but if it was placed during
- // the rest of the tower generation, some of those blocks wouldn't exist, hence it must be done here.
- for(int y = 0; y < towerBlueprint.length; y++){
- for(int z = 0; z < towerBlueprint[y].length; z++){
- for(int x = 0; x < towerBlueprint[y][z].length; x++){
-
- BlockPos pos = origin.add(x - width / 2, y, z - width / 2);
-
- switch(orientation){
- case WEST:
- x1 = flip ? width - x : x;
- z1 = z;
- break;
- case NORTH:
- x1 = z;
- z1 = flip ? x : width - x;
- break;
- case EAST:
- x1 = flip ? x : width - x;
- z1 = width - z;
- break;
- case SOUTH:
- x1 = width - z;
- z1 = flip ? width - x : x;
- break;
- default:
- break;
- }
-
- if(blockStateList[towerBlueprint[y][z1][x1]] != null){
-
- if(blockStateList[towerBlueprint[y][z1][x1]].getBlock() == Blocks.TORCH){
- if(placeTorch(world, pos, true)){
- blocksPlaced.add(pos);
- }else{
- Wizardry.logger.info("Attempted to generate a torch at " + pos + " in " + world
- + ", but failed!");
- }
- }
-
- // World should always be a WorldServer, but it's worth checking anyway.
- if(blockStateList[towerBlueprint[y][z1][x1]].getBlock() == Blocks.CHEST
- && world instanceof WorldServer){
- if(placeChest(world, pos)){
- blocksPlaced.add(pos);
- LootTable table = world.getLootTableManager().getLootTableFromLocation(
- new ResourceLocation(Wizardry.MODID, "chests/wizard_tower"));
- IInventory inventory = (IInventory)world.getTileEntity(pos);
- LootContext context = new LootContext.Builder((WorldServer)world).build();
- table.fillInventory(inventory, random, context);
- }else{
- Wizardry.logger.info("Attempted to generate a chest at " + pos + " in " + world
- + ", but failed!");
- }
- }
- }
- }
- }
- }
-
- if(evilWizard){
-
- EntityEvilWizard wizard = new EntityEvilWizard(world);
- wizard.hasTower = true;
- wizard.setLocationAndAngles(origin.getX() + 1.5, origin.getY() + towerBlueprint.length - 9.5,
- origin.getZ() + 1.5, 0, 0);
- wizard.onInitialSpawn(world.getDifficultyForLocation(origin), null);
-
- world.spawnEntity(wizard);
-
- }else{
-
- EntityWizard wizard = new EntityWizard(world);
- wizard.setLocationAndAngles(origin.getX() + 1.5, origin.getY() + towerBlueprint.length - 9.5,
- origin.getZ() + 1.5, 0, 0);
- wizard.onInitialSpawn(world.getDifficultyForLocation(origin), null);
- wizard.setTowerBlocks(blocksPlaced);
-
- world.spawnEntity(wizard);
- }
- }
- }
- }
-
- /**
- * Places a torch at the given position in the given world and automatically assigns an appropriate state. Order of
- * priority is U-S-W-N-E, or S-W-N-E-U if wallPriority is true.
- *
- * @param world The world to place the torch in.
- * @param pos The position to place the torch at.
- * @param wallPriority True to prioritise wall torches, false to prioritise floor torches.
- * @return True if the torch was placed, false if it is not possible.
- */
- private static boolean placeTorch(World world, BlockPos pos, boolean wallPriority){
-
- for(EnumFacing facing : ArrayUtils.add(EnumFacing.HORIZONTALS, wallPriority ? 4 : 0, EnumFacing.UP)){
- if(world.isSideSolid(pos.offset(facing.getOpposite()), facing)){
- world.setBlockState(pos, Blocks.TORCH.getDefaultState().withProperty(BlockTorch.FACING, facing));
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Places a chest at the given position in the given world and automatically assigns an appropriate state. Order of
- * priority is S-W-N-E.
- *
- * @param world The world to chest the torch in.
- * @param pos The position to chest the torch at.
- * @return True if the chest was placed, false if it is not possible.
- */
- private static boolean placeChest(World world, BlockPos pos){
-
- for(EnumFacing facing : EnumFacing.HORIZONTALS){
- if(world.isAirBlock(pos.offset(facing))){
- world.setBlockState(pos, Blocks.CHEST.getDefaultState().withProperty(BlockChest.FACING, facing));
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Checks whether a tower generated at the given coordinates will intersect any solid or liquid blocks. Only tests
- * for liquids (not solid blocks) for the first four layers to account for the floor and for sloping terrain.
- *
- * @return True if none of the blocks which the tower would replace are solid or liquid. Dirt, stone etc., water and
- * lava count, as do logs, but leaves and plants don't.
- */
- private static boolean checkSpaceForTower(World world, BlockPos pos, int[][][] towerBlueprint,
- EnumFacing orientation, boolean flip){
-
- // x, y and z are the position the block is being put in.
- // x1, y and z1 are the position in the blueprint which determines which block is being placed.
-
- int x1 = 0, z1 = 0;
-
- // It is assumed that the width of the blueprint is the same all the way up, and that the layers are square.
- int width = towerBlueprint[0].length - 1;
-
- for(int y = 0; y < towerBlueprint.length; y++){
- for(int z = 0; z < towerBlueprint[y].length; z++){
- for(int x = 0; x < towerBlueprint[y][z].length; x++){
-
- BlockPos pos1 = pos.add(x - width / 2, y, z - width / 2);
-
- switch(orientation){
- case WEST:
- x1 = flip ? width - x : x;
- z1 = z;
- break;
- case NORTH:
- x1 = z;
- z1 = flip ? x : width - x;
- break;
- case EAST:
- x1 = flip ? x : width - x;
- z1 = width - z;
- break;
- case SOUTH:
- x1 = width - z;
- z1 = flip ? width - x : x;
- break;
- default:
- break;
- }
-
- if(towerBlueprint[y][z1][x1] != 0 && !WizardryUtilities.canBlockBeReplacedB(world, pos1)
- // TODO: Is this a better replacement for the subsequent two lines?
- // && !world.getBlockState(pos1).getBlock().isFoliage(world, pos1)
- && !(world.getBlockState(pos1).getBlock() instanceof IPlantable)
- && !(world.getBlockState(pos1).getBlock() instanceof BlockLeaves)
- && (y > 3 || world.getBlockState(pos1).getBlock() instanceof BlockLiquid)){
- return false;
- }
- }
- }
- }
-
- return true;
- }
-
- /** Array of relative positions of each block in a layer of foundations. */
- private static final BlockPos[] foundationLayer = new BlockPos[]{new BlockPos(-2, 0, -1), new BlockPos(-2, 0, 0),
- new BlockPos(-2, 0, 1), new BlockPos(2, 0, -1), new BlockPos(2, 0, 0), new BlockPos(2, 0, 1),
- new BlockPos(-1, 0, -2), new BlockPos(0, 0, -2), new BlockPos(1, 0, -2), new BlockPos(-1, 0, 2),
- new BlockPos(0, 0, 2), new BlockPos(1, 0, 2),};
-
- /**
- * 3D matrix of integers representing the different blocks which make up the wizard tower. The blocks corresponding
- * to each integer are as follows (note that some blocks change depending on the biome):
- *
- * 0 Nothing (keep existing block)
- * 1 Air (remove existing block)
- * 2 Floor (planks)
- * 3 Bookshelf
- * 4 Roof (stained clay)
- * 5 Floor slab (lower half)
- * 6 Floor slab (upper half)
- * 7 Wall (cobblestone by default)
- * 8 Glass pane
- * 9 Door (metadata is handled by the built-in vanilla method)
- * 10 Arcane workbench
- * 11 Torch (metadata is handled separately depending on adjacent blocks)
- * 12 Chest (only generates if the wizard is evil, otherwise places a bookshelf instead)
- */
- private static final int[][][] towerBlueprintSmall = {
- // x is horizontal, z is vertical, y is layers
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 2, 2, 2, 0, 0, 0}, {0, 0, 0, 2, 2, 2, 0, 0, 0}, {0, 0, 0, 2, 2, 2, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 9, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 6, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 0, 7, 0, 0, 0}, {0, 0, 0, 11, 1, 11, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 11, 7, 0, 0}, {0, 0, 7, 5, 6, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 6, 7, 0, 0}, {0, 0, 7, 1, 1, 5, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 11, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 7, 2, 2, 2, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 2, 7, 0}, {0, 7, 5, 2, 2, 6, 2, 7, 0}, {0, 7, 2, 2, 2, 2, 2, 7, 0},
- {0, 0, 7, 2, 2, 2, 7, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 7, 3, 3, 3, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 10, 1, 3, 7, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 7, 11, 1, 11, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 8, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 1, 1, 3, 7, 0},
- {0, 0, 7, 11, 1, 11, 7, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 4, 7, 7, 7, 4, 0, 0}, {0, 4, 7, 1, 1, 1, 7, 4, 0},
- {4, 7, 1, 1, 1, 1, 3, 7, 4}, {4, 7, 1, 1, 1, 1, 3, 7, 4}, {4, 7, 1, 1, 1, 1, 12, 7, 4},
- {0, 4, 7, 1, 1, 1, 7, 4, 0}, {0, 0, 4, 7, 7, 7, 4, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0},
- {0, 4, 1, 1, 1, 1, 1, 4, 0}, {0, 4, 1, 1, 1, 1, 1, 4, 0}, {0, 4, 1, 1, 1, 1, 1, 4, 0},
- {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},
- {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0},
- {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0},
- {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0},
- {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},}};
-
- /**
- * 3D matrix of integers representing the different blocks which make up the wizard tower. The blocks corresponding
- * to each integer are as follows (note that some blocks change depending on the biome):
- *
- * 0 Nothing (keep existing block)
- * 1 Air (remove existing block)
- * 2 Floor (planks)
- * 3 Bookshelf
- * 4 Roof (stained clay)
- * 5 Floor slab (lower half)
- * 6 Floor slab (upper half)
- * 7 Wall (cobblestone by default)
- * 8 Glass pane
- * 9 Door (metadata is handled by the built-in vanilla method)
- * 10 Arcane workbench
- * 11 Torch (metadata is handled separately depending on adjacent blocks)
- * 12 Chest (only generates if the wizard is evil, otherwise places a bookshelf instead)
- */
- private static final int[][][] towerBlueprintMedium = {
- // x is horizontal, z is vertical, y is layers
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 2, 2, 2, 0, 0, 0}, {0, 0, 0, 2, 2, 2, 0, 0, 0}, {0, 0, 0, 2, 2, 2, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 9, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 6, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 0, 7, 0, 0, 0}, {0, 0, 0, 11, 1, 11, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 11, 7, 0, 0}, {0, 0, 7, 5, 6, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 6, 7, 0, 0}, {0, 0, 7, 1, 1, 5, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 11, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 6, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 11, 7, 0, 0}, {0, 0, 7, 5, 6, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 6, 7, 0, 0}, {0, 0, 7, 1, 1, 5, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 11, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 7, 2, 2, 2, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 2, 7, 0}, {0, 7, 5, 2, 2, 6, 2, 7, 0}, {0, 7, 2, 2, 2, 2, 2, 7, 0},
- {0, 0, 7, 2, 2, 2, 7, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 7, 3, 3, 3, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 10, 1, 3, 7, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 7, 11, 1, 11, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 8, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 1, 1, 3, 7, 0},
- {0, 0, 7, 11, 1, 11, 7, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 4, 7, 7, 7, 4, 0, 0}, {0, 4, 7, 1, 1, 1, 7, 4, 0},
- {4, 7, 1, 1, 1, 1, 3, 7, 4}, {4, 7, 1, 1, 1, 1, 3, 7, 4}, {4, 7, 1, 1, 1, 1, 12, 7, 4},
- {0, 4, 7, 1, 1, 1, 7, 4, 0}, {0, 0, 4, 7, 7, 7, 4, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0},
- {0, 4, 1, 1, 1, 1, 1, 4, 0}, {0, 4, 1, 1, 1, 1, 1, 4, 0}, {0, 4, 1, 1, 1, 1, 1, 4, 0},
- {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},
- {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0},
- {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0},
- {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0},
- {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},}};
-
- /**
- * 3D matrix of integers representing the different blocks which make up the wizard tower. The blocks corresponding
- * to each integer are as follows (note that some blocks change depending on the biome):
- *
- * 0 Nothing (keep existing block)
- * 1 Air (remove existing block)
- * 2 Floor (planks)
- * 3 Bookshelf
- * 4 Roof (stained clay)
- * 5 Floor slab (lower half)
- * 6 Floor slab (upper half)
- * 7 Wall (cobblestone by default)
- * 8 Glass pane
- * 9 Door (metadata is handled by the built-in vanilla method)
- * 10 Arcane workbench
- * 11 Torch (metadata is handled separately depending on adjacent blocks)
- * 12 Chest (only generates if the wizard is evil, otherwise places a bookshelf instead)
- */
- private static final int[][][] towerBlueprintTall = {
- // x is horizontal, z is vertical, y is layers
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 2, 2, 2, 0, 0, 0}, {0, 0, 0, 2, 2, 2, 0, 0, 0}, {0, 0, 0, 2, 2, 2, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 9, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 6, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 0, 7, 0, 0, 0}, {0, 0, 0, 11, 1, 11, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 11, 7, 0, 0}, {0, 0, 7, 5, 6, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 6, 7, 0, 0}, {0, 0, 7, 1, 1, 5, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0}, {0, 0, 0, 1, 1, 1, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 11, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 6, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 11, 7, 0, 0}, {0, 0, 7, 5, 6, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 6, 7, 0, 0}, {0, 0, 7, 1, 1, 5, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 11, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 6, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 11, 7, 0, 0}, {0, 0, 7, 5, 6, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 6, 7, 0, 0}, {0, 0, 7, 1, 1, 5, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 1, 6, 5, 7, 0, 0}, {0, 0, 7, 11, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0},
- {0, 0, 7, 5, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 7, 1, 1, 1, 7, 0, 0},
- {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 7, 2, 2, 2, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 2, 7, 0}, {0, 7, 5, 2, 2, 6, 2, 7, 0}, {0, 7, 2, 2, 2, 2, 2, 7, 0},
- {0, 0, 7, 2, 2, 2, 7, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 7, 3, 3, 3, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 10, 1, 3, 7, 0},
- {0, 0, 7, 1, 1, 1, 7, 0, 0}, {0, 0, 0, 7, 7, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 7, 11, 1, 11, 7, 0, 0},
- {0, 7, 1, 1, 1, 1, 3, 7, 0}, {0, 8, 1, 1, 1, 1, 3, 7, 0}, {0, 7, 1, 1, 1, 1, 3, 7, 0},
- {0, 0, 7, 11, 1, 11, 7, 0, 0}, {0, 0, 0, 7, 8, 7, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 4, 7, 7, 7, 4, 0, 0}, {0, 4, 7, 1, 1, 1, 7, 4, 0},
- {4, 7, 1, 1, 1, 1, 3, 7, 4}, {4, 7, 1, 1, 1, 1, 3, 7, 4}, {4, 7, 1, 1, 1, 1, 12, 7, 4},
- {0, 4, 7, 1, 1, 1, 7, 4, 0}, {0, 0, 4, 7, 7, 7, 4, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0},
- {0, 4, 1, 1, 1, 1, 1, 4, 0}, {0, 4, 1, 1, 1, 1, 1, 4, 0}, {0, 4, 1, 1, 1, 1, 1, 4, 0},
- {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},
- {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0},
- {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0},
- {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 4, 1, 1, 1, 4, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0},
- {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 4, 4, 4, 0, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 0, 4, 4, 4, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 4, 1, 4, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0},}};
-
- /**
- * 3D matrix of integers representing the different blocks which make up the wizard tower. The blocks corresponding
- * to each integer are as follows (note that some blocks change depending on the biome):
- *
- * 0 Nothing (keep existing block)
- * 1 Air (remove existing block)
- * 2 Floor (planks)
- * 3 Bookshelf
- * 4 Roof (stained clay)
- * 5 Floor slab (lower half)
- * 6 Floor slab (upper half)
- * 7 Wall (cobblestone by default)
- * 8 Glass pane
- * 9 Door (metadata is handled by the built-in vanilla method)
- * 10 Arcane workbench
- * 11 Torch (metadata is handled separately depending on adjacent blocks)
- * 12 Chest (only generates if the wizard is evil, otherwise places a bookshelf instead)
- */
- private static final int[][][] towerBlueprintDouble = {
- // x is horizontal, z is vertical, y is layers
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 6, 5, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 9, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 5, 1, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 6, 1, 1, 7, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 0, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 11, 1, 11, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 8, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 1, 1, 11, 7, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 5, 6, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 1, 1, 6, 7, 7, 7, 0, 0}, {0, 0, 0, 0, 7, 1, 1, 5, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 6, 5, 7, 7, 7, 0, 0},
- {0, 0, 0, 0, 7, 11, 1, 1, 1, 1, 1, 7, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 7, 7, 0, 0},
- {0, 0, 0, 0, 0, 7, 8, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 5, 1, 1, 7, 7, 8, 7, 0},
- {0, 0, 0, 0, 7, 6, 1, 1, 1, 1, 1, 8, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 7, 8, 7, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 8, 7, 0, 4, 4, 4, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 7, 7, 7, 4},
- {0, 0, 0, 0, 7, 1, 1, 11, 7, 1, 1, 7, 4}, {0, 0, 0, 0, 7, 5, 6, 1, 7, 7, 7, 7, 4},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 4, 4, 4, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 4, 4, 4, 0},
- {0, 0, 0, 0, 7, 1, 1, 6, 7, 4, 1, 4, 0}, {0, 0, 0, 0, 7, 1, 1, 5, 7, 4, 4, 4, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 1, 6, 5, 7, 0, 4, 0, 0},
- {0, 0, 0, 0, 7, 11, 1, 1, 7, 4, 1, 4, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 4, 0, 0},
- {0, 0, 0, 0, 0, 7, 8, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 7, 5, 1, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 4, 0, 0}, {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 2, 2, 2, 7, 0, 0, 0, 0}, {0, 0, 0, 7, 1, 1, 1, 1, 2, 7, 0, 0, 0},
- {0, 0, 0, 7, 5, 2, 2, 6, 2, 7, 4, 0, 0}, {0, 0, 0, 7, 2, 2, 2, 2, 2, 7, 0, 0, 0},
- {0, 0, 0, 0, 7, 2, 2, 2, 7, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 3, 3, 3, 7, 0, 0, 0, 0}, {0, 0, 0, 7, 1, 1, 1, 1, 3, 7, 0, 0, 0},
- {0, 0, 0, 7, 1, 1, 1, 1, 3, 7, 0, 0, 0}, {0, 0, 0, 7, 1, 1, 10, 1, 3, 7, 0, 0, 0},
- {0, 0, 0, 0, 7, 1, 1, 1, 7, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 7, 8, 7, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 7, 11, 1, 11, 7, 0, 0, 0, 0}, {0, 0, 0, 7, 1, 1, 1, 1, 3, 7, 0, 0, 0},
- {0, 0, 0, 8, 1, 1, 1, 1, 3, 7, 0, 0, 0}, {0, 0, 0, 7, 1, 1, 1, 1, 3, 7, 0, 0, 0},
- {0, 0, 0, 0, 7, 11, 1, 11, 7, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 7, 8, 7, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 7, 7, 7, 4, 0, 0, 0, 0},
- {0, 0, 0, 4, 7, 1, 1, 1, 7, 4, 0, 0, 0}, {0, 0, 4, 7, 1, 1, 1, 1, 3, 7, 4, 0, 0},
- {0, 0, 4, 7, 1, 1, 1, 1, 3, 7, 4, 0, 0}, {0, 0, 4, 7, 1, 1, 1, 1, 12, 7, 4, 0, 0},
- {0, 0, 0, 4, 7, 1, 1, 1, 7, 4, 0, 0, 0}, {0, 0, 0, 0, 4, 7, 7, 7, 4, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 4, 1, 1, 1, 4, 0, 0, 0, 0}, {0, 0, 0, 4, 1, 1, 1, 1, 1, 4, 0, 0, 0},
- {0, 0, 0, 4, 1, 1, 1, 1, 1, 4, 0, 0, 0}, {0, 0, 0, 4, 1, 1, 1, 1, 1, 4, 0, 0, 0},
- {0, 0, 0, 0, 4, 1, 1, 1, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 1, 1, 1, 4, 0, 0, 0, 0},
- {0, 0, 0, 0, 4, 1, 1, 1, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 4, 1, 1, 1, 4, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 4, 1, 4, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 4, 1, 1, 1, 4, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 4, 1, 4, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 4, 1, 4, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 4, 1, 4, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},},
- {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},}};
-
-}
diff --git a/src/main/java/electroblob/wizardry/advancement/AdvancementHelper.java b/src/main/java/electroblob/wizardry/advancement/AdvancementHelper.java
deleted file mode 100644
index 1a725a97..00000000
--- a/src/main/java/electroblob/wizardry/advancement/AdvancementHelper.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package electroblob.wizardry.advancement;
-
-import electroblob.wizardry.Wizardry;
-import net.minecraft.advancements.Advancement;
-import net.minecraft.advancements.AdvancementManager;
-import net.minecraft.advancements.AdvancementProgress;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.entity.player.EntityPlayerMP;
-import net.minecraft.util.ResourceLocation;
-
-/**
- * This is a provisory class for the transition to 1.12
- * It contains an enum with all the advancements of this mod
- * and an helper to grant advancements on the server side only (like the command)
- * @author Corail31
- * @since Wizardry 4.1
- */
-public class AdvancementHelper {
- public enum EnumAdvancement {
- crystal,
- arcane_initiate,
- apprentice,
- master,
- all_spells,
- wizard_trade,
- buy_master_spell,
- freeze_blaze,
- charge_creeper,
- frankenstein,
- special_upgrade,
- craft_flask,
- elemental,
- armour_set,
- legendary,
- self_destruct,
- pig_tornado,
- jam_wizard,
- slime_skeleton,
- anger_wizard,
- defeat_evil_wizard,
- max_out_wand,
- element_master,
- identify_spell;
- }
-
- public static boolean grantAdvancement(EntityPlayer player, EnumAdvancement advancementName) {
- if (player == null) { return false; }
- if (player.world.isRemote) { return true; }
- EntityPlayerMP player_mp = player.getServer().getPlayerList().getPlayerByUUID(player.getUniqueID());
- AdvancementManager am = player_mp.getServerWorld().getAdvancementManager();
- Advancement advancement = am.getAdvancement(new ResourceLocation(Wizardry.MODID, advancementName.name()));
- if (advancement == null) { return false; }
- AdvancementProgress advancementprogress = player_mp.getAdvancements().getProgress(advancement);
- if (!advancementprogress.isDone()) {
- for (String criteria : advancementprogress.getRemaningCriteria()) {
- player_mp.getAdvancements().grantCriterion(advancement, criteria);
- }
- }
- return true;
- }
-}
diff --git a/src/main/java/electroblob/wizardry/advancement/ArcaneWorkbenchTrigger.java b/src/main/java/electroblob/wizardry/advancement/ArcaneWorkbenchTrigger.java
new file mode 100644
index 00000000..175f9cdf
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/advancement/ArcaneWorkbenchTrigger.java
@@ -0,0 +1,135 @@
+package electroblob.wizardry.advancement;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonObject;
+import net.minecraft.advancements.ICriterionTrigger;
+import net.minecraft.advancements.PlayerAdvancements;
+import net.minecraft.advancements.critereon.AbstractCriterionInstance;
+import net.minecraft.advancements.critereon.ItemPredicate;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ResourceLocation;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Advancement trigger for things done in the arcane workbench. The majority of any
+ * ICriterionTrigger class is just boilerplate, and this is no exception. */
+public class ArcaneWorkbenchTrigger implements ICriterionTrigger {
+
+ private final ResourceLocation id;
+ private final Map listeners = Maps.newHashMap();
+
+ public ArcaneWorkbenchTrigger(ResourceLocation id){
+ this.id = id;
+ }
+
+ public ResourceLocation getId(){
+ return this.id;
+ }
+
+ public void addListener(PlayerAdvancements advancements, Listener listener){
+
+ ArcaneWorkbenchTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners == null){
+ listeners = new ArcaneWorkbenchTrigger.Listeners(advancements);
+ this.listeners.put(advancements, listeners);
+ }
+
+ listeners.add(listener);
+ }
+
+ public void removeListener(PlayerAdvancements advancements, Listener listener){
+
+ ArcaneWorkbenchTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners != null){
+ listeners.remove(listener);
+
+ if(listeners.isEmpty()){
+ this.listeners.remove(advancements);
+ }
+ }
+ }
+
+ public void removeAllListeners(PlayerAdvancements advancements){
+ this.listeners.remove(advancements);
+ }
+
+ public ArcaneWorkbenchTrigger.Instance deserializeInstance(JsonObject json, JsonDeserializationContext context){
+ return new ArcaneWorkbenchTrigger.Instance(this.id, ItemPredicate.deserialize(json.get("item")));
+ }
+
+ public void trigger(EntityPlayerMP player, ItemStack stack){
+
+ ArcaneWorkbenchTrigger.Listeners listeners = this.listeners.get(player.getAdvancements());
+
+ if(listeners != null){
+ listeners.trigger(stack);
+ }
+ }
+
+ public static class Instance extends AbstractCriterionInstance {
+
+ private final ItemPredicate item;
+
+ public Instance(ResourceLocation criterionIn, ItemPredicate item){
+ super(criterionIn);
+ this.item = item;
+ }
+
+ public boolean test(ItemStack stack){
+ return this.item.test(stack);
+ }
+ }
+
+ static class Listeners {
+
+ private final PlayerAdvancements playerAdvancements;
+ private final Set> listeners = Sets.newHashSet();
+
+ public Listeners(PlayerAdvancements advancements){
+ this.playerAdvancements = advancements;
+ }
+
+ public boolean isEmpty(){
+ return this.listeners.isEmpty();
+ }
+
+ public void add(Listener listener){
+ this.listeners.add(listener);
+ }
+
+ public void remove(Listener listener){
+ this.listeners.remove(listener);
+ }
+
+ public void trigger(ItemStack stack){
+
+ List> list = null;
+
+ for(Listener listener : this.listeners){
+
+ if(listener.getCriterionInstance().test(stack)){
+
+ if(list == null){
+ list = Lists.newArrayList();
+ }
+
+ list.add(listener);
+ }
+ }
+
+ if(list != null){
+ for(Listener listener : list){
+ listener.grantCriterion(this.playerAdvancements);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/util/CustomAdvancementTrigger.java b/src/main/java/electroblob/wizardry/advancement/CustomAdvancementTrigger.java
similarity index 96%
rename from src/main/java/electroblob/wizardry/util/CustomAdvancementTrigger.java
rename to src/main/java/electroblob/wizardry/advancement/CustomAdvancementTrigger.java
index 8fb2693a..21a1cf99 100644
--- a/src/main/java/electroblob/wizardry/util/CustomAdvancementTrigger.java
+++ b/src/main/java/electroblob/wizardry/advancement/CustomAdvancementTrigger.java
@@ -1,10 +1,9 @@
-package electroblob.wizardry.util;
+package electroblob.wizardry.advancement;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonObject;
-
import electroblob.wizardry.Wizardry;
import net.minecraft.advancements.ICriterionInstance;
import net.minecraft.advancements.ICriterionTrigger;
@@ -34,7 +33,7 @@ public class CustomAdvancementTrigger implements ICriterionTrigger {
+
+ private final ResourceLocation id;
+ private final Map listeners = Maps.newHashMap();
+
+ public SpellCastTrigger(ResourceLocation id){
+ this.id = id;
+ }
+
+ public ResourceLocation getId(){
+ return this.id;
+ }
+
+ public void addListener(PlayerAdvancements advancements, Listener listener){
+
+ SpellCastTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners == null){
+ listeners = new SpellCastTrigger.Listeners(advancements);
+ this.listeners.put(advancements, listeners);
+ }
+
+ listeners.add(listener);
+ }
+
+ public void removeListener(PlayerAdvancements advancements, Listener listener){
+
+ SpellCastTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners != null){
+ listeners.remove(listener);
+
+ if(listeners.isEmpty()){
+ this.listeners.remove(advancements);
+ }
+ }
+ }
+
+ public void removeAllListeners(PlayerAdvancements advancements){
+ this.listeners.remove(advancements);
+ }
+
+ public SpellCastTrigger.Instance deserializeInstance(JsonObject json, JsonDeserializationContext context){
+ return new SpellCastTrigger.Instance(this.id, SpellPredicate.deserialize(json.get("spell")),
+ ItemPredicate.deserialize(json.get("item")));
+ }
+
+ public void trigger(EntityPlayerMP player, Spell spell, ItemStack stack){
+
+ SpellCastTrigger.Listeners listeners = this.listeners.get(player.getAdvancements());
+
+ if(listeners != null){
+ listeners.trigger(spell, stack);
+ }
+ }
+
+ public static class Instance extends AbstractCriterionInstance {
+
+ private final SpellPredicate spell;
+ private final ItemPredicate item;
+
+ public Instance(ResourceLocation criterion, SpellPredicate spell, ItemPredicate item){
+ super(criterion);
+ this.spell = spell;
+ this.item = item;
+ }
+
+ public boolean test(Spell spell, ItemStack stack){
+ return this.spell.test(spell) && item.test(stack);
+ }
+ }
+
+ static class Listeners {
+
+ private final PlayerAdvancements playerAdvancements;
+ private final Set> listeners = Sets.newHashSet();
+
+ public Listeners(PlayerAdvancements advancements){
+ this.playerAdvancements = advancements;
+ }
+
+ public boolean isEmpty(){
+ return this.listeners.isEmpty();
+ }
+
+ public void add(Listener listener){
+ this.listeners.add(listener);
+ }
+
+ public void remove(Listener listener){
+ this.listeners.remove(listener);
+ }
+
+ public void trigger(Spell spell, ItemStack stack){
+
+ List> list = null;
+
+ for(Listener listener : this.listeners){
+
+ if(listener.getCriterionInstance().test(spell, stack)){
+
+ if(list == null){
+ list = Lists.newArrayList();
+ }
+
+ list.add(listener);
+ }
+ }
+
+ if(list != null){
+ for(Listener listener : list){
+ listener.grantCriterion(this.playerAdvancements);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/advancement/SpellDiscoveryTrigger.java b/src/main/java/electroblob/wizardry/advancement/SpellDiscoveryTrigger.java
new file mode 100644
index 00000000..f24fef36
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/advancement/SpellDiscoveryTrigger.java
@@ -0,0 +1,143 @@
+package electroblob.wizardry.advancement;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.event.DiscoverSpellEvent;
+import electroblob.wizardry.spell.Spell;
+import net.minecraft.advancements.ICriterionTrigger;
+import net.minecraft.advancements.PlayerAdvancements;
+import net.minecraft.advancements.critereon.AbstractCriterionInstance;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Advancement trigger that is triggered when a spell is discovered. The majority of any
+ * ICriterionTrigger class is just boilerplate, and this is no exception. */
+public class SpellDiscoveryTrigger implements ICriterionTrigger {
+
+ private final ResourceLocation id;
+ private final Map listeners = Maps.newHashMap();
+
+ public SpellDiscoveryTrigger(ResourceLocation id){
+ this.id = id;
+ }
+
+ public ResourceLocation getId(){
+ return this.id;
+ }
+
+ public void addListener(PlayerAdvancements advancements, Listener listener){
+
+ SpellDiscoveryTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners == null){
+ listeners = new SpellDiscoveryTrigger.Listeners(advancements);
+ this.listeners.put(advancements, listeners);
+ }
+
+ listeners.add(listener);
+ }
+
+ public void removeListener(PlayerAdvancements advancements, Listener listener){
+
+ SpellDiscoveryTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners != null){
+ listeners.remove(listener);
+
+ if(listeners.isEmpty()){
+ this.listeners.remove(advancements);
+ }
+ }
+ }
+
+ public void removeAllListeners(PlayerAdvancements advancements){
+ this.listeners.remove(advancements);
+ }
+
+ public SpellDiscoveryTrigger.Instance deserializeInstance(JsonObject json, JsonDeserializationContext context){
+
+ String s = JsonUtils.getString(json, "source");
+ DiscoverSpellEvent.Source source = DiscoverSpellEvent.Source.byName(s);
+ if(source == null) throw new JsonSyntaxException("No such spell discovery source: " + s);
+ return new SpellDiscoveryTrigger.Instance(this.id, SpellPredicate.deserialize(json.get("spell")), source);
+ }
+
+ public void trigger(EntityPlayerMP player, Spell spell, DiscoverSpellEvent.Source source){
+
+ SpellDiscoveryTrigger.Listeners listeners = this.listeners.get(player.getAdvancements());
+
+ if(listeners != null){
+ listeners.trigger(spell, source);
+ }
+ }
+
+ public static class Instance extends AbstractCriterionInstance {
+
+ private final SpellPredicate spell;
+ private final DiscoverSpellEvent.Source source;
+
+ public Instance(ResourceLocation criterion, SpellPredicate spell, DiscoverSpellEvent.Source source){
+ super(criterion);
+ this.spell = spell;
+ this.source = source;
+ }
+
+ public boolean test(Spell spell, DiscoverSpellEvent.Source source){
+ return this.spell.test(spell) && source == this.source;
+ }
+ }
+
+ static class Listeners {
+
+ private final PlayerAdvancements playerAdvancements;
+ private final Set> listeners = Sets.newHashSet();
+
+ public Listeners(PlayerAdvancements advancements){
+ this.playerAdvancements = advancements;
+ }
+
+ public boolean isEmpty(){
+ return this.listeners.isEmpty();
+ }
+
+ public void add(Listener listener){
+ this.listeners.add(listener);
+ }
+
+ public void remove(Listener listener){
+ this.listeners.remove(listener);
+ }
+
+ public void trigger(Spell spell, DiscoverSpellEvent.Source source){
+
+ List> list = null;
+
+ for(Listener listener : this.listeners){
+
+ if(listener.getCriterionInstance().test(spell, source)){
+
+ if(list == null){
+ list = Lists.newArrayList();
+ }
+
+ list.add(listener);
+ }
+ }
+
+ if(list != null){
+ for(Listener listener : list){
+ listener.grantCriterion(this.playerAdvancements);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/advancement/SpellPredicate.java b/src/main/java/electroblob/wizardry/advancement/SpellPredicate.java
new file mode 100644
index 00000000..24d6c694
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/advancement/SpellPredicate.java
@@ -0,0 +1,117 @@
+package electroblob.wizardry.advancement;
+
+import com.google.common.collect.Streams;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.constants.Tier;
+import electroblob.wizardry.spell.Spell;
+import net.minecraft.util.JsonUtils;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+
+/** Predicate used by advancement triggers to match spells. */
+public class SpellPredicate {
+
+ public static final SpellPredicate ANY = new SpellPredicate();
+ private final Spell spell;
+ private final Tier[] tiers;
+ private final Element[] elements;
+
+ public SpellPredicate(){
+ this.spell = null;
+ this.tiers = Tier.values();
+ this.elements = Element.values();
+ }
+
+ public SpellPredicate(@Nullable Spell spell, Tier[] tiers, Element[] elements){
+ this.spell = spell;
+ this.tiers = tiers;
+ this.elements = elements;
+ }
+
+ public boolean test(Spell spell){
+
+ if(this.spell != null && spell != this.spell){
+ return false;
+ }else if(!Arrays.asList(this.tiers).contains(spell.getTier())){
+ return false;
+ }else if(!Arrays.asList(this.elements).contains(spell.getElement())){
+ return false;
+ }
+
+ return true;
+ }
+
+ public static SpellPredicate deserialize(@Nullable JsonElement element){
+
+ if(element != null && !element.isJsonNull()){
+
+ JsonObject jsonobject = JsonUtils.getJsonObject(element, "spell");
+
+ Spell spell = null;
+
+ if(jsonobject.has("spell")){
+
+ String s = JsonUtils.getString(jsonobject, "spell");
+ spell = Spell.get(s);
+
+ if(spell == null){
+ throw new JsonSyntaxException("Unknown spell id '" + s + "'");
+ }
+ }
+
+ Tier[] tiers = Tier.values();
+
+ if(jsonobject.has("tiers")){
+ try{
+ JsonArray array = JsonUtils.getJsonArray(jsonobject, "tiers");
+ tiers = Streams.stream(array)
+ .map(je -> Tier.fromName(JsonUtils.getString(je, "element of array tiers")))
+ .toArray(Tier[]::new);
+ }catch(IllegalArgumentException e){
+ throw new JsonSyntaxException("Incorrect spell predicate value", e);
+ }
+ }
+
+ Element[] elements = Element.values();
+
+ if(jsonobject.has("elements")){
+ try{
+ JsonArray array = JsonUtils.getJsonArray(jsonobject, "elements");
+ elements = Streams.stream(array)
+ .map(je -> Element.fromName(JsonUtils.getString(je, "element of array elements")))
+ .toArray(Element[]::new);
+ }catch(IllegalArgumentException e){
+ throw new JsonSyntaxException("Incorrect spell predicate value", e);
+ }
+ }
+
+ return new SpellPredicate(spell, tiers, elements);
+
+ }else{
+ return ANY;
+ }
+ }
+
+ public static SpellPredicate[] deserializeArray(@Nullable JsonElement element){
+
+ if(element != null && !element.isJsonNull()){
+
+ JsonArray jsonarray = JsonUtils.getJsonArray(element, "spells");
+ SpellPredicate[] predicates = new SpellPredicate[jsonarray.size()];
+
+ for(int i = 0; i < predicates.length; ++i){
+ predicates[i] = deserialize(jsonarray.get(i));
+ }
+
+ return predicates;
+
+ }else{
+ return new SpellPredicate[0];
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/advancement/StructureTrigger.java b/src/main/java/electroblob/wizardry/advancement/StructureTrigger.java
new file mode 100644
index 00000000..aedcd6a4
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/advancement/StructureTrigger.java
@@ -0,0 +1,136 @@
+package electroblob.wizardry.advancement;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonObject;
+import electroblob.wizardry.worldgen.WorldGenSurfaceStructure;
+import net.minecraft.advancements.ICriterionTrigger;
+import net.minecraft.advancements.PlayerAdvancements;
+import net.minecraft.advancements.critereon.AbstractCriterionInstance;
+import net.minecraft.entity.player.EntityPlayerMP;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.world.WorldServer;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Copied from PositionTrigger and modified to work with wizardry's structures. The majority of any
+ * ICriterionTrigger class is just boilerplate, and this is no exception. */
+public class StructureTrigger implements ICriterionTrigger {
+
+ private final ResourceLocation id;
+ private final Map listeners = Maps.newHashMap();
+
+ public StructureTrigger(ResourceLocation id){
+ this.id = id;
+ }
+
+ public ResourceLocation getId(){
+ return this.id;
+ }
+
+ public void addListener(PlayerAdvancements advancements, Listener listener){
+
+ StructureTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners == null){
+ listeners = new StructureTrigger.Listeners(advancements);
+ this.listeners.put(advancements, listeners);
+ }
+
+ listeners.add(listener);
+ }
+
+ public void removeListener(PlayerAdvancements advancements, Listener listener){
+
+ StructureTrigger.Listeners listeners = this.listeners.get(advancements);
+
+ if(listeners != null){
+ listeners.remove(listener);
+
+ if(listeners.isEmpty()){
+ this.listeners.remove(advancements);
+ }
+ }
+ }
+
+ public void removeAllListeners(PlayerAdvancements advancements){
+ this.listeners.remove(advancements);
+ }
+
+ public StructureTrigger.Instance deserializeInstance(JsonObject json, JsonDeserializationContext context){
+ return new StructureTrigger.Instance(this.id, JsonUtils.getString(json, "structure_type"));
+ }
+
+ public void trigger(EntityPlayerMP player){
+
+ StructureTrigger.Listeners listeners = this.listeners.get(player.getAdvancements());
+
+ if(listeners != null){
+ listeners.trigger(player.getServerWorld(), player.posX, player.posY, player.posZ);
+ }
+ }
+
+ public static class Instance extends AbstractCriterionInstance {
+
+ private final WorldGenSurfaceStructure structureType;
+
+ public Instance(ResourceLocation criterionIn, String name){
+ super(criterionIn);
+ this.structureType = WorldGenSurfaceStructure.byName(name);
+ }
+
+ public boolean test(WorldServer world, double x, double y, double z){
+ return structureType.isInsideStructure(world, x, y, z);
+ }
+ }
+
+ static class Listeners {
+
+ private final PlayerAdvancements playerAdvancements;
+ private final Set> listeners = Sets.newHashSet();
+
+ public Listeners(PlayerAdvancements advancements){
+ this.playerAdvancements = advancements;
+ }
+
+ public boolean isEmpty(){
+ return this.listeners.isEmpty();
+ }
+
+ public void add(Listener listener){
+ this.listeners.add(listener);
+ }
+
+ public void remove(Listener listener){
+ this.listeners.remove(listener);
+ }
+
+ public void trigger(WorldServer world, double x, double y, double z){
+
+ List> list = null;
+
+ for(Listener listener : this.listeners){
+
+ if(listener.getCriterionInstance().test(world, x, y, z)){
+
+ if(list == null){
+ list = Lists.newArrayList();
+ }
+
+ list.add(listener);
+ }
+ }
+
+ if(list != null){
+ for(Listener listener : list){
+ listener.grantCriterion(this.playerAdvancements);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/api/WizardryEnumHelper.java b/src/main/java/electroblob/wizardry/api/WizardryEnumHelper.java
new file mode 100644
index 00000000..4c73f606
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/api/WizardryEnumHelper.java
@@ -0,0 +1,96 @@
+package electroblob.wizardry.api;
+
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.constants.SpellType;
+import electroblob.wizardry.constants.Tier;
+import electroblob.wizardry.util.SpellProperties;
+import net.minecraft.util.text.Style;
+import net.minecraftforge.common.util.EnumHelper;
+
+/**
+ * This class contains methods similar to those in {@link EnumHelper} specific to wizardry's enum types.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.2
+ */
+public final class WizardryEnumHelper {
+
+ // Make sure these are updated if the relevant constructors are updated!
+ // Can't we do some kind of reflection to access them and generate these arrays?
+ private static final Class[] TIER_ARGUMENTS = new Class[]{Integer.class, Integer.class, Integer.class, Style.class, String.class};
+ private static final Class[] ELEMENT_ARGUMENTS = new Class[]{Style.class, String.class, String.class};
+ private static final Class[] SPELL_TYPE_ARGUMENTS = new Class[]{String.class};
+ private static final Class[] SPELL_CONTEXT_ARGUMENTS = new Class[]{String.class};
+
+ /**
+ * Wrapper for the generic method {@link EnumHelper#addEnum(Class, String, Class[], Object...)} which is
+ * specifically for adding new tiers. Use this method in preference to the generic one in case the constructor
+ * parameters change for {@code Tier}.
+ *
+ * As of version 4.2, wizardry now has partial support for externally-added tiers; you'll need to do some of the
+ * legwork yourself though.
+ *
+ * @param codeName The name of the enum constant in the code. This will be returned if you call toString() on the
+ * resulting enum constant; other than that it doesn't really make much difference.
+ * @param maxCharge The maximum charge for wands of this tier.
+ * @param upgradeLimit The maximum total number of special upgrades that can be applied to wands of this tier.
+ * @param weight The weight of this tier in the standard weighting.
+ * @param colour The colour of text associated with this tier, as a style object.
+ * @param name The unlocalised name of this tier, as used in translation keys.
+ * @return The resulting {@code Tier} enum constant.
+ */
+ public static Tier addTier(String codeName, int maxCharge, int upgradeLimit, int weight, Style colour, String name){
+ return EnumHelper.addEnum(Tier.class, codeName, TIER_ARGUMENTS, maxCharge, upgradeLimit, weight, colour, name);
+ }
+
+ /**
+ * Wrapper for the generic method {@link EnumHelper#addEnum(Class, String, Class[], Object...)} which is
+ * specifically for adding new elements. Use this method in preference to the generic one in case the constructor
+ * parameters change for {@code Element}.
+ *
+ * As of version 4.2, wizardry now has full support for externally-added elements.
+ *
+ * @param codeName The name of the enum constant in the code. This will be returned if you call toString() on the
+ * resulting enum constant; other than that it doesn't really make much difference.
+ * @param colour The colour of text associated with this element, as a style object.
+ * @param name The unlocalised name of this element, as used in translation keys.
+ * @param modID The mod ID of the mod that added this element, for icon rendering purposes.
+ * @return The resulting {@code Element} enum constant.
+ */
+ // This is the only one that needs the mod ID argument because elements are the only ones that have icons
+ // For some reason Minecraft doesn't seem to care about the resource domain for lang files, it just pools them
+ public static Element addElement(String codeName, Style colour, String name, String modID){
+ return EnumHelper.addEnum(Element.class, codeName, ELEMENT_ARGUMENTS, colour, name, modID);
+ }
+
+ /**
+ * Wrapper for the generic method {@link EnumHelper#addEnum(Class, String, Class[], Object...)} which is
+ * specifically for adding new spell types. Use this method in preference to the generic one in case the constructor
+ * parameters change for {@code SpellType}.
+ *
+ * As of version 4.2, wizardry now has full support for externally-added spell types.
+ *
+ * @param codeName The name of the enum constant in the code. This will be returned if you call toString() on the
+ * resulting enum constant; other than that it doesn't really make much difference.
+ * @param name The unlocalised name of this spell type, as used in translation keys.
+ * @return The resulting {@code SpellType} enum constant.
+ */
+ public static SpellType addSpellType(String codeName, String name){
+ return EnumHelper.addEnum(SpellType.class, codeName, SPELL_TYPE_ARGUMENTS, name);
+ }
+
+ /**
+ * Wrapper for the generic method {@link EnumHelper#addEnum(Class, String, Class[], Object...)} which is
+ * specifically for adding new spell contexts (for use in spell property JSON files). Use this method in preference
+ * to the generic one in case the constructor parameters change for {@code Context}.
+ *
+ * @param codeName The name of the enum constant in the code. This will be returned if you call toString() on the
+ * resulting enum constant; other than that it doesn't really make much difference.
+ * @param name The identifier for this spell context, as used in the JSON file.
+ * @return The resulting {@code Context} enum constant.
+ */
+ public static SpellProperties.Context addSpellContext(String codeName, String name){
+ return EnumHelper.addEnum(SpellProperties.Context.class, codeName, SPELL_CONTEXT_ARGUMENTS, name);
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/block/BlockArcaneWorkbench.java b/src/main/java/electroblob/wizardry/block/BlockArcaneWorkbench.java
index d2407cff..8e76c803 100644
--- a/src/main/java/electroblob/wizardry/block/BlockArcaneWorkbench.java
+++ b/src/main/java/electroblob/wizardry/block/BlockArcaneWorkbench.java
@@ -5,6 +5,7 @@ import electroblob.wizardry.WizardryGuiHandler;
import electroblob.wizardry.tileentity.TileEntityArcaneWorkbench;
import net.minecraft.block.BlockContainer;
import net.minecraft.block.material.Material;
+import net.minecraft.block.state.BlockFaceShape;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.InventoryHelper;
@@ -51,6 +52,16 @@ public class BlockArcaneWorkbench extends BlockContainer {
return false;
}
+ @Override
+ public boolean isFullCube(IBlockState state){
+ return false;
+ }
+
+ @Override
+ public BlockFaceShape getBlockFaceShape(IBlockAccess world, IBlockState state, BlockPos pos, EnumFacing face){
+ return face == EnumFacing.DOWN ? BlockFaceShape.SOLID : BlockFaceShape.UNDEFINED;
+ }
+
@Override
public boolean onBlockActivated(World world, BlockPos pos, IBlockState block, EntityPlayer player, EnumHand hand,
EnumFacing side, float hitX, float hitY, float hitZ){
diff --git a/src/main/java/electroblob/wizardry/block/BlockCrystal.java b/src/main/java/electroblob/wizardry/block/BlockCrystal.java
new file mode 100644
index 00000000..2d0f4dd9
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/block/BlockCrystal.java
@@ -0,0 +1,76 @@
+package electroblob.wizardry.block;
+
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.registry.WizardryTabs;
+import net.minecraft.block.Block;
+import net.minecraft.block.material.MapColor;
+import net.minecraft.block.material.Material;
+import net.minecraft.block.properties.PropertyEnum;
+import net.minecraft.block.state.BlockStateContainer;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.creativetab.CreativeTabs;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.NonNullList;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.IBlockAccess;
+
+import java.util.EnumMap;
+
+public class BlockCrystal extends Block {
+
+ public static final PropertyEnum ELEMENT = PropertyEnum.create("element", Element.class);
+
+ private static final EnumMap map_colours = new EnumMap<>(Element.class);
+
+ static {
+ map_colours.put(Element.MAGIC, MapColor.PINK);
+ map_colours.put(Element.FIRE, MapColor.ORANGE_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.ICE, MapColor.LIGHT_BLUE);
+ map_colours.put(Element.LIGHTNING, MapColor.CYAN);
+ map_colours.put(Element.NECROMANCY, MapColor.PURPLE);
+ map_colours.put(Element.EARTH, MapColor.GREEN);
+ map_colours.put(Element.SORCERY, MapColor.LIME);
+ map_colours.put(Element.HEALING, MapColor.YELLOW);
+ }
+
+ public BlockCrystal(Material material){
+ super(material);
+ this.setDefaultState(this.blockState.getBaseState().withProperty(ELEMENT, Element.MAGIC));
+ this.setCreativeTab(WizardryTabs.WIZARDRY);
+ this.setHarvestLevel("pickaxe", 2);
+ }
+
+ @Override
+ public int damageDropped(IBlockState state){
+ return (state.getValue(ELEMENT)).ordinal();
+ }
+
+ @Override
+ public MapColor getMapColor(IBlockState state, IBlockAccess world, BlockPos pos){
+ return map_colours.get(state.getProperties().get(ELEMENT));
+ }
+
+ @Override
+ public void getSubBlocks(CreativeTabs tab, NonNullList items){
+ if(this.getCreativeTab() == tab){
+ for(Element element : Element.values()){
+ items.add(new ItemStack(this, 1, element.ordinal()));
+ }
+ }
+ }
+
+ @Override
+ public IBlockState getStateFromMeta(int metadata){
+ return this.getDefaultState().withProperty(ELEMENT, Element.values()[metadata]);
+ }
+
+ @Override
+ public int getMetaFromState(IBlockState state){
+ return (state.getValue(ELEMENT)).ordinal();
+ }
+
+ @Override
+ protected BlockStateContainer createBlockState(){
+ return new BlockStateContainer(this, ELEMENT);
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/block/BlockCrystalFlower.java b/src/main/java/electroblob/wizardry/block/BlockCrystalFlower.java
index 1ca537ea..ed9a09d3 100644
--- a/src/main/java/electroblob/wizardry/block/BlockCrystalFlower.java
+++ b/src/main/java/electroblob/wizardry/block/BlockCrystalFlower.java
@@ -1,10 +1,8 @@
package electroblob.wizardry.block;
-import java.util.Random;
-
-import electroblob.wizardry.Wizardry;
import electroblob.wizardry.registry.WizardryBlocks;
-import electroblob.wizardry.util.WizardryParticleType;
+import electroblob.wizardry.util.ParticleBuilder;
+import electroblob.wizardry.util.ParticleBuilder.Type;
import net.minecraft.block.BlockBush;
import net.minecraft.block.SoundType;
import net.minecraft.block.material.Material;
@@ -19,6 +17,8 @@ import net.minecraftforge.event.entity.player.BonemealEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import java.util.Random;
+
// Extending BlockBush allows me to remove nearly everything from this class.
@Mod.EventBusSubscriber
public class BlockCrystalFlower extends BlockBush {
@@ -41,10 +41,10 @@ public class BlockCrystalFlower extends BlockBush {
@Override
public void randomDisplayTick(IBlockState state, World world, BlockPos pos, Random random){
if(world.isRemote && random.nextBoolean()){
- Wizardry.proxy.spawnParticle(WizardryParticleType.SPARKLE, world, pos.getX() + random.nextDouble(),
- pos.getY() + random.nextDouble() / 2 + 0.5, pos.getZ() + random.nextDouble(), 0d, 0.01, 0d,
- 20 + random.nextInt(10), 0.5f + (random.nextFloat() / 2), 0.5f + (random.nextFloat() / 2),
- 0.5f + (random.nextFloat() / 2));
+ ParticleBuilder.create(Type.SPARKLE)
+ .pos(pos.getX() + random.nextDouble(), pos.getY() + random.nextDouble() / 2 + 0.5, pos.getZ() + random.nextDouble()).vel(0, 0.01, 0)
+ .time(20 + random.nextInt(10)).clr(0.5f + (random.nextFloat() / 2), 0.5f + (random.nextFloat() / 2),
+ 0.5f + (random.nextFloat() / 2)).spawn(world);
}
}
diff --git a/src/main/java/electroblob/wizardry/block/BlockCrystalOre.java b/src/main/java/electroblob/wizardry/block/BlockCrystalOre.java
index 916b6564..a96c9d1a 100644
--- a/src/main/java/electroblob/wizardry/block/BlockCrystalOre.java
+++ b/src/main/java/electroblob/wizardry/block/BlockCrystalOre.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.block;
-import java.util.Random;
-
import electroblob.wizardry.registry.WizardryItems;
import net.minecraft.block.Block;
import net.minecraft.block.SoundType;
@@ -13,6 +11,8 @@ import net.minecraft.util.math.MathHelper;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
+import java.util.Random;
+
public class BlockCrystalOre extends Block {
public BlockCrystalOre(Material material){
diff --git a/src/main/java/electroblob/wizardry/block/BlockDryFrostedIce.java b/src/main/java/electroblob/wizardry/block/BlockDryFrostedIce.java
new file mode 100644
index 00000000..8e80b5ae
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/block/BlockDryFrostedIce.java
@@ -0,0 +1,28 @@
+package electroblob.wizardry.block;
+
+import net.minecraft.block.BlockFrostedIce;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+
+import java.util.Random;
+
+/** Like {@link BlockFrostedIce}, but melting does not depend on light level or neighbouring blocks, and it just
+ * disappears instead of turning to water. */
+public class BlockDryFrostedIce extends BlockFrostedIce {
+
+ @Override
+ protected void turnIntoWater(World world, BlockPos pos){
+ world.destroyBlock(pos, false);
+ }
+
+ @Override
+ public void updateTick(World worldIn, BlockPos pos, IBlockState state, Random rand){
+ if(rand.nextInt(3) == 0){
+ this.slightlyMelt(worldIn, pos, state, rand, true);
+ }else{
+ worldIn.scheduleUpdate(pos, this, MathHelper.getInt(rand, 20, 40));
+ }
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/block/BlockMagicLight.java b/src/main/java/electroblob/wizardry/block/BlockMagicLight.java
index 77974eec..72dd8a5e 100644
--- a/src/main/java/electroblob/wizardry/block/BlockMagicLight.java
+++ b/src/main/java/electroblob/wizardry/block/BlockMagicLight.java
@@ -1,23 +1,31 @@
package electroblob.wizardry.block;
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.item.ItemArtefact;
+import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.tileentity.TileEntityMagicLight;
-import net.minecraft.block.BlockContainer;
+import net.minecraft.block.Block;
+import net.minecraft.block.ITileEntityProvider;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
+import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumBlockRenderType;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.EnumHand;
import net.minecraft.util.math.AxisAlignedBB;
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
-public class BlockMagicLight extends BlockContainer {
+public class BlockMagicLight extends Block implements ITileEntityProvider {
- private static final AxisAlignedBB AABB = new AxisAlignedBB(0, 0, 0, 0, 0, 0);
+ //private static final AxisAlignedBB AABB = new AxisAlignedBB(0, 0, 0, 0, 0, 0);
- public BlockMagicLight(Material par2Material){
- super(par2Material);
+ public BlockMagicLight(Material material){
+ super(material);
this.setLightLevel(1.0f);
+ this.setBlockUnbreakable();
}
@Override
@@ -28,13 +36,38 @@ public class BlockMagicLight extends BlockContainer {
}
@Override
- public AxisAlignedBB getBoundingBox(IBlockState state, IBlockAccess source, BlockPos pos){
- return AABB;
+ public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing facing, float hitX, float hitY, float hitZ){
+ // Let the player dispel any lights if they have the lantern charm, not just the permanent ones because that would be annoying!
+ if(player.getHeldItem(hand).getItem() instanceof ISpellCastingItem && ItemArtefact.isArtefactActive(player, WizardryItems.charm_light)){
+
+ world.setBlockToAir(pos);
+ return true;
+
+ }else{
+ return super.onBlockActivated(world, pos, state, player, hand, facing, hitX, hitY, hitZ);
+ }
}
+// @Override
+// public AxisAlignedBB getBoundingBox(IBlockState state, IBlockAccess source, BlockPos pos){
+// return AABB;
+// }
+
@Override
public boolean isCollidable(){
- return false;
+ // This method has nothing to do with entity movement, it's just for raytracing
+ return true;
+ }
+
+ @Override
+ public boolean addDestroyEffects(World world, BlockPos pos, net.minecraft.client.particle.ParticleManager manager){
+ if(world.getBlockState(pos).getBlock() == this) return true; // No break particles!
+ else return super.addDestroyEffects(world, pos, manager);
+ }
+
+ @Override
+ public boolean hasTileEntity(IBlockState state){
+ return true;
}
@Override
diff --git a/src/main/java/electroblob/wizardry/block/BlockObsidianCrust.java b/src/main/java/electroblob/wizardry/block/BlockObsidianCrust.java
new file mode 100644
index 00000000..269459bc
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/block/BlockObsidianCrust.java
@@ -0,0 +1,116 @@
+package electroblob.wizardry.block;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockObsidian;
+import net.minecraft.block.properties.PropertyInteger;
+import net.minecraft.block.state.BlockStateContainer;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.init.Blocks;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+
+import java.util.Random;
+
+/** Like {@link net.minecraft.block.BlockFrostedIce}, but for lava instead of water. */
+// This is mostly copied from that class, with a few changes
+public class BlockObsidianCrust extends BlockObsidian {
+
+ public static final PropertyInteger AGE = PropertyInteger.create("age", 0, 3);
+
+ public BlockObsidianCrust(){
+ this.setDefaultState(this.blockState.getBaseState().withProperty(AGE, 0));
+ }
+
+ @Override
+ public int getMetaFromState(IBlockState state){
+ return state.getValue(AGE);
+ }
+
+ @Override
+ public IBlockState getStateFromMeta(int meta){
+ return this.getDefaultState().withProperty(AGE, MathHelper.clamp(meta, 0, 3));
+ }
+
+ @Override
+ public void updateTick(World world, BlockPos pos, IBlockState state, Random random){
+ if((random.nextInt(3) == 0 || this.countNeighbors(world, pos) < 4) && world.getLightFromNeighbors(pos) > 11 - state.getValue(AGE) - state.getLightOpacity()){
+ this.slightlyMelt(world, pos, state, random, true);
+ }else{
+ world.scheduleUpdate(pos, this, MathHelper.getInt(random, 20, 40));
+ }
+ }
+
+ @Override
+ public void neighborChanged(IBlockState state, World world, BlockPos pos, Block block, BlockPos fromPos){
+ if(block == this){
+ int i = this.countNeighbors(world, pos);
+
+ if(i < 2){
+ this.melt(world, pos);
+ }
+ }
+ }
+
+ private int countNeighbors(World world, BlockPos pos){
+
+ int i = 0;
+
+ for(EnumFacing enumfacing : EnumFacing.values()){
+ if(world.getBlockState(pos.offset(enumfacing)).getBlock() == this){
+ ++i;
+
+ if(i >= 4){
+ return i;
+ }
+ }
+ }
+
+ return i;
+ }
+
+ protected void slightlyMelt(World world, BlockPos pos, IBlockState state, Random random, boolean meltNeighbours){
+
+ int i = state.getValue(AGE);
+
+ if(i < 3){
+
+ world.setBlockState(pos, state.withProperty(AGE, i + 1), 2);
+ world.scheduleUpdate(pos, this, MathHelper.getInt(random, 20, 40));
+
+ }else{
+
+ this.melt(world, pos);
+
+ if(meltNeighbours){
+
+ for(EnumFacing enumfacing : EnumFacing.values()){
+
+ BlockPos blockpos = pos.offset(enumfacing);
+ IBlockState iblockstate = world.getBlockState(blockpos);
+
+ if(iblockstate.getBlock() == this){
+ this.slightlyMelt(world, blockpos, iblockstate, random, false);
+ }
+ }
+ }
+ }
+ }
+
+ protected void melt(World world, BlockPos pos){
+ world.setBlockState(pos, Blocks.LAVA.getDefaultState());
+ world.neighborChanged(pos, Blocks.LAVA, pos);
+ }
+
+ @Override
+ protected BlockStateContainer createBlockState(){
+ return new BlockStateContainer(this, AGE);
+ }
+
+ @Override
+ public ItemStack getItem(World world, BlockPos pos, IBlockState state){
+ return ItemStack.EMPTY;
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/block/BlockPedestal.java b/src/main/java/electroblob/wizardry/block/BlockPedestal.java
new file mode 100644
index 00000000..9da9c6f5
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/block/BlockPedestal.java
@@ -0,0 +1,123 @@
+package electroblob.wizardry.block;
+
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.registry.WizardryTabs;
+import electroblob.wizardry.tileentity.TileEntityShrineCore;
+import net.minecraft.block.Block;
+import net.minecraft.block.ITileEntityProvider;
+import net.minecraft.block.material.MapColor;
+import net.minecraft.block.material.Material;
+import net.minecraft.block.properties.PropertyBool;
+import net.minecraft.block.properties.PropertyEnum;
+import net.minecraft.block.state.BlockStateContainer;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.creativetab.CreativeTabs;
+import net.minecraft.entity.Entity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.BlockRenderLayer;
+import net.minecraft.util.NonNullList;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.Explosion;
+import net.minecraft.world.IBlockAccess;
+import net.minecraft.world.World;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+import java.util.EnumMap;
+
+public class BlockPedestal extends Block implements ITileEntityProvider {
+
+ public static final PropertyEnum ELEMENT = PropertyEnum.create("element", Element.class,
+ Arrays.copyOfRange(Element.values(), 1, Element.values().length)); // Everything except MAGIC
+
+ // A 'natural' pedestal is one that was generated as part of a structure, is unbreakable and has a tileentity
+ public static final PropertyBool NATURAL = PropertyBool.create("natural");
+
+ private static final EnumMap map_colours = new EnumMap<>(Element.class);
+
+ static {
+ map_colours.put(Element.FIRE, MapColor.RED_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.ICE, MapColor.LIGHT_BLUE_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.LIGHTNING, MapColor.CYAN_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.NECROMANCY, MapColor.PURPLE_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.EARTH, MapColor.BROWN_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.SORCERY, MapColor.GRAY);
+ map_colours.put(Element.HEALING, MapColor.YELLOW_STAINED_HARDENED_CLAY);
+ }
+
+ public BlockPedestal(Material material){
+ super(material);
+ this.setDefaultState(this.blockState.getBaseState().withProperty(ELEMENT, Element.FIRE).withProperty(NATURAL, false));
+ this.setCreativeTab(WizardryTabs.WIZARDRY);
+ this.setHardness(1.5F);
+ this.setResistance(10.0F);
+ }
+
+ @Override
+ public int damageDropped(IBlockState state){
+ return state.getValue(ELEMENT).ordinal(); // Ignore the NATURAL state here, it's unobtainable
+ }
+
+ @Override
+ public MapColor getMapColor(IBlockState state, IBlockAccess world, BlockPos pos){
+ return map_colours.get(state.getProperties().get(ELEMENT));
+ }
+
+ @Override
+ public void getSubBlocks(CreativeTabs tab, NonNullList items){
+ // Ignore the NATURAL state here, it's unobtainable
+ if(this.getCreativeTab() == tab){
+ for(Element element : Arrays.copyOfRange(Element.values(), 1, Element.values().length)){
+ items.add(new ItemStack(this, 1, element.ordinal()));
+ }
+ }
+ }
+
+ @Override
+ public BlockRenderLayer getRenderLayer(){
+ return BlockRenderLayer.CUTOUT; // Required to shade parts of the block faces differently to others
+ }
+
+ @Override
+ public float getBlockHardness(IBlockState state, World world, BlockPos pos){
+ return state.getValue(NATURAL) ? -1 : super.getBlockHardness(state, world, pos);
+ }
+
+ @Override
+ public float getExplosionResistance(World world, BlockPos pos, @Nullable Entity exploder, Explosion explosion){
+ return world.getBlockState(pos).getValue(NATURAL) ? 6000000.0F : super.getExplosionResistance(world, pos, exploder, explosion);
+ }
+
+ @Override
+ public boolean hasTileEntity(IBlockState state){
+ return state.getValue(NATURAL); // Only naturally-generated pedestals have a (shrine core) tile entity
+ }
+
+ @Nullable
+ @Override
+ public TileEntity createNewTileEntity(World world, int meta){
+ return new TileEntityShrineCore();
+ }
+
+ @Override
+ public IBlockState getStateFromMeta(int metadata){
+ boolean natural = false;
+ if(metadata > ELEMENT.getAllowedValues().size()){
+ natural = true;
+ metadata -= ELEMENT.getAllowedValues().size();
+ }
+ return this.getDefaultState().withProperty(ELEMENT, Element.values()[metadata]).withProperty(NATURAL, natural);
+ }
+
+ @Override
+ public int getMetaFromState(IBlockState state){
+ return state.getValue(ELEMENT).ordinal() + (state.getValue(NATURAL) ? ELEMENT.getAllowedValues().size() : 0);
+ }
+
+ @Override
+ protected BlockStateContainer createBlockState(){
+ return new BlockStateContainer(this, ELEMENT, NATURAL);
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/block/BlockRunestone.java b/src/main/java/electroblob/wizardry/block/BlockRunestone.java
new file mode 100644
index 00000000..996eaa52
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/block/BlockRunestone.java
@@ -0,0 +1,85 @@
+package electroblob.wizardry.block;
+
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.registry.WizardryTabs;
+import net.minecraft.block.Block;
+import net.minecraft.block.material.MapColor;
+import net.minecraft.block.material.Material;
+import net.minecraft.block.properties.PropertyEnum;
+import net.minecraft.block.state.BlockStateContainer;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.creativetab.CreativeTabs;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.BlockRenderLayer;
+import net.minecraft.util.NonNullList;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.IBlockAccess;
+
+import java.util.Arrays;
+import java.util.EnumMap;
+
+public class BlockRunestone extends Block {
+
+ public static final PropertyEnum ELEMENT = PropertyEnum.create("element", Element.class,
+ Arrays.copyOfRange(Element.values(), 1, Element.values().length)); // Everything except MAGIC
+
+ private static final EnumMap map_colours = new EnumMap<>(Element.class);
+
+ static {
+ map_colours.put(Element.FIRE, MapColor.RED_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.ICE, MapColor.LIGHT_BLUE_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.LIGHTNING, MapColor.CYAN_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.NECROMANCY, MapColor.PURPLE_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.EARTH, MapColor.BROWN_STAINED_HARDENED_CLAY);
+ map_colours.put(Element.SORCERY, MapColor.GRAY);
+ map_colours.put(Element.HEALING, MapColor.YELLOW_STAINED_HARDENED_CLAY);
+ }
+
+ public BlockRunestone(Material material){
+ super(material);
+ this.setDefaultState(this.blockState.getBaseState().withProperty(ELEMENT, Element.FIRE));
+ this.setCreativeTab(WizardryTabs.WIZARDRY);
+ this.setHardness(1.5F);
+ this.setResistance(10.0F);
+ }
+
+ @Override
+ public int damageDropped(IBlockState state){
+ return state.getValue(ELEMENT).ordinal();
+ }
+
+ @Override
+ public MapColor getMapColor(IBlockState state, IBlockAccess world, BlockPos pos){
+ return map_colours.get(state.getProperties().get(ELEMENT));
+ }
+
+ @Override
+ public void getSubBlocks(CreativeTabs tab, NonNullList items){
+ if(this.getCreativeTab() == tab){
+ for(Element element : Arrays.copyOfRange(Element.values(), 1, Element.values().length)){
+ items.add(new ItemStack(this, 1, element.ordinal()));
+ }
+ }
+ }
+
+ @Override
+ public BlockRenderLayer getRenderLayer(){
+ return BlockRenderLayer.CUTOUT; // Required to shade parts of the block faces differently to others
+ }
+
+ @Override
+ public IBlockState getStateFromMeta(int metadata){
+ return this.getDefaultState().withProperty(ELEMENT, Element.values()[metadata]);
+ }
+
+ @Override
+ public int getMetaFromState(IBlockState state){
+ return state.getValue(ELEMENT).ordinal();
+ }
+
+ @Override
+ protected BlockStateContainer createBlockState(){
+ return new BlockStateContainer(this, ELEMENT);
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/block/BlockSnare.java b/src/main/java/electroblob/wizardry/block/BlockSnare.java
index 878826df..a10df677 100644
--- a/src/main/java/electroblob/wizardry/block/BlockSnare.java
+++ b/src/main/java/electroblob/wizardry/block/BlockSnare.java
@@ -1,13 +1,13 @@
package electroblob.wizardry.block;
-import java.util.Random;
-
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.tileentity.TileEntityPlayerSave;
+import electroblob.wizardry.util.AllyDesignationSystem;
import electroblob.wizardry.util.MagicDamage;
import electroblob.wizardry.util.MagicDamage.DamageType;
-import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.block.Block;
-import net.minecraft.block.BlockContainer;
+import net.minecraft.block.ITileEntityProvider;
import net.minecraft.block.SoundType;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
@@ -25,8 +25,9 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
-// TODO: Apparently you shouldn't extend BlockContainer. I feel like BlockArcaneWorkbench should, but what about the rest?
-public class BlockSnare extends BlockContainer {
+import java.util.Random;
+
+public class BlockSnare extends Block implements ITileEntityProvider {
private static final AxisAlignedBB AABB = new AxisAlignedBB(0.0f, 0.0f, 0.0f, 1.0f, 0.0625f, 1.0f);
@@ -58,10 +59,14 @@ public class BlockSnare extends BlockContainer {
TileEntityPlayerSave tileentity = (TileEntityPlayerSave)world.getTileEntity(pos);
- if(WizardryUtilities.isValidTarget(tileentity.getCaster(), entity)){
- ((EntityLivingBase)entity).attackEntityFrom(
- MagicDamage.causeDirectMagicDamage(tileentity.getCaster(), DamageType.MAGIC), 6);
- ((EntityLivingBase)entity).addPotionEffect(new PotionEffect(MobEffects.SLOWNESS, 100, 2));
+ if(AllyDesignationSystem.isValidTarget(tileentity.getCaster(), entity)){
+
+ entity.attackEntityFrom(MagicDamage.causeDirectMagicDamage(tileentity.getCaster(), DamageType.MAGIC),
+ Spells.snare.getProperty(Spell.DAMAGE).floatValue());
+
+ ((EntityLivingBase)entity).addPotionEffect(new PotionEffect(MobEffects.SLOWNESS,
+ Spells.snare.getProperty(Spell.EFFECT_DURATION).intValue(),
+ Spells.snare.getProperty(Spell.EFFECT_STRENGTH).intValue()));
world.destroyBlock(pos, false);
}
diff --git a/src/main/java/electroblob/wizardry/block/BlockSpectral.java b/src/main/java/electroblob/wizardry/block/BlockSpectral.java
index 300582dc..fe34473a 100644
--- a/src/main/java/electroblob/wizardry/block/BlockSpectral.java
+++ b/src/main/java/electroblob/wizardry/block/BlockSpectral.java
@@ -1,13 +1,11 @@
package electroblob.wizardry.block;
-import java.util.Random;
-
-import electroblob.wizardry.Wizardry;
import electroblob.wizardry.registry.WizardryBlocks;
import electroblob.wizardry.tileentity.TileEntityTimer;
-import electroblob.wizardry.util.WizardryParticleType;
+import electroblob.wizardry.util.ParticleBuilder;
+import electroblob.wizardry.util.ParticleBuilder.Type;
import net.minecraft.block.Block;
-import net.minecraft.block.BlockContainer;
+import net.minecraft.block.ITileEntityProvider;
import net.minecraft.block.SoundType;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
@@ -24,10 +22,10 @@ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
-// For future reference - extend BlockContainer whenever possible because it has methods for removing tile entities on
-// block break.
+import java.util.Random;
+
@Mod.EventBusSubscriber
-public class BlockSpectral extends BlockContainer {
+public class BlockSpectral extends Block implements ITileEntityProvider {
public BlockSpectral(Material material){
super(material);
@@ -45,10 +43,8 @@ public class BlockSpectral extends BlockContainer {
return EnumBlockRenderType.MODEL;
}
- // Apparently it's OK to override this, despite it being deprecated. More
- // importantly, it being deprecated is not
- // Forge's doing, rather it is Mojang themselves misusing the @Deprecated
- // annotation to mean 'internal, don't call'.
+ // Apparently it's OK to override this, despite it being deprecated. More importantly, it being deprecated is not
+ // Forge's doing, rather it is Mojang themselves misusing the @Deprecated annotation to mean 'internal, don't call'.
@Override
public boolean isOpaqueCube(IBlockState state){
return false;
@@ -61,18 +57,14 @@ public class BlockSpectral extends BlockContainer {
@Override
public void randomDisplayTick(IBlockState state, World world, BlockPos pos, Random random){
- // Middle of block
- Wizardry.proxy.spawnParticle(WizardryParticleType.DUST, world, pos.getX() + random.nextDouble(),
- pos.getY() + random.nextDouble(), pos.getZ() + random.nextDouble(), 0, 0, 0,
- (int)(16.0D / (Math.random() * 0.8D + 0.2D)), 0.4f + random.nextFloat() * 0.2f,
- 0.6f + random.nextFloat() * 0.4f, 0.6f + random.nextFloat() * 0.4f);
- // Top surface
- Wizardry.proxy.spawnParticle(WizardryParticleType.DUST, world, pos.getX() + random.nextDouble(), pos.getY() + 1,
- pos.getZ() + random.nextDouble(), 0, 0, 0, (int)(16.0D / (Math.random() * 0.8D + 0.2D)),
- 0.4f + random.nextFloat() * 0.2f, 0.6f + random.nextFloat() * 0.4f, 0.6f + random.nextFloat() * 0.4f);
- Wizardry.proxy.spawnParticle(WizardryParticleType.DUST, world, pos.getX() + random.nextDouble(), pos.getY() + 1,
- pos.getZ() + random.nextDouble(), 0, 0, 0, (int)(16.0D / (Math.random() * 0.8D + 0.2D)),
- 0.4f + random.nextFloat() * 0.2f, 0.6f + random.nextFloat() * 0.4f, 0.6f + random.nextFloat() * 0.4f);
+
+ for(int i=0; i<2; i++){
+ ParticleBuilder.create(Type.DUST)
+ .pos(pos.getX() + random.nextDouble(), pos.getY() + random.nextDouble(), pos.getZ() + random.nextDouble())
+ .time((int)(16.0D / (Math.random() * 0.8D + 0.2D)))
+ .clr(0.4f + random.nextFloat() * 0.2f, 0.6f + random.nextFloat() * 0.4f, 0.6f + random.nextFloat() * 0.4f)
+ .shaded(true).spawn(world);
+ }
}
// Overriden to make the block always look full brightness despite not emitting
@@ -82,6 +74,11 @@ public class BlockSpectral extends BlockContainer {
return 15;
}
+ @Override
+ public boolean hasTileEntity(IBlockState state){
+ return true;
+ }
+
@Override
public TileEntity createNewTileEntity(World world, int metadata){
return new TileEntityTimer(1200);
diff --git a/src/main/java/electroblob/wizardry/block/BlockStatue.java b/src/main/java/electroblob/wizardry/block/BlockStatue.java
index 4dce4a22..cf5b7dee 100644
--- a/src/main/java/electroblob/wizardry/block/BlockStatue.java
+++ b/src/main/java/electroblob/wizardry/block/BlockStatue.java
@@ -1,14 +1,13 @@
package electroblob.wizardry.block;
-import java.util.Random;
-
-import electroblob.wizardry.spell.Petrify;
import electroblob.wizardry.tileentity.TileEntityStatue;
+import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.block.Block;
-import net.minecraft.block.BlockContainer;
+import net.minecraft.block.ITileEntityProvider;
import net.minecraft.block.SoundType;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
+import net.minecraft.entity.EntityLiving;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.BlockRenderLayer;
import net.minecraft.util.EnumBlockRenderType;
@@ -20,10 +19,17 @@ import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
-public class BlockStatue extends BlockContainer {
+import java.util.Random;
+
+public class BlockStatue extends Block implements ITileEntityProvider {
private boolean isIce;
+ /** The NBT tag name for storing the petrified flag (used for rendering) in the target's tag compound. */
+ public static final String PETRIFIED_NBT_KEY = "petrified";
+ /** The NBT tag name for storing the frozen flag (used for rendering) in the target's tag compound. */
+ public static final String FROZEN_NBT_KEY = "frozen";
+
public BlockStatue(Material material){
super(material);
this.isIce = material == Material.ICE;
@@ -106,6 +112,11 @@ public class BlockStatue extends BlockContainer {
return this.isIce ? EnumBlockRenderType.MODEL : EnumBlockRenderType.ENTITYBLOCK_ANIMATED;
}
+ @Override
+ public boolean hasTileEntity(IBlockState state){
+ return true;
+ }
+
@Override
public TileEntity createNewTileEntity(World world, int metadata){
return new TileEntityStatue(this.isIce);
@@ -146,7 +157,7 @@ public class BlockStatue extends BlockContainer {
// This is only when position == 1 because world.destroyBlock calls this function for the other blocks.
if(tileentity != null && tileentity.position == 1 && tileentity.creature != null){
- tileentity.creature.getEntityData().removeTag(Petrify.NBT_KEY);
+ tileentity.creature.getEntityData().removeTag(BlockStatue.PETRIFIED_NBT_KEY);
tileentity.creature.isDead = false;
world.spawnEntity(tileentity.creature);
}
@@ -166,4 +177,84 @@ public class BlockStatue extends BlockContainer {
return this.isIce && block == this ? false : super.shouldSideBeRendered(blockState, blockAccess, pos, side);
}
+
+ /**
+ * Turns the given entity into a statue. The type of statue depends on the block instance this method was invoked on.
+ * @param entity The entity to turn into a statue.
+ * @param duration The time for which the entity should remain a statue. For petrified creatures, this is the minimum
+ * time it can stay as a statue.
+ * @return True if the entity was successfully turned into a statue, false if not (i.e. something was in the way).
+ */
+ // Making this an instance method means it works equally well for both types of statue
+ public boolean convertToStatue(EntityLiving entity, int duration){
+
+ if(entity.deathTime > 0) return false;
+
+ BlockPos pos = new BlockPos(entity);
+ World world = entity.world;
+
+ entity.hurtTime = 0; // Stops the entity looking red while frozen and the resulting z-fighting
+ entity.extinguish();
+
+ // Short mobs such as spiders and pigs
+ if((entity.height < 1.2 || entity.isChild()) && WizardryUtilities.canBlockBeReplaced(world, pos)){
+
+ world.setBlockState(pos, this.getDefaultState());
+ if(world.getTileEntity(pos) instanceof TileEntityStatue){
+ ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(entity, 1, 1);
+ ((TileEntityStatue)world.getTileEntity(pos)).setLifetime(duration);
+ }
+
+ entity.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true);
+ entity.setDead();
+ return true;
+ }
+ // Normal sized mobs like zombies and skeletons
+ else if(entity.height < 2.5 && WizardryUtilities.canBlockBeReplaced(world, pos)
+ && WizardryUtilities.canBlockBeReplaced(world, pos.up())){
+
+ world.setBlockState(pos, this.getDefaultState());
+ if(world.getTileEntity(pos) instanceof TileEntityStatue){
+ ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(entity, 1, 2);
+ ((TileEntityStatue)world.getTileEntity(pos)).setLifetime(duration);
+ }
+
+ world.setBlockState(pos.up(), this.getDefaultState());
+ if(world.getTileEntity(pos.up()) instanceof TileEntityStatue){
+ ((TileEntityStatue)world.getTileEntity(pos.up())).setCreatureAndPart(entity, 2, 2);
+ }
+
+ entity.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true);
+ entity.setDead();
+ return true;
+ }
+ // Tall mobs like endermen
+ else if(WizardryUtilities.canBlockBeReplaced(world, pos)
+ && WizardryUtilities.canBlockBeReplaced(world, pos.up())
+ && WizardryUtilities.canBlockBeReplaced(world, pos.up(2))){
+
+ world.setBlockState(pos, this.getDefaultState());
+ if(world.getTileEntity(pos) instanceof TileEntityStatue){
+ ((TileEntityStatue)world.getTileEntity(pos)).setCreatureAndPart(entity, 1, 3);
+ ((TileEntityStatue)world.getTileEntity(pos)).setLifetime(duration);
+ }
+
+ world.setBlockState(pos.up(), this.getDefaultState());
+ if(world.getTileEntity(pos.up()) instanceof TileEntityStatue){
+ ((TileEntityStatue)world.getTileEntity(pos.up())).setCreatureAndPart(entity, 2, 3);
+ }
+
+ world.setBlockState(pos.up(2), this.getDefaultState());
+ if(world.getTileEntity(pos.up(2)) instanceof TileEntityStatue){
+ ((TileEntityStatue)world.getTileEntity(pos.up(2))).setCreatureAndPart(entity, 3, 3);
+ }
+
+ entity.getEntityData().setBoolean(this.isIce ? FROZEN_NBT_KEY : PETRIFIED_NBT_KEY, true);
+ entity.setDead();
+ return true;
+ }
+
+ return false;
+ }
+
}
diff --git a/src/main/java/electroblob/wizardry/block/BlockThorns.java b/src/main/java/electroblob/wizardry/block/BlockThorns.java
new file mode 100644
index 00000000..0ceb6035
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/block/BlockThorns.java
@@ -0,0 +1,181 @@
+package electroblob.wizardry.block;
+
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.registry.WizardryBlocks;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.tileentity.TileEntityPlayerSaveTimed;
+import electroblob.wizardry.util.AllyDesignationSystem;
+import electroblob.wizardry.util.MagicDamage;
+import net.minecraft.block.*;
+import net.minecraft.block.BlockDoublePlant.EnumBlockHalf;
+import net.minecraft.block.properties.PropertyEnum;
+import net.minecraft.block.properties.PropertyInteger;
+import net.minecraft.block.state.BlockStateContainer;
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.init.Items;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.DamageSource;
+import net.minecraft.util.math.AxisAlignedBB;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.IBlockAccess;
+import net.minecraft.world.World;
+import net.minecraftforge.event.entity.player.PlayerInteractEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+
+import java.util.Random;
+
+@Mod.EventBusSubscriber
+public class BlockThorns extends BlockBush implements ITileEntityProvider {
+
+ public static final int GROWTH_STAGES = 8;
+
+ public static final PropertyInteger AGE = PropertyInteger.create("age", 0, GROWTH_STAGES-1);
+ public static final PropertyEnum HALF = PropertyEnum.create("half", EnumBlockHalf.class);
+
+ public BlockThorns(){
+ this.setDefaultState(this.blockState.getBaseState().withProperty(HALF, EnumBlockHalf.LOWER).withProperty(AGE, 7));
+ this.setHardness(4);
+ this.setSoundType(SoundType.PLANT);
+ this.setCreativeTab(null);
+ }
+
+ @Override
+ public AxisAlignedBB getBoundingBox(IBlockState state, IBlockAccess source, BlockPos pos){
+ return FULL_BLOCK_AABB;
+ }
+
+ @Override
+ public IBlockState getStateFromMeta(int meta){
+ return this.getDefaultState().withProperty(HALF, EnumBlockHalf.values()[meta / GROWTH_STAGES]).withProperty(AGE, meta % GROWTH_STAGES);
+ }
+
+ @Override
+ public int getMetaFromState(IBlockState state){
+ return state.getValue(HALF).ordinal() * GROWTH_STAGES + state.getValue(AGE);
+ }
+
+// @Override
+// public void updateTick(World world, BlockPos pos, IBlockState state, Random rand){
+//
+// super.updateTick(world, pos, state, rand);
+//
+// // Update the state, including on the client, but don't do a block update since it's only visual
+// if(state.getValue(AGE) < GROWTH_STAGES-1) world.setBlockState(pos, state.withProperty(AGE, state.getValue(AGE) + 1), 2);
+// }
+
+ @Override
+ protected BlockStateContainer createBlockState(){
+ return new BlockStateContainer(this, HALF, AGE);
+ }
+
+ public void placeAt(World world, BlockPos lowerPos, int flags){
+ world.setBlockState(lowerPos, this.getDefaultState().withProperty(HALF, EnumBlockHalf.LOWER).withProperty(AGE, 0), flags);
+ world.setBlockState(lowerPos.up(), this.getDefaultState().withProperty(HALF, EnumBlockHalf.UPPER).withProperty(AGE, 0), flags);
+ }
+
+ @Override
+ public void onBlockPlacedBy(World world, BlockPos pos, IBlockState state, EntityLivingBase placer, ItemStack stack){
+ world.setBlockState(pos.up(), this.getDefaultState().withProperty(HALF, EnumBlockHalf.UPPER), 2);
+ }
+
+ @Override
+ public void breakBlock(World world, BlockPos pos, IBlockState state){
+ super.breakBlock(world, pos, state);
+ if(state.getValue(HALF) == EnumBlockHalf.LOWER){
+ if(world.getBlockState(pos.up()).getBlock() == this){
+ world.destroyBlock(pos.up(), false);
+ }
+ }else{
+ if(world.getBlockState(pos.down()).getBlock() == this){
+ world.destroyBlock(pos.down(), false);
+ }
+ }
+ }
+
+ public boolean canBlockStay(World worldIn, BlockPos pos, IBlockState state){
+ if(state.getValue(HALF) == BlockDoublePlant.EnumBlockHalf.UPPER){
+ return worldIn.getBlockState(pos.down()).getBlock() == this;
+ }else{
+ IBlockState iblockstate = worldIn.getBlockState(pos.up());
+ return iblockstate.getBlock() == this && this.canSustainBush(worldIn.getBlockState(pos.down()));
+ }
+ }
+
+// @Override
+// public IBlockState getActualState(IBlockState state, IBlockAccess world, BlockPos pos){
+// // Copied from BlockFlowerPot on authority of the Forge docs, which says this check is necessary
+// SoundLoopSpellDispenser tileentity = world instanceof ChunkCache ? ((ChunkCache)world).getTileEntity(pos, Chunk.EnumCreateEntityType.CHECK) : world.getTileEntity(pos);
+//
+// if(tileentity instanceof TileEntityPlayerSaveTimed){
+// state = state.withProperty(AGE, Math.min(7, ((TileEntityPlayerSaveTimed)tileentity).timer/2));
+// }else{
+// state = state.withProperty(AGE, 7);
+// }
+//
+// return state;
+// }
+
+ @Override
+ public void onEntityCollision(World world, BlockPos pos, IBlockState state, Entity entity){
+ if(!world.isRemote){
+ if(applyThornDamage(world, pos, entity)){
+ entity.setInWeb();
+ }
+ }
+ }
+
+ private static boolean applyThornDamage(World world, BlockPos pos, Entity target){
+
+ DamageSource source = DamageSource.CACTUS;
+
+ TileEntity tileentity = world.getTileEntity(pos);
+
+ if(tileentity instanceof TileEntityPlayerSaveTimed){
+ if(AllyDesignationSystem.isValidTarget(((TileEntityPlayerSaveTimed)tileentity).getCaster(), target)){
+ source = MagicDamage.causeDirectMagicDamage(((TileEntityPlayerSaveTimed)tileentity).getCaster(),
+ MagicDamage.DamageType.MAGIC);
+ }else{
+ return false; // Don't attack or slow allies of the caster
+ }
+ }
+
+ if(world.getTotalWorldTime() % 20 == 0) target.attackEntityFrom(source, Spells.forest_of_thorns.getProperty(Spell.DAMAGE).floatValue());
+
+ return true;
+ }
+
+ @Override
+ public Block.EnumOffsetType getOffsetType(){
+ return Block.EnumOffsetType.XZ;
+ }
+
+ @Override
+ public TileEntity createNewTileEntity(World world, int metadata){
+ return new TileEntityPlayerSaveTimed(600);
+ }
+
+ @Override
+ public boolean hasTileEntity(IBlockState state){
+ return true;
+ }
+
+ @Override public boolean isReplaceable(IBlockAccess world, BlockPos pos){ return false; }
+ @Override protected boolean canSustainBush(IBlockState state){ return state.isNormalCube(); }
+ @Override public Item getItemDropped(IBlockState state, Random rand, int fortune){ return Items.AIR; }
+ @Override public boolean canSilkHarvest(World world, BlockPos pos, IBlockState state, EntityPlayer player){ return false; }
+
+ @SubscribeEvent
+ public static void onLeftClickBlockEvent(PlayerInteractEvent.LeftClickBlock event){
+ if(!event.getWorld().isRemote && event.getWorld().getTotalWorldTime() % 20 == 0
+ && event.getWorld().getBlockState(event.getPos()).getBlock() == WizardryBlocks.thorns){
+ applyThornDamage(event.getWorld(), event.getPos(), event.getEntity());
+ }
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/block/BlockTransportationStone.java b/src/main/java/electroblob/wizardry/block/BlockTransportationStone.java
index 9d1684a4..ea0595a1 100644
--- a/src/main/java/electroblob/wizardry/block/BlockTransportationStone.java
+++ b/src/main/java/electroblob/wizardry/block/BlockTransportationStone.java
@@ -1,12 +1,16 @@
package electroblob.wizardry.block;
-import java.util.Random;
-
-import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.item.ItemWand;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.item.ItemArtefact;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardryBlocks;
+import electroblob.wizardry.registry.WizardryItems;
+import electroblob.wizardry.spell.Transportation;
+import electroblob.wizardry.util.Location;
+import electroblob.wizardry.util.ParticleBuilder;
+import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
@@ -20,6 +24,10 @@ import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
public class BlockTransportationStone extends Block {
private static final AxisAlignedBB AABB = new AxisAlignedBB(0.0625f * 5, 0, 0.0625f * 5, 0.0625f * 11, 0.0625f * 6,
@@ -65,6 +73,11 @@ public class BlockTransportationStone extends Block {
public boolean isOpaqueCube(IBlockState state){
return false;
}
+
+ @Override
+ public boolean isSideSolid(IBlockState base_state, IBlockAccess world, BlockPos pos, EnumFacing side){
+ return side == EnumFacing.DOWN;
+ }
@SuppressWarnings("deprecation")
@Override
@@ -98,7 +111,7 @@ public class BlockTransportationStone extends Block {
ItemStack stack = player.getHeldItem(hand);
- if(stack.getItem() instanceof ItemWand){
+ if(stack.getItem() instanceof ISpellCastingItem){
if(WizardData.get(player) != null){
WizardData data = WizardData.get(player);
@@ -107,17 +120,59 @@ public class BlockTransportationStone extends Block {
for(int z = -1; z <= 1; z++){
BlockPos pos1 = pos.add(x, 0, z);
if(testForCircle(world, pos1)){
- data.setStoneCircleLocation(pos1, world.provider.getDimension());
- if(!world.isRemote) player.sendMessage(
- new TextComponentTranslation("tile." + Wizardry.MODID + ":transportation_stone.confirm",
- Spells.transportation.getNameForTranslationFormatted()));
+
+ Location here = new Location(pos1, player.dimension);
+
+ List locations = data.getVariable(Transportation.LOCATIONS_KEY);
+ if(locations == null) data.setVariable(Transportation.LOCATIONS_KEY, locations = new ArrayList<>(Transportation.MAX_REMEMBERED_LOCATIONS));
+
+ if(ItemArtefact.isArtefactActive(player, WizardryItems.charm_transportation)){
+
+ if(locations.contains(here)){
+ locations.remove(here);
+ if(!world.isRemote) player.sendStatusMessage(new TextComponentTranslation("tile." + Wizardry.MODID + ":transportation_stone.forget", here.pos.getX(), here.pos.getY(), here.pos.getZ(), here.dimension), true);
+
+ }else{
+
+ locations.add(here);
+ if(!world.isRemote) player.sendStatusMessage(new TextComponentTranslation("tile." + Wizardry.MODID + ":transportation_stone.remember", here.pos.getX(), here.pos.getY(), here.pos.getZ(), here.dimension), true);
+
+ if(locations.size() > Transportation.MAX_REMEMBERED_LOCATIONS){
+ Location removed = locations.remove(0);
+ if(!world.isRemote) player.sendStatusMessage(new TextComponentTranslation("tile." + Wizardry.MODID + ":transportation_stone.forget", removed.pos.getX(), removed.pos.getY(), removed.pos.getZ(), removed.dimension), true);
+ }
+ }
+
+ }else{
+ if(locations.isEmpty()) locations.add(here);
+ else{
+ locations.remove(here); // Prevents duplicates
+ locations.set(locations.size() - 1, here);
+ }
+ if(!world.isRemote) player.sendStatusMessage(new TextComponentTranslation("tile." + Wizardry.MODID + ":transportation_stone.confirm", Spells.transportation.getNameForTranslationFormatted()), true);
+ }
+
return true;
}
}
}
- if(!world.isRemote)
- player.sendMessage(new TextComponentTranslation("tile." + Wizardry.MODID + ":transportation_stone.invalid"));
+ if(!world.isRemote){
+ player.sendStatusMessage(new TextComponentTranslation("tile." + Wizardry.MODID + ":transportation_stone.invalid"), true);
+ }else{
+
+ BlockPos centre = findMostLikelyCircle(world, pos);
+ // Displays particles in the required shape
+ for(int x = -1; x <= 1; x++){
+ for(int z = -1; z <= 1; z++){
+ if(x == 0 && z == 0) continue;
+ ParticleBuilder.create(ParticleBuilder.Type.PATH)
+ .pos(WizardryUtilities.getCentre(centre).add(x, -0.3125, z)).clr(0x86ff65)
+ .time(200).scale(2).spawn(world);
+ }
+ }
+ }
+
return true;
}
}
@@ -131,12 +186,47 @@ public class BlockTransportationStone extends Block {
for(int x = -1; x <= 1; x++){
for(int z = -1; z <= 1; z++){
+ if(x == 0 && z == 0) continue;
if(world.getBlockState(pos.add(x, 0, z)).getBlock() != WizardryBlocks.transportation_stone){
- if(x != 0 || z != 0) return false;
+ return false;
}
}
}
return true;
}
+
+ private static BlockPos findMostLikelyCircle(World world, BlockPos pos){
+
+ int bestSoFar = 0;
+ BlockPos result = null;
+
+ for(int x = -1; x <= 1; x++){
+ for(int z = -1; z <= 1; z++){
+ if(x == 0 && z == 0) continue;
+ BlockPos pos1 = pos.add(x, 0, z);
+ int n = getCircleCompleteness(world, pos1);
+ if(n > bestSoFar){
+ bestSoFar = n;
+ result = pos1;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private static int getCircleCompleteness(World world, BlockPos pos){
+
+ int n = 0;
+
+ for(int x = -1; x <= 1; x++){
+ for(int z = -1; z <= 1; z++){
+ if(x == 0 && z == 0) continue;
+ if(world.getBlockState(pos.add(x, 0, z)).getBlock() == WizardryBlocks.transportation_stone) n++;
+ }
+ }
+
+ return n;
+ }
}
diff --git a/src/main/java/electroblob/wizardry/block/BlockVanishingCobweb.java b/src/main/java/electroblob/wizardry/block/BlockVanishingCobweb.java
index e37165a9..c8532ced 100644
--- a/src/main/java/electroblob/wizardry/block/BlockVanishingCobweb.java
+++ b/src/main/java/electroblob/wizardry/block/BlockVanishingCobweb.java
@@ -1,9 +1,8 @@
package electroblob.wizardry.block;
-import java.util.Random;
-
import electroblob.wizardry.tileentity.TileEntityTimer;
-import net.minecraft.block.BlockContainer;
+import net.minecraft.block.Block;
+import net.minecraft.block.ITileEntityProvider;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.Entity;
@@ -17,15 +16,16 @@ import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
-// For future reference - extend BlockContainer whenever possible because it has methods for removing tile entities on block break.
-public class BlockVanishingCobweb extends BlockContainer {
+import java.util.Random;
+
+public class BlockVanishingCobweb extends Block implements ITileEntityProvider {
public BlockVanishingCobweb(Material material){
super(material);
}
- @Override
@SideOnly(Side.CLIENT)
+ @Override
public BlockRenderLayer getRenderLayer(){
return BlockRenderLayer.CUTOUT;
}
@@ -50,6 +50,11 @@ public class BlockVanishingCobweb extends BlockContainer {
return false;
}
+ @Override
+ public boolean hasTileEntity(IBlockState state){
+ return true;
+ }
+
@Override
public TileEntity createNewTileEntity(World world, int metadata){
return new TileEntityTimer(400);
diff --git a/src/main/java/electroblob/wizardry/client/ClientProxy.java b/src/main/java/electroblob/wizardry/client/ClientProxy.java
index 4adcb7ac..ee3b2be0 100644
--- a/src/main/java/electroblob/wizardry/client/ClientProxy.java
+++ b/src/main/java/electroblob/wizardry/client/ClientProxy.java
@@ -1,147 +1,90 @@
package electroblob.wizardry.client;
-import java.lang.ref.WeakReference;
-import java.util.HashMap;
-
-import org.lwjgl.input.Keyboard;
-
import electroblob.wizardry.CommonProxy;
-import electroblob.wizardry.SpellGlyphData;
-import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.audio.MovingSoundEntity;
+import electroblob.wizardry.client.audio.SoundLoop;
+import electroblob.wizardry.client.audio.SoundLoopSpell;
+import electroblob.wizardry.client.gui.GuiSpellDisplay;
+import electroblob.wizardry.client.gui.config.NamedBooleanEntry;
+import electroblob.wizardry.client.gui.config.SpellHUDSkinChooserEntry;
+import electroblob.wizardry.client.gui.handbook.GuiWizardHandbook;
import electroblob.wizardry.client.model.ModelWizardArmour;
-import electroblob.wizardry.client.particle.ParticleBlizzard;
-import electroblob.wizardry.client.particle.ParticleDarkMagic;
-import electroblob.wizardry.client.particle.ParticleDust;
-import electroblob.wizardry.client.particle.ParticleGiantBubble;
-import electroblob.wizardry.client.particle.ParticleIce;
-import electroblob.wizardry.client.particle.ParticleLeaf;
-import electroblob.wizardry.client.particle.ParticleMagicFlame;
-import electroblob.wizardry.client.particle.ParticlePath;
-import electroblob.wizardry.client.particle.ParticleRotatingSparkle;
-import electroblob.wizardry.client.particle.ParticleSnow;
-import electroblob.wizardry.client.particle.ParticleSpark;
-import electroblob.wizardry.client.particle.ParticleSparkle;
-import electroblob.wizardry.client.particle.ParticleTornado;
-import electroblob.wizardry.client.renderer.LayerStone;
-import electroblob.wizardry.client.renderer.RenderArc;
-import electroblob.wizardry.client.renderer.RenderArcaneWorkbench;
-import electroblob.wizardry.client.renderer.RenderBlackHole;
-import electroblob.wizardry.client.renderer.RenderBlank;
-import electroblob.wizardry.client.renderer.RenderBubble;
-import electroblob.wizardry.client.renderer.RenderDecay;
-import electroblob.wizardry.client.renderer.RenderDecoy;
-import electroblob.wizardry.client.renderer.RenderEvilWizard;
-import electroblob.wizardry.client.renderer.RenderFireRing;
-import electroblob.wizardry.client.renderer.RenderForceArrow;
-import electroblob.wizardry.client.renderer.RenderHammer;
-import electroblob.wizardry.client.renderer.RenderIceGiant;
-import electroblob.wizardry.client.renderer.RenderIceSpike;
-import electroblob.wizardry.client.renderer.RenderLightningDisc;
-import electroblob.wizardry.client.renderer.RenderLightningPulse;
-import electroblob.wizardry.client.renderer.RenderMagicArrow;
-import electroblob.wizardry.client.renderer.RenderMagicLight;
-import electroblob.wizardry.client.renderer.RenderPhoenix;
-import electroblob.wizardry.client.renderer.RenderProjectile;
-import electroblob.wizardry.client.renderer.RenderSigil;
-import electroblob.wizardry.client.renderer.RenderSpiritHorse;
-import electroblob.wizardry.client.renderer.RenderSpiritWolf;
-import electroblob.wizardry.client.renderer.RenderStatue;
-import electroblob.wizardry.client.renderer.RenderWizard;
-import electroblob.wizardry.entity.EntityArc;
+import electroblob.wizardry.client.particle.*;
+import electroblob.wizardry.client.particle.ParticleWizardry.IWizardryParticleFactory;
+import electroblob.wizardry.client.renderer.*;
+import electroblob.wizardry.command.SpellEmitter;
+import electroblob.wizardry.data.DispenserCastingData;
+import electroblob.wizardry.data.SpellEmitterData;
+import electroblob.wizardry.data.SpellGlyphData;
+import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.entity.EntityShield;
-import electroblob.wizardry.entity.construct.EntityArrowRain;
-import electroblob.wizardry.entity.construct.EntityBlackHole;
-import electroblob.wizardry.entity.construct.EntityBlizzard;
-import electroblob.wizardry.entity.construct.EntityBubble;
-import electroblob.wizardry.entity.construct.EntityDecay;
-import electroblob.wizardry.entity.construct.EntityEarthquake;
-import electroblob.wizardry.entity.construct.EntityFireRing;
-import electroblob.wizardry.entity.construct.EntityFireSigil;
-import electroblob.wizardry.entity.construct.EntityForcefield;
-import electroblob.wizardry.entity.construct.EntityFrostSigil;
-import electroblob.wizardry.entity.construct.EntityHailstorm;
-import electroblob.wizardry.entity.construct.EntityHammer;
-import electroblob.wizardry.entity.construct.EntityHealAura;
-import electroblob.wizardry.entity.construct.EntityIceSpike;
-import electroblob.wizardry.entity.construct.EntityLightningPulse;
-import electroblob.wizardry.entity.construct.EntityLightningSigil;
-import electroblob.wizardry.entity.construct.EntityTornado;
-import electroblob.wizardry.entity.living.EntityDecoy;
-import electroblob.wizardry.entity.living.EntityEvilWizard;
-import electroblob.wizardry.entity.living.EntityIceGiant;
-import electroblob.wizardry.entity.living.EntityIceWraith;
-import electroblob.wizardry.entity.living.EntityLightningWraith;
-import electroblob.wizardry.entity.living.EntityPhoenix;
-import electroblob.wizardry.entity.living.EntityShadowWraith;
-import electroblob.wizardry.entity.living.EntitySpiritHorse;
-import electroblob.wizardry.entity.living.EntitySpiritWolf;
-import electroblob.wizardry.entity.living.EntityStormElemental;
-import electroblob.wizardry.entity.living.EntityWizard;
-import electroblob.wizardry.entity.living.ISpellCaster;
-import electroblob.wizardry.entity.living.ISummonedCreature;
-import electroblob.wizardry.entity.projectile.EntityDarknessOrb;
-import electroblob.wizardry.entity.projectile.EntityDart;
-import electroblob.wizardry.entity.projectile.EntityFirebolt;
-import electroblob.wizardry.entity.projectile.EntityFirebomb;
-import electroblob.wizardry.entity.projectile.EntityForceArrow;
-import electroblob.wizardry.entity.projectile.EntityForceOrb;
-import electroblob.wizardry.entity.projectile.EntityIceCharge;
-import electroblob.wizardry.entity.projectile.EntityIceLance;
-import electroblob.wizardry.entity.projectile.EntityIceShard;
-import electroblob.wizardry.entity.projectile.EntityLightningArrow;
-import electroblob.wizardry.entity.projectile.EntityLightningDisc;
-import electroblob.wizardry.entity.projectile.EntityMagicMissile;
-import electroblob.wizardry.entity.projectile.EntityPoisonBomb;
-import electroblob.wizardry.entity.projectile.EntitySmokeBomb;
-import electroblob.wizardry.entity.projectile.EntitySpark;
-import electroblob.wizardry.entity.projectile.EntitySparkBomb;
-import electroblob.wizardry.entity.projectile.EntityThunderbolt;
+import electroblob.wizardry.entity.construct.*;
+import electroblob.wizardry.entity.living.*;
+import electroblob.wizardry.entity.projectile.*;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.event.SpellCastEvent.Source;
+import electroblob.wizardry.integration.antiqueatlas.WizardryAntiqueAtlasIntegration;
import electroblob.wizardry.item.ItemScroll;
import electroblob.wizardry.item.ItemSpellBook;
import electroblob.wizardry.item.ItemWand;
-import electroblob.wizardry.packet.PacketCastContinuousSpell;
-import electroblob.wizardry.packet.PacketCastSpell;
-import electroblob.wizardry.packet.PacketNPCCastSpell;
-import electroblob.wizardry.packet.PacketPlayerSync.Message;
-import electroblob.wizardry.packet.PacketTransportation;
+import electroblob.wizardry.packet.*;
+import electroblob.wizardry.potion.PotionSlowTime;
import electroblob.wizardry.registry.Spells;
-import electroblob.wizardry.spell.Clairvoyance;
-import electroblob.wizardry.spell.None;
-import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.registry.WizardrySounds;
+import electroblob.wizardry.spell.*;
import electroblob.wizardry.tileentity.TileEntityArcaneWorkbench;
import electroblob.wizardry.tileentity.TileEntityMagicLight;
+import electroblob.wizardry.tileentity.TileEntityShrineCore;
import electroblob.wizardry.tileentity.TileEntityStatue;
+import electroblob.wizardry.util.ParticleBuilder;
+import electroblob.wizardry.util.ParticleBuilder.Type;
import electroblob.wizardry.util.WandHelper;
-import electroblob.wizardry.util.WizardryParticleType;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
+import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.GuiMerchant;
import net.minecraft.client.model.ModelBiped;
import net.minecraft.client.renderer.entity.RenderBlaze;
+import net.minecraft.client.renderer.entity.RenderHusk;
+import net.minecraft.client.renderer.entity.RenderSkeleton;
import net.minecraft.client.resources.I18n;
+import net.minecraft.client.resources.IReloadableResourceManager;
+import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.monster.EntityBlaze;
import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.init.SoundEvents;
+import net.minecraft.inventory.ContainerMerchant;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.tileentity.TileEntityDispenser;
import net.minecraft.util.EnumParticleTypes;
import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.SoundCategory;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.text.Style;
+import net.minecraft.util.text.TextComponentTranslation;
+import net.minecraft.village.MerchantRecipeList;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Property;
import net.minecraftforge.fml.client.config.GuiConfigEntries.NumberSliderEntry;
import net.minecraftforge.fml.client.registry.ClientRegistry;
import net.minecraftforge.fml.client.registry.RenderingRegistry;
+import org.lwjgl.input.Keyboard;
+
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* The client proxy for wizardry.
@@ -154,6 +97,9 @@ public class ClientProxy extends CommonProxy {
/** Static instance of the mixed font renderer */
public static MixedFontRenderer mixedFontRenderer;
+ /** Static particle factory map */
+ private static final Map factories = new HashMap<>();
+
// Key Bindings
public static final KeyBinding NEXT_SPELL = new KeyBinding("key." + Wizardry.MODID + ".next_spell", Keyboard.KEY_N, "key.categories." + Wizardry.MODID);
public static final KeyBinding PREVIOUS_SPELL = new KeyBinding("key." + Wizardry.MODID + ".previous_spell", Keyboard.KEY_B, "key.categories." + Wizardry.MODID);
@@ -161,6 +107,9 @@ public class ClientProxy extends CommonProxy {
// Armour Model
public static final ModelBiped WIZARD_ARMOUR_MODEL = new ModelWizardArmour(0.75f);
+ /** The wrap width for standard multi-line descriptions (see {@link ClientProxy#addMultiLineDescription(List, String, Style)}). */
+ private static final int TOOLTIP_WRAP_WIDTH = 140;
+
// SECTION Registry
// ===============================================================================================================
@@ -175,16 +124,28 @@ public class ClientProxy extends CommonProxy {
ClientRegistry.registerKeyBinding(PREVIOUS_SPELL);
}
- @Override
- public void registerSpellHUD(){
- MinecraftForge.EVENT_BUS.register(new GuiSpellDisplay(Minecraft.getMinecraft()));
- }
-
@Override
public void initGuiBits(){
mixedFontRenderer = new MixedFontRenderer(Minecraft.getMinecraft().gameSettings, new ResourceLocation("textures/font/ascii.png"),
Minecraft.getMinecraft().renderEngine, false);
- GuiWizardHandbook.initDisplayRecipes();
+ }
+
+ @Override
+ public void registerResourceReloadListeners(){
+ IResourceManager manager = Minecraft.getMinecraft().getResourceManager();
+ if(manager instanceof IReloadableResourceManager){
+ ((IReloadableResourceManager)manager).registerReloadListener(GuiSpellDisplay::loadSkins);
+ ((IReloadableResourceManager)manager).registerReloadListener(GuiWizardHandbook::loadHandbookFile);
+ }
+ }
+
+// @Override
+// public void registerSoundEventListener(){
+// Minecraft.getMinecraft().getSoundHandler().addListener(ContinuousSpellSoundEntity::soundPlayed);
+// }
+
+ public void registerAtlasMarkers(){
+ WizardryAntiqueAtlasIntegration.registerMarkers();
}
// SECTION Misc
@@ -194,6 +155,16 @@ public class ClientProxy extends CommonProxy {
public void setToNumberSliderEntry(Property property){
property.setConfigEntryClass(NumberSliderEntry.class);
}
+
+ @Override
+ public void setToHUDChooserEntry(Property property){
+ property.setConfigEntryClass(SpellHUDSkinChooserEntry.class);
+ }
+
+ @Override
+ public void setToNamedBooleanEntry(Property property){
+ property.setConfigEntryClass(NamedBooleanEntry.class);
+ }
@Override
public World getTheWorld(){
@@ -201,13 +172,66 @@ public class ClientProxy extends CommonProxy {
}
@Override
- public void playMovingSound(Entity entity, SoundEvent sound, float volume, float pitch, boolean repeat){
- Minecraft.getMinecraft().getSoundHandler().playSound(new MovingSoundEntity(entity, sound, volume, pitch, repeat));
+ public void playMovingSound(Entity entity, SoundEvent sound, SoundCategory category, float volume, float pitch, boolean repeat){
+ Minecraft.getMinecraft().getSoundHandler().playSound(new MovingSoundEntity<>(entity, sound, category, volume, pitch, repeat));
+ }
+
+ @Override
+ public void playSpellSoundLoop(EntityLivingBase entity, Spell spell, SoundEvent start, SoundEvent loop, SoundEvent end, SoundCategory category, float volume, float pitch){
+ SoundLoop.addLoop(new SoundLoopSpell.SoundLoopSpellEntity(start, loop, end, spell, entity, volume, pitch));
+ }
+
+ @Override
+ public void playSpellSoundLoop(World world, double x, double y, double z, Spell spell, SoundEvent start, SoundEvent loop, SoundEvent end, SoundCategory category, float volume, float pitch, int duration){
+ if(duration == -1){
+ SoundLoop.addLoop(new SoundLoopSpell.SoundLoopSpellDispenser(start, loop, end, spell, world, x, y, z, volume, pitch));
+ }else{
+ SoundLoop.addLoop(new SoundLoopSpell.SoundLoopSpellPosTimed(start, loop, end, spell, duration, x, y, z, volume, pitch));
+ }
+ }
+
+ @Override
+ public Set getSpellHUDSkins(){
+ return GuiSpellDisplay.getSkinKeys();
}
// SECTION Items
// ===============================================================================================================
+ @Override
+ public boolean shouldDisplayDiscovered(Spell spell, ItemStack stack){
+
+ EntityPlayerSP player = Minecraft.getMinecraft().player;
+
+ if(player == null) return false;
+
+ // Displayed recipe
+ if(Minecraft.getMinecraft().currentScreen instanceof GuiMerchant){
+ // It doesn't actually matter if the recipe is selected or not, since the itemstack will only ever
+ // match one of them anyway - and we'd have to reflect into GuiMerchant to get the selected recipe
+ MerchantRecipeList recipes = ((GuiMerchant)Minecraft.getMinecraft().currentScreen).getMerchant().getRecipes(player);
+ if(recipes != null && recipes.stream().anyMatch(r -> r.getItemToSell() == stack)){
+ // Spell books are always discovered when wizards are selling them
+ return true;
+ }
+ }
+
+ // Recipe output slot
+ // Required or players would be able to find out what the spell is without actually completing the trade
+ if(player.openContainer instanceof ContainerMerchant){
+
+ if(((ContainerMerchant)player.openContainer).getMerchantInventory().getStackInSlot(2) == stack){
+ return true;
+ }
+ }
+
+ if(!Wizardry.settings.discoveryMode) return true;
+ if(player.isCreative()) return true;
+ if(WizardData.get(player) != null && WizardData.get(player).hasSpellBeenDiscovered(spell)) return true;
+
+ return false;
+ }
+
@Override
public FontRenderer getFontRenderer(ItemStack stack){
@@ -216,12 +240,10 @@ public class ClientProxy extends CommonProxy {
if(stack.getItem() instanceof ItemWand){
spell = WandHelper.getCurrentSpell(stack);
}else if(stack.getItem() instanceof ItemSpellBook || stack.getItem() instanceof ItemScroll){
- spell = Spell.get(stack.getItemDamage());
+ spell = Spell.byMetadata(stack.getItemDamage());
}
- if(Minecraft.getMinecraft().player != null && Wizardry.settings.discoveryMode && WizardData.get(Minecraft.getMinecraft().player) != null
- && !Minecraft.getMinecraft().player.capabilities.isCreativeMode
- && !WizardData.get(Minecraft.getMinecraft().player).hasSpellBeenDiscovered(spell)){
+ if(!shouldDisplayDiscovered(spell, stack)){
return mixedFontRenderer;
}
@@ -231,16 +253,14 @@ public class ClientProxy extends CommonProxy {
@Override
public String getScrollDisplayName(ItemStack scroll){
- // Displays [Empty slot] if spell is continuous.
- Spell spell = Spell.get(scroll.getItemDamage());
- if(spell.isContinuous) spell = Spells.none;
+ Spell spell = Spell.byMetadata(scroll.getItemDamage());
EntityPlayer player = Minecraft.getMinecraft().player;
boolean discovered = true;
// It seems that this method is called when the world is loading, before thePlayer has been initialised.
// If the player is null, the spell is assumed to be discovered.
- if(player != null && Wizardry.settings.discoveryMode && !player.capabilities.isCreativeMode && WizardData.get(player) != null
+ if(player != null && Wizardry.settings.discoveryMode && !player.isCreative() && WizardData.get(player) != null
&& !WizardData.get(player).hasSpellBeenDiscovered(spell)){
discovered = false;
}
@@ -261,71 +281,59 @@ public class ClientProxy extends CommonProxy {
return super.getConjuredBowDurability(stack);
}
+ @Override
+ public void addMultiLineDescription(List tooltip, String key, Style style){
+ String description = style.getFormattingCode() + I18n.format(key);
+ tooltip.addAll(Minecraft.getMinecraft().fontRenderer.listFormattedStringToWidth(description, TOOLTIP_WRAP_WIDTH));
+ }
+
// SECTION Particles
// ===============================================================================================================
+ /** Use {@link ParticleWizardry#registerParticle(ResourceLocation, IWizardryParticleFactory)}, this is internal. */
+ // I mean, it does exactly the same thing but I might want to make it do something else in future...
+ public static void addParticleFactory(ResourceLocation name, IWizardryParticleFactory factory){
+ factories.put(name, factory);
+ }
+
@Override
- public void spawnParticle(WizardryParticleType type, World world, double x, double y, double z, double velX, double velY, double velZ, int maxAge,
- float r, float g, float b, boolean doGravity, double radius){
-
- // Colour values are now automatically clamped to between 0 and 1, as values outside this range seem to
- // cause strange effects in 1.10 (or more specifically, particles that are bright pink!)
- // TODO: This is a terrible dirty fix, but it'll do for now. Find a nicer way in future.
- if(type != WizardryParticleType.MAGIC_FIRE) r = MathHelper.clamp(r, 0, 1);
- g = MathHelper.clamp(g, 0, 1);
- b = MathHelper.clamp(b, 0, 1);
-
- switch(type){
-
- case BLIZZARD:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleBlizzard(world, maxAge, x, z, radius, y));
- break;
- case BRIGHT_DUST:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleDust(world, x, y, z, velX, velY, velZ, r, g, b, false));
- break;
- case DARK_MAGIC:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleDarkMagic(world, x, y, z, velX, velY, velZ, r, g, b));
- break;
- case DUST:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleDust(world, x, y, z, velX, velY, velZ, r, g, b, true));
- break;
- case ICE:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleIce(world, x, y, z, velX, velY, velZ, maxAge));
- break;
- case LEAF:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleLeaf(world, x, y, z, velX, velY, velZ, maxAge));
- break;
- case MAGIC_BUBBLE:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleGiantBubble(world, x, y, z, velX, velY, velZ));
- break;
- case MAGIC_FIRE:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleMagicFlame(world, x, y, z, velX, velY, velZ, maxAge, r == 0 ? 1 + world.rand.nextFloat() : r));
- break;
- case PATH:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticlePath(world, x, y, z, velX, velY, velZ, r, g, b, maxAge));
- break;
- case SNOW:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleSnow(world, x, y, z, velX, velY, velZ));
- break;
- case SPARK:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleSpark(world, x, y, z, velX, velY, velZ));
- break;
- case SPARKLE:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleSparkle(world, x, y, z, velX, velY, velZ, r, g, b, maxAge, doGravity));
- break;
- case SPARKLE_ROTATING:
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleRotatingSparkle(world, maxAge, x, z, radius, y, r, g, b));
- break;
- default:
- break;
+ public void registerParticles(){
+ // I'll be a good programmer and use the API method rather than the one above. Lead by example, as they say...
+ ParticleWizardry.registerParticle(Type.BEAM, ParticleBeam::new);
+ ParticleWizardry.registerParticle(Type.BUFF, ParticleBuff::new);
+ ParticleWizardry.registerParticle(Type.DARK_MAGIC, ParticleDarkMagic::new);
+ ParticleWizardry.registerParticle(Type.DUST, ParticleDust::new);
+ ParticleWizardry.registerParticle(Type.FLASH, ParticleFlash::new);
+ ParticleWizardry.registerParticle(Type.ICE, ParticleIce::new);
+ ParticleWizardry.registerParticle(Type.LEAF, ParticleLeaf::new);
+ ParticleWizardry.registerParticle(Type.LIGHTNING, ParticleLightning::new);
+ ParticleWizardry.registerParticle(Type.LIGHTNING_PULSE, ParticleLightningPulse::new);
+ ParticleWizardry.registerParticle(Type.MAGIC_BUBBLE, ParticleMagicBubble::new);
+ ParticleWizardry.registerParticle(Type.MAGIC_FIRE, ParticleMagicFlame::new);
+ ParticleWizardry.registerParticle(Type.PATH, ParticlePath::new);
+ ParticleWizardry.registerParticle(Type.SCORCH, ParticleScorch::new);
+ ParticleWizardry.registerParticle(Type.SNOW, ParticleSnow::new);
+ ParticleWizardry.registerParticle(Type.SPARK, ParticleSpark::new);
+ ParticleWizardry.registerParticle(Type.SPARKLE, ParticleSparkle::new);
+ ParticleWizardry.registerParticle(Type.SPHERE, ParticleSphere::new);
+ ParticleWizardry.registerParticle(Type.SUMMON, ParticleSummon::new);
+ ParticleWizardry.registerParticle(Type.VINE, ParticleVine::new);
+ }
+
+ @Override
+ public ParticleWizardry createParticle(ResourceLocation type, World world, double x, double y, double z){
+ IWizardryParticleFactory factory = factories.get(type);
+ if(factory == null){
+ Wizardry.logger.warn("Unrecognised particle type {} ! Ensure the particle is properly registered.", type);
+ return null;
}
+ return factory.createParticle(world, x, y, z);
}
@Override
public void spawnTornadoParticle(World world, double x, double y, double z, double velX, double velZ, double radius, int maxAge,
IBlockState block, BlockPos pos){
- Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleTornado(world, maxAge, x, z, radius, y, velX, velZ, block).setBlockPos(pos));// ,
- // world.rand.nextInt(6)));
+ Minecraft.getMinecraft().effectRenderer.addEffect(new ParticleTornado(world, maxAge, x, z, radius, y, velX, velZ, block).setBlockPos(pos));// , world.rand.nextInt(6)));
}
// SECTION Packet Handlers
@@ -336,14 +344,12 @@ public class ClientProxy extends CommonProxy {
World world = Minecraft.getMinecraft().world;
Entity caster = world.getEntityByID(message.casterID);
- Spell spell = Spell.get(message.spellID);
+ Spell spell = Spell.byNetworkID(message.spellID);
// Should always be true
if(caster instanceof EntityPlayer){
((EntityPlayer)caster).setActiveHand(message.hand);
- // Duration isn't needed because it only ever affects things server-side, and anything that is
- // seen client-side gets synced elsewhere.
spell.cast(world, (EntityPlayer)caster, message.hand, 0, message.modifiers);
Source source = Source.OTHER;
@@ -358,19 +364,34 @@ public class ClientProxy extends CommonProxy {
// No need to check if the spell succeeded, because the packet is only ever sent when it succeeds.
// The handler for this event now deals with discovery.
- MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post((EntityPlayer)caster, spell, message.modifiers, source));
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(source, spell, (EntityPlayer)caster, message.modifiers));
}else{
Wizardry.logger.warn("Recieved a PacketCastSpell, but the caster ID was not the ID of a player");
}
}
+ @Override
+ public void handleCastSpellAtPosPacket(PacketCastSpellAtPos.Message message){
+
+ World world = Minecraft.getMinecraft().world;
+ Spell spell = Spell.byNetworkID(message.spellID);
+
+ spell.cast(world, message.position.x, message.position.y, message.position.z, message.direction, 0, message.duration, message.modifiers);
+
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.COMMAND, spell, world, message.position.x, message.position.y, message.position.z, message.direction, message.modifiers));
+
+ if(spell.isContinuous){
+ SpellEmitter.add(spell, world, message.position.x, message.position.y, message.position.z, message.direction, message.duration, message.modifiers);
+ }
+ }
+
@Override
public void handleCastContinuousSpellPacket(PacketCastContinuousSpell.Message message){
World world = Minecraft.getMinecraft().world;
Entity caster = world.getEntityByID(message.casterID);
- Spell spell = Spell.get(message.spellID);
+ Spell spell = Spell.byNetworkID(message.spellID);
// Should always be true
if(caster instanceof EntityPlayer){
@@ -380,7 +401,7 @@ public class ClientProxy extends CommonProxy {
if(data.isCasting()){
WizardData.get((EntityPlayer)caster).stopCastingContinuousSpell();
}else{
- WizardData.get((EntityPlayer)caster).startCastingContinuousSpell(spell, message.modifiers);
+ WizardData.get((EntityPlayer)caster).startCastingContinuousSpell(spell, message.modifiers, message.duration);
}
}
}else{
@@ -394,7 +415,7 @@ public class ClientProxy extends CommonProxy {
World world = Minecraft.getMinecraft().world;
Entity caster = world.getEntityByID(message.casterID);
Entity target = message.targetID == -1 ? null : world.getEntityByID(message.targetID);
- Spell spell = Spell.get(message.spellID);
+ Spell spell = Spell.byNetworkID(message.spellID);
// Should always be true
if(caster instanceof EntityLiving){
@@ -403,7 +424,7 @@ public class ClientProxy extends CommonProxy {
spell.cast(world, (EntityLiving)caster, message.hand, 0, (EntityLivingBase)target, message.modifiers);
// Again, no need to check if the spell succeeded, because the packet is only ever sent when it
// succeeds.
- MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post((EntityLiving)caster, spell, message.modifiers, Source.NPC));
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.NPC, spell, (EntityLiving)caster, message.modifiers));
}
if(caster instanceof ISpellCaster){
@@ -412,10 +433,41 @@ public class ClientProxy extends CommonProxy {
((EntityLiving)caster).setAttackTarget((EntityLivingBase)target);
}
}
- }else{
+ }else if(caster != null){
Wizardry.logger.warn("Recieved a PacketNPCCastSpell, but the caster ID was not the ID of an EntityLiving");
}
}
+
+ @Override
+ public void handleDispenserCastSpellPacket(PacketDispenserCastSpell.Message message){
+
+ World world = Minecraft.getMinecraft().world;
+
+ if(world.getTileEntity(message.pos) instanceof TileEntityDispenser){ // Should always be true
+
+ Spell spell = Spell.byNetworkID(message.spellID);
+
+ spell.cast(world, message.x, message.y, message.z, message.direction, 0, -1, message.modifiers);
+ // No need to check if the spell succeeded, because the packet is only ever sent when it succeeds.
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.DISPENSER, spell, world, message.x, message.y,
+ message.z, message.direction, message.modifiers));
+
+ if(spell.isContinuous || spell instanceof None){
+
+ DispenserCastingData data = DispenserCastingData.get((TileEntityDispenser)world.getTileEntity(message.pos));
+
+ if(spell.isContinuous){
+ data.startCasting(spell, message.x, message.y, message.z, message.duration, message.modifiers);
+ }else{
+ data.stopCasting();
+ }
+ }
+
+ }else{
+ Wizardry.logger.warn("Recieved a PacketDispenserCastSpell, but no tileEntity was found at the supplied location.");
+ }
+
+ }
@Override
public void handleTransportationPacket(PacketTransportation.Message message){
@@ -423,78 +475,150 @@ public class ClientProxy extends CommonProxy {
World world = Minecraft.getMinecraft().world;
Entity caster = world.getEntityByID(message.casterID);
// Moved from when the packet is sent to when it is received; fixes the sound not playing in first person.
- caster.playSound(SoundEvents.BLOCK_PORTAL_TRAVEL, 1, 1);
+ caster.playSound(WizardrySounds.SPELL_TRANSPORTATION_TRAVEL, 1, 1);
for(int i = 0; i < 20; i++){
double radius = 1;
- double angle = world.rand.nextDouble() * Math.PI * 2;
- double x = caster.posX + radius * Math.cos(angle);
+ float angle = world.rand.nextFloat() * (float)Math.PI * 2;
+ double x = caster.posX + radius * MathHelper.cos(angle);
double y = caster.getEntityBoundingBox().minY + world.rand.nextDouble() * 2;
- double z = caster.posZ + radius * Math.sin(angle);
- Minecraft.getMinecraft().effectRenderer
- .addEffect(new ParticleSparkle(world, x, y, z, 0, 0.02, 0, 0.6f, 1.0f, 0.6f, 80 + world.rand.nextInt(10)));
+ double z = caster.posZ + radius * MathHelper.sin(angle);
+ ParticleBuilder.create(Type.SPARKLE).pos(x, y, z).vel(0, 0.02, 0).clr(0.6f, 1, 0.6f)
+ .time(80 + world.rand.nextInt(10)).spawn(world);
}
for(int i = 0; i < 20; i++){
double radius = 1;
- double angle = world.rand.nextDouble() * Math.PI * 2;
- double x = caster.posX + radius * Math.cos(angle);
+ float angle = world.rand.nextFloat() * (float)Math.PI * 2;
+ double x = caster.posX + radius * MathHelper.cos(angle);
double y = caster.getEntityBoundingBox().minY + world.rand.nextDouble() * 2;
- double z = caster.posZ + radius * Math.sin(angle);
+ double z = caster.posZ + radius * MathHelper.sin(angle);
world.spawnParticle(EnumParticleTypes.VILLAGER_HAPPY, x, y, z, 0, 0.02, 0);
}
for(int i = 0; i < 20; i++){
double radius = 1;
- double angle = world.rand.nextDouble() * Math.PI * 2;
- double x = caster.posX + radius * Math.cos(angle);
+ float angle = world.rand.nextFloat() * (float)Math.PI * 2;
+ double x = caster.posX + radius * MathHelper.cos(angle);
double y = caster.getEntityBoundingBox().minY + world.rand.nextDouble() * 2;
- double z = caster.posZ + radius * Math.sin(angle);
+ double z = caster.posZ + radius * MathHelper.sin(angle);
world.spawnParticle(EnumParticleTypes.ENCHANTMENT_TABLE, x, y, z, 0, 0.02, 0);
}
}
@Override
- public void handlePlayerSyncPacket(Message message){
+ public void handlePlayerSyncPacket(PacketPlayerSync.Message message){
- WizardData properties = WizardData.get(Minecraft.getMinecraft().player);
+ WizardData data = WizardData.get(Minecraft.getMinecraft().player);
- if(properties != null){
+ if(data != null){
- properties.spellsDiscovered = message.spellsDiscovered;
+ data.synchronisedRandom.setSeed(message.seed);
+ data.spellsDiscovered = message.spellsDiscovered;
+
+ message.spellData.forEach(data::setVariable);
if(message.selectedMinionID == -1){
- properties.selectedMinion = null;
+ data.selectedMinion = null;
}else{
Entity entity = Minecraft.getMinecraft().world.getEntityByID(message.selectedMinionID);
if(entity instanceof ISummonedCreature){
- properties.selectedMinion = new WeakReference((ISummonedCreature)entity);
+ data.selectedMinion = new WeakReference<>((ISummonedCreature)entity);
}else{
- properties.selectedMinion = null;
+ data.selectedMinion = null;
}
}
}
}
@Override
- public void handleGlyphDataPacket(electroblob.wizardry.packet.PacketGlyphData.Message message){
+ public void handleGlyphDataPacket(PacketGlyphData.Message message){
SpellGlyphData data = SpellGlyphData.get(Minecraft.getMinecraft().world);
- data.randomNames = new HashMap();
- data.randomDescriptions = new HashMap();
+ data.randomNames = new HashMap<>();
+ data.randomDescriptions = new HashMap<>();
for(Spell spell : Spell.getSpells(Spell.allSpells)){
// -1 because the none spell isn't included
- data.randomNames.put(spell, message.names.get(spell.id() - 1));
- data.randomDescriptions.put(spell, message.descriptions.get(spell.id() - 1));
+ // This is a case where we must use the network ID, not the metadata
+ data.randomNames.put(spell, message.names.get(spell.networkID() - 1));
+ data.randomDescriptions.put(spell, message.descriptions.get(spell.networkID() - 1));
}
}
@Override
- public void handleClairvoyancePacket(electroblob.wizardry.packet.PacketClairvoyance.Message message){
+ public void handleEmitterDataPacket(PacketEmitterData.Message message){
+ message.emitters.forEach(e -> e.setWorld(Minecraft.getMinecraft().world)); // Do this as soon as possible!
+ SpellEmitterData data = SpellEmitterData.get(Minecraft.getMinecraft().world);
+ // We shouldn't need to clear the emitters because when a player logs in or changes dimension the client world
+ // is wiped anyway, so the call to get() above should result in a fresh SpellEmitterData instance
+ message.emitters.forEach(data::add);
+ }
+
+ @Override
+ public void handleClairvoyancePacket(PacketClairvoyance.Message message){
Clairvoyance.spawnPathPaticles(Minecraft.getMinecraft().world, message.path, message.durationMultiplier);
}
+ @Override
+ public void handleAdvancementSyncPacket(PacketSyncAdvancements.Message message){
+ GuiWizardHandbook.updateUnlockStatus(message.showToasts, message.completedAdvancements);
+ }
+
+ @Override
+ public void handleEndSlowTimePacket(PacketEndSlowTime.Message message){
+ Entity entity = Minecraft.getMinecraft().world.getEntityByID(message.hostID);
+ if(entity instanceof EntityLivingBase) PotionSlowTime.unblockNearbyEntities((EntityLivingBase)entity);
+ else Wizardry.logger.warn("Received a PacketEndSlowTime, but the entity ID did not match any living entity");
+ }
+
+ @Override
+ public void handleResurrectionPacket(PacketResurrection.Message message){
+ Entity entity = Minecraft.getMinecraft().world.getEntityByID(message.playerID);
+ if(entity instanceof EntityPlayer){
+ ((Resurrection)Spells.resurrection).resurrect((EntityPlayer)entity);
+ if(entity == Minecraft.getMinecraft().player){
+ Minecraft.getMinecraft().world.spawnEntity(entity);
+ Minecraft.getMinecraft().displayGuiScreen(null);
+ }
+ }
+ else Wizardry.logger.warn("Received a PacketResurrection, but the entity ID did not match any player");
+ }
+
+ @Override
+ public void handlePossessionPacket(PacketPossession.Message message){
+
+ Entity entity = Minecraft.getMinecraft().world.getEntityByID(message.playerID);
+
+ if(entity instanceof EntityPlayer){
+
+ EntityPlayer player = (EntityPlayer)entity;
+
+ if(message.targetID == -1){
+ ((Possession)Spells.possession).endPossession(player);
+ }else{
+ Entity target = Minecraft.getMinecraft().world.getEntityByID(message.targetID);
+ if(target instanceof EntityLiving){
+ ((Possession)Spells.possession).possess(player, (EntityLiving)target, message.duration);
+ player.sendStatusMessage(new TextComponentTranslation("spell." + Spells.possession.getRegistryName()
+ + ".success", Minecraft.getMinecraft().gameSettings.keyBindSneak.getDisplayName()), true);
+ }
+ else Wizardry.logger.warn("Received a PacketPossession, but the target ID did not match any living entity");
+ }
+ }
+ else Wizardry.logger.warn("Received a PacketPossession, but the player ID did not match any player");
+ }
+
+ public void handleConquerShrinePacket(PacketConquerShrine.Message message){
+
+ TileEntity tileEntity = Minecraft.getMinecraft().world.getTileEntity(new BlockPos(message.x, message.y, message.z));
+
+ if(tileEntity instanceof TileEntityShrineCore){
+ ((TileEntityShrineCore)tileEntity).conquer();
+
+ }else Wizardry.logger.warn("Received a PacketConquerShrine, but there was no shrine core at the position sent");
+ }
+
// SECTION Rendering
// ===============================================================================================================
@@ -507,6 +631,7 @@ public class ClientProxy extends CommonProxy {
@Override
public void initialiseLayers(){
LayerStone.initialiseLayers();
+ LayerFrost.initialiseLayers();
}
@Override
@@ -516,6 +641,12 @@ public class ClientProxy extends CommonProxy {
// Yet another advantage to the new system: turns out you don't even need to register the renderer if you
// just want the vanilla one for the mob you're extending.
+ // Luckily for us, the vanilla husk renderer is only parametrised to EntityZombie
+ RenderingRegistry.registerEntityRenderingHandler(EntityHuskMinion.class, RenderHusk::new);
+ // This now extends AbstractSkeleton so we need to bind the renderer ourselves
+ RenderingRegistry.registerEntityRenderingHandler(EntitySkeletonMinion.class, RenderSkeleton::new);
+ RenderingRegistry.registerEntityRenderingHandler(EntityStrayMinion.class, RenderStrayMinion::new);
+
// An anonymous class in a lambda expression! No point writing a separate class really, is there?
RenderingRegistry.registerEntityRenderingHandler(EntityLightningWraith.class, manager -> new RenderBlaze(manager){
@Override
@@ -549,15 +680,15 @@ public class ClientProxy extends CommonProxy {
RenderingRegistry.registerEntityRenderingHandler(EntityForceArrow.class, RenderForceArrow::new);
// Creatures
- RenderingRegistry.registerEntityRenderingHandler(EntitySpiritWolf.class, manager -> new RenderSpiritWolf(manager));
- RenderingRegistry.registerEntityRenderingHandler(EntitySpiritHorse.class, manager -> new RenderSpiritHorse(manager));
+ RenderingRegistry.registerEntityRenderingHandler(EntitySpiritWolf.class, RenderSpiritWolf::new);
+ RenderingRegistry.registerEntityRenderingHandler(EntitySpiritHorse.class, RenderSpiritHorse::new);
RenderingRegistry.registerEntityRenderingHandler(EntityWizard.class, RenderWizard::new);
RenderingRegistry.registerEntityRenderingHandler(EntityEvilWizard.class, RenderEvilWizard::new);
RenderingRegistry.registerEntityRenderingHandler(EntityDecoy.class, RenderDecoy::new);
// Throwables
RenderingRegistry.registerEntityRenderingHandler(EntitySparkBomb.class,
- manager -> new RenderProjectile(manager, 0.6f, new ResourceLocation(Wizardry.MODID, "textures/entity/spark_bomb.png"), false));
+ manager -> new RenderProjectile(manager, 0.6f, new ResourceLocation(Wizardry.MODID, "textures/items/spark_bomb.png"), false));
RenderingRegistry.registerEntityRenderingHandler(EntityFirebomb.class,
manager -> new RenderProjectile(manager, 0.6f, new ResourceLocation(Wizardry.MODID, "textures/items/firebomb.png"), false));
RenderingRegistry.registerEntityRenderingHandler(EntityPoisonBomb.class,
@@ -576,21 +707,29 @@ public class ClientProxy extends CommonProxy {
manager -> new RenderLightningDisc(manager, new ResourceLocation(Wizardry.MODID, "textures/entity/lightning_sigil.png"), 2.0f));
RenderingRegistry.registerEntityRenderingHandler(EntitySmokeBomb.class,
manager -> new RenderProjectile(manager, 0.6f, new ResourceLocation(Wizardry.MODID, "textures/items/smoke_bomb.png"), false));
+ RenderingRegistry.registerEntityRenderingHandler(EntityEmber.class,
+ manager -> new RenderProjectile(manager, 0.15f, new ResourceLocation(Wizardry.MODID, "textures/entity/ember.png"), false));
+ RenderingRegistry.registerEntityRenderingHandler(EntityMagicFireball.class,
+ manager -> new RenderProjectile(manager, 0.7f, new ResourceLocation(Wizardry.MODID, "textures/entity/fireball.png"), false));
+ RenderingRegistry.registerEntityRenderingHandler(EntityLargeMagicFireball.class,
+ manager -> new RenderProjectile(manager, 1.5f, new ResourceLocation(Wizardry.MODID, "textures/entity/fireball.png"), false));
+ RenderingRegistry.registerEntityRenderingHandler(EntityIceball.class,
+ manager -> new RenderProjectile(manager, 0.7f, new ResourceLocation(Wizardry.MODID, "textures/entity/iceball.png"), false));
// Effects and constructs
- RenderingRegistry.registerEntityRenderingHandler(EntityArc.class, RenderArc::new);
RenderingRegistry.registerEntityRenderingHandler(EntityBlackHole.class, RenderBlackHole::new);
RenderingRegistry.registerEntityRenderingHandler(EntityShield.class, RenderBlank::new);
RenderingRegistry.registerEntityRenderingHandler(EntityBubble.class, RenderBubble::new);
RenderingRegistry.registerEntityRenderingHandler(EntityHammer.class, RenderHammer::new);
RenderingRegistry.registerEntityRenderingHandler(EntityIceSpike.class, RenderIceSpike::new);
+ RenderingRegistry.registerEntityRenderingHandler(EntityForcefield.class, RenderForcefield::new);
+ //RenderingRegistry.registerEntityRenderingHandler(EntityContainmentField.class, RenderContainmentField::new);
// Stuff that doesn't render
RenderingRegistry.registerEntityRenderingHandler(EntityBlizzard.class, RenderBlank::new);
RenderingRegistry.registerEntityRenderingHandler(EntityTornado.class, RenderBlank::new);
RenderingRegistry.registerEntityRenderingHandler(EntityArrowRain.class, RenderBlank::new);
RenderingRegistry.registerEntityRenderingHandler(EntityShadowWraith.class, RenderBlank::new);
- RenderingRegistry.registerEntityRenderingHandler(EntityForcefield.class, RenderBlank::new);
RenderingRegistry.registerEntityRenderingHandler(EntityThunderbolt.class, RenderBlank::new);
RenderingRegistry.registerEntityRenderingHandler(EntityStormElemental.class, RenderBlank::new);
RenderingRegistry.registerEntityRenderingHandler(EntityEarthquake.class, RenderBlank::new);
@@ -608,7 +747,8 @@ public class ClientProxy extends CommonProxy {
RenderingRegistry.registerEntityRenderingHandler(EntityFireRing.class,
manager -> new RenderFireRing(manager, new ResourceLocation(Wizardry.MODID, "textures/entity/ring_of_fire.png"), 5.0f));
RenderingRegistry.registerEntityRenderingHandler(EntityDecay.class, RenderDecay::new);
- RenderingRegistry.registerEntityRenderingHandler(EntityLightningPulse.class, manager -> new RenderLightningPulse(manager, 8.0f));
+ RenderingRegistry.registerEntityRenderingHandler(EntityCombustionRune.class,
+ manager -> new RenderSigil(manager, new ResourceLocation(Wizardry.MODID, "textures/entity/combustion_rune.png"), 2.0f, true));
// TESRs
ClientRegistry.bindTileEntitySpecialRenderer(TileEntityArcaneWorkbench.class, new RenderArcaneWorkbench());
diff --git a/src/main/java/electroblob/wizardry/client/DrawingUtils.java b/src/main/java/electroblob/wizardry/client/DrawingUtils.java
new file mode 100644
index 00000000..20b99a60
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/DrawingUtils.java
@@ -0,0 +1,247 @@
+package electroblob.wizardry.client;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.inventory.GuiContainer;
+import net.minecraft.client.renderer.*;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
+
+/**
+ * Utility class containing some useful static methods for drawing GUIs. Previously these were spread across the main
+ * {@code WizardryUtilities} class and various individual GUI classes.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.2
+ * @see MixedFontRenderer
+ */
+//@SideOnly(Side.CLIENT)
+public final class DrawingUtils {
+
+ /**
+ * The integer colour for black passed into the font renderer methods. This used to be 0 but that's now white for
+ * some reason, so I've made a it a constant in case it changes again.
+ */
+ // I think this is actually ever-so-slightly lighter than pure black, but the difference is unnoticeable.
+ public static final int BLACK = 1;
+
+ /**
+ * Shorthand for {@link DrawingUtils#drawTexturedRect(int, int, int, int, int, int, int, int)} which draws the
+ * entire texture (u and v are set to 0 and textureWidth and textureHeight are the same as width and height).
+ */
+ public static void drawTexturedRect(int x, int y, int width, int height){
+ drawTexturedRect(x, y, 0, 0, width, height, width, height);
+ }
+
+ /**
+ * Draws a textured rectangle, taking the size of the image and the bit needed into
+ * account, unlike {@link net.minecraft.client.gui.Gui#drawTexturedModalRect(int, int, int, int, int, int)
+ * Gui.drawTexturedModalRect(int, int, int, int, int, int)}, which is harcoded for only 256x256 textures. Also handy
+ * for custom potion icons.
+ *
+ * @param x The x position of the rectangle
+ * @param y The y position of the rectangle
+ * @param u The x position of the top left corner of the section of the image wanted
+ * @param v The y position of the top left corner of the section of the image wanted
+ * @param width The width of the section
+ * @param height The height of the section
+ * @param textureWidth The width of the actual image.
+ * @param textureHeight The height of the actual image.
+ */
+ public static void drawTexturedRect(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight){
+ DrawingUtils.drawTexturedFlippedRect(x, y, u, v, width, height, textureWidth, textureHeight, false, false);
+ }
+
+ /**
+ * Draws a textured rectangle, taking the size of the image and the bit needed into
+ * account, unlike {@link net.minecraft.client.gui.Gui#drawTexturedModalRect(int, int, int, int, int, int)
+ * Gui.drawTexturedModalRect(int, int, int, int, int, int)}, which is harcoded for only 256x256 textures. Also handy
+ * for custom potion icons. This version allows the texture to additionally be flipped in x and/or y.
+ *
+ * @param x The x position of the rectangle
+ * @param y The y position of the rectangle
+ * @param u The x position of the top left corner of the section of the image wanted
+ * @param v The y position of the top left corner of the section of the image wanted
+ * @param width The width of the section
+ * @param height The height of the section
+ * @param textureWidth The width of the actual image.
+ * @param textureHeight The height of the actual image.
+ * @param flipX Whether to flip the texture in the x direction.
+ * @param flipY Whether to flip the texture in the y direction.
+ */
+ public static void drawTexturedFlippedRect(int x, int y, int u, int v, int width, int height, int textureWidth, int textureHeight, boolean flipX, boolean flipY){
+
+ float f = 1F / (float)textureWidth;
+ float f1 = 1F / (float)textureHeight;
+
+ int u1 = flipX ? u + width : u;
+ int u2 = flipX ? u : u + width;
+ int v1 = flipY ? v + height : v;
+ int v2 = flipY ? v : v + height;
+
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ buffer.begin(org.lwjgl.opengl.GL11.GL_QUADS, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos((double)(x), (double)(y + height), 0).tex((double)((float)(u1) * f), (double)((float)(v2) * f1)).endVertex();
+ buffer.pos((double)(x + width), (double)(y + height), 0).tex((double)((float)(u2) * f), (double)((float)(v2) * f1)).endVertex();
+ buffer.pos((double)(x + width), (double)(y), 0).tex((double)((float)(u2) * f), (double)((float)(v1) * f1)).endVertex();
+ buffer.pos((double)(x), (double)(y), 0).tex((double)((float)(u1) * f), (double)((float)(v1) * f1)).endVertex();
+
+ tessellator.draw();
+ }
+
+ /**
+ * Draws a textured rectangle, stretching the section of the image to fit the size given.
+ *
+ * @param x The x position of the rectangle
+ * @param y The y position of the rectangle
+ * @param u The x position of the top left corner of the section of the image wanted, expressed as a fraction of the
+ * image width
+ * @param v The y position of the top left corner of the section of the image wanted, expressed as a fraction of the
+ * image width
+ * @param finalWidth The width as rendered
+ * @param finalHeight The height as rendered
+ * @param width The width of the section, expressed as a fraction of the image width
+ * @param height The height of the section, expressed as a fraction of the image width
+ */
+ public static void drawTexturedStretchedRect(int x, int y, int u, int v, int finalWidth, int finalHeight, int width,
+ int height){
+
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos((x), y + finalHeight, 0).tex(u, v + height).endVertex();
+ buffer.pos(x + finalWidth, y + finalHeight, 0).tex(u + width, v + height).endVertex();
+ buffer.pos(x + finalWidth, (y), 0).tex(u + width, v).endVertex();
+ buffer.pos((x), (y), 0).tex(u, v).endVertex();
+
+ tessellator.draw();
+ }
+
+ /**
+ * Mixes the two given opaque colours in the proportion specified.
+ * @param colour1 The first colour to mix, as a 6-digit hexadecimal.
+ * @param colour2 The second colour to mix, as a 6-digit hexadecimal.
+ * @param proportion The proportion of the second colour; will be clamped to between 0 and 1.
+ * @return The resulting colour, as a 6-digit hexadecimal.
+ */
+ public static int mix(int colour1, int colour2, float proportion){
+
+ proportion = MathHelper.clamp(proportion, 0, 1);
+
+ int r1 = colour1 >> 16 & 255;
+ int g1 = colour1 >> 8 & 255;
+ int b1 = colour1 & 255;
+ int r2 = colour2 >> 16 & 255;
+ int g2 = colour2 >> 8 & 255;
+ int b2 = colour2 & 255;
+
+ int r = (int)(r1 + (r2-r1) * proportion);
+ int g = (int)(g1 + (g2-g1) * proportion);
+ int b = (int)(b1 + (b2-b1) * proportion);
+
+ return (r << 16) + (g << 8) + b;
+ }
+
+ /**
+ * Makes the given opaque colour translucent with the given opacity.
+ * @param colour An integer colour code, should be a 6-digit hexadecimal (i.e. opaque).
+ * @param opacity The opacity to apply to the given colour, as a fraction between 0 and 1.
+ * @return The resulting integer colour code, which will be an 8-digit hexadecimal.
+ */
+ public static int makeTranslucent(int colour, float opacity){
+ return colour + ((int)(0xff * opacity * 0x01000000));
+ }
+
+ /**
+ * Draws the given string at the given position, scaling it if it does not fit within the given width.
+ * @param font A {@code FontRenderer} object.
+ * @param text The text to display.
+ * @param x The x position of the top-left corner of the text.
+ * @param y The y position of the top-left corner of the text.
+ * @param scale The scale that the text should normally be if it does not exceed the maximum width.
+ * @param colour The colour to render the text in, supports translucency.
+ * @param width The maximum width of the text. This is not scaled; you should pass in the width of the actual
+ * area of the screen in which the text needs to fit, regardless of the scale parameter.
+ * @param centre Whether to adjust the y position such that the centre of the text lines up with where its centre
+ * would be if it was not scaled (automatically or manually).
+ * @param alignR True to right-align the text, false for normal left alignment.
+ */
+ public static void drawScaledStringToWidth(FontRenderer font, String text, float x, float y, float scale, int colour, float width, boolean centre, boolean alignR){
+
+ float textWidth = font.getStringWidth(text) * scale;
+ float textHeight = font.FONT_HEIGHT * scale;
+
+ if(textWidth > width){
+ scale *= width/textWidth;
+ }else if(alignR){ // Alignment makes no difference if the string fills the entire width
+ x += width - textWidth;
+ }
+
+ if(centre) y += (font.FONT_HEIGHT - textHeight)/2;
+
+ DrawingUtils.drawScaledTranslucentString(font, text, x, y, scale, colour);
+ }
+
+ /** Draws the given string at the given position, scaling the text by the specified factor. Also enables blending to
+ * render text in semitransparent colours (e.g. 0x88ffffff). */
+ public static void drawScaledTranslucentString(FontRenderer font, String text, float x, float y, float scale, int colour){
+
+ GlStateManager.pushMatrix();
+ GlStateManager.enableBlend();
+ GlStateManager.scale(scale, scale, scale);
+ // Because we scaled the entire rendering space, the coordinates have to be scaled inversely
+ x /= scale;
+ y /= scale;
+ font.drawStringWithShadow(text, x, y, colour);
+ GlStateManager.disableBlend();
+ GlStateManager.popMatrix();
+ }
+
+ /**
+ * Draws an itemstack and (optionally) its tooltip, directly. Mainly intended for use outside of GUI classes, since
+ * most of the GL state changes done in this method (which are the main reason it exists at all) are already done
+ * when drawing a GUI.
+ *
+ * @param gui An instance of a GUI class.
+ * @param stack The itemstack to draw.
+ * @param x The x position of the left-hand edge of the itemstack.
+ * @param y The y position of the top edge of the itemstack.
+ * @param mouseX The x position of the mouse, used for tooltip positioning.
+ * @param mouseY The y position of the mouse, used for tooltip positioning.
+ * @param tooltip Whether to draw the tooltip.
+ */
+ public static void drawItemAndTooltip(GuiContainer gui, ItemStack stack, int x, int y, int mouseX, int mouseY, boolean tooltip){
+
+ RenderItem renderItem = Minecraft.getMinecraft().getRenderItem();
+ GlStateManager.pushMatrix();
+ RenderHelper.enableGUIStandardItemLighting();
+ GlStateManager.disableLighting();
+ GlStateManager.enableRescaleNormal();
+ GlStateManager.enableColorMaterial();
+ GlStateManager.enableLighting();
+ renderItem.zLevel = 100.0F;
+
+ if(!stack.isEmpty()){
+ renderItem.renderItemAndEffectIntoGUI(stack, x, y);
+ renderItem.renderItemOverlays(Minecraft.getMinecraft().fontRenderer, stack, x, y);
+
+ if(tooltip){
+ gui.drawHoveringText(gui.getItemToolTip(stack), mouseX + gui.getXSize()/2 - gui.width/2,
+ mouseY + gui.getYSize()/2 - gui.height/2);
+ }
+ }
+
+ GlStateManager.popMatrix();
+ GlStateManager.enableLighting();
+ GlStateManager.enableDepth();
+ RenderHelper.enableStandardItemLighting();
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/GuiArcaneWorkbench.java b/src/main/java/electroblob/wizardry/client/GuiArcaneWorkbench.java
deleted file mode 100644
index edc27350..00000000
--- a/src/main/java/electroblob/wizardry/client/GuiArcaneWorkbench.java
+++ /dev/null
@@ -1,251 +0,0 @@
-package electroblob.wizardry.client;
-
-import org.lwjgl.input.Keyboard;
-
-import electroblob.wizardry.SpellGlyphData;
-import electroblob.wizardry.WizardData;
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.constants.Element;
-import electroblob.wizardry.item.ItemWand;
-import electroblob.wizardry.packet.PacketControlInput;
-import electroblob.wizardry.packet.WizardryPacketHandler;
-import electroblob.wizardry.spell.Spell;
-import electroblob.wizardry.tileentity.ContainerArcaneWorkbench;
-import electroblob.wizardry.tileentity.TileEntityArcaneWorkbench;
-import electroblob.wizardry.util.WandHelper;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiButton;
-import net.minecraft.client.gui.inventory.GuiContainer;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.resources.I18n;
-import net.minecraft.entity.player.InventoryPlayer;
-import net.minecraft.inventory.IInventory;
-import net.minecraft.inventory.Slot;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
-
-public class GuiArcaneWorkbench extends GuiContainer {
-
- private GuiButton applyBtn;
- private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID,
- "textures/gui/arcane_workbench.png");
-
- private IInventory playerInventory;
- private IInventory arcaneWorkbenchInventory;
-
- private final int tooltipWidth = 164;
-
- // We report the actual size of the GUI to Minecraft when a wand is in so JEI doesn't overdraw it.
- // For calculations, we use the size without the tooltip.
- private final int xSizeNoTip = 176;
-
- public GuiArcaneWorkbench(InventoryPlayer invPlayer, TileEntityArcaneWorkbench entity){
- super(new ContainerArcaneWorkbench(invPlayer, entity));
- this.playerInventory = invPlayer;
- this.arcaneWorkbenchInventory = entity;
- xSize = xSizeNoTip;
- ySize = 220;
- }
-
- @Override
- public void drawScreen(int p_73863_1_, int p_73863_2_, float p_73863_3_){
-
- this.drawDefaultBackground();
-
- // Tests if there is a wand in the workbench and edits the positioning accordingly
- if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.WAND_SLOT).getHasStack() && this.inventorySlots
- .getSlot(ContainerArcaneWorkbench.WAND_SLOT).getStack().getItem() instanceof ItemWand){
- xSize = xSizeNoTip + tooltipWidth;
- guiLeft = (this.width - this.xSize) / 2;
- this.applyBtn.x = (this.width - tooltipWidth) / 2 + 48;
- }else{
- xSize = xSizeNoTip;
- guiLeft = (this.width - this.xSize) / 2;
- this.applyBtn.x = this.width / 2 + 48;
- }
-
- if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.WAND_SLOT).getHasStack()){
- this.applyBtn.enabled = true;
- }else{
- this.applyBtn.enabled = false;
- }
-
- super.drawScreen(p_73863_1_, p_73863_2_, p_73863_3_);
-
- // Required now, or item mouseover tooltips won't render.
- this.renderHoveredToolTip(p_73863_1_, p_73863_2_);
- }
-
- @Override
- public void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY){
-
- GlStateManager.color(1F, 1F, 1F, 1F);
- Minecraft.getMinecraft().renderEngine.bindTexture(texture);
-
- // Main inventory
- drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSizeNoTip, ySize);
-
- // Changing slots
- for(int i = 0; i < ContainerArcaneWorkbench.CRYSTAL_SLOT; i++){
- Slot slot = this.inventorySlots.getSlot(i);
- if(slot.xPos >= 0 && slot.yPos >= 0)
- this.drawTexturedModalRect(guiLeft + slot.xPos - 10, guiTop + slot.yPos - 10, 0, 220, 36, 36);
- }
-
- // Tooltip only drawn if there is a wand
- if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.WAND_SLOT).getHasStack() && this.inventorySlots
- .getSlot(ContainerArcaneWorkbench.WAND_SLOT).getStack().getItem() instanceof ItemWand){
-
- // Tooltip box
- drawTexturedModalRect(guiLeft + xSizeNoTip, guiTop, xSizeNoTip, 0, 256 - xSizeNoTip - 4, ySize);
- drawTexturedModalRect(guiLeft + 252, guiTop, xSizeNoTip + 4, 0, tooltipWidth - 2 * (256 - xSizeNoTip - 4), ySize);
- drawTexturedModalRect(guiLeft + xSize - (256 - xSizeNoTip - 4), guiTop, xSizeNoTip + 4, 0,
- 256 - xSizeNoTip - 4, ySize);
-
- ItemStack wand = this.inventorySlots.getSlot(ContainerArcaneWorkbench.WAND_SLOT).getStack();
-
- Spell[] spells = WandHelper.getSpells(wand);
-
- int i = 0;
-
- for(Spell spell : spells){
-
- boolean discovered = true;
-
- if(!this.mc.player.capabilities.isCreativeMode && WizardData.get(this.mc.player) != null){
- discovered = WizardData.get(this.mc.player).hasSpellBeenDiscovered(spell);
- }
- // As of Wizardry 1.2, the icons have been split off into their own texture files to allow for add-on
- // mods to add their own.
- Minecraft.getMinecraft().renderEngine
- .bindTexture(discovered ? spell.element.getIcon() : Element.MAGIC.getIcon());
-
- // Renders the little element icon
- WizardryUtilities.drawTexturedRect(guiLeft + xSizeNoTip + 5, guiTop + 34 + 10 * i++, 8, 8);
- }
-
- int x = 0;
- int y = guiTop + 50 + spells.length * 10;
-
- // Look how much shorter this is with the WandHelper class!
- for(Item item : WandHelper.getSpecialUpgrades()){
-
- int level = WandHelper.getUpgradeLevel(wand, item);
-
- if(level > 0){
- ItemStack stack = new ItemStack(item, level);
- GlStateManager.enableDepth();
- this.itemRender.renderItemAndEffectIntoGUI(stack, guiLeft + xSizeNoTip + 6 + x, y);
- this.itemRender.renderItemOverlayIntoGUI(this.fontRenderer, stack, guiLeft + xSizeNoTip + 6 + x, y,
- null);
- x += 18;
- GlStateManager.disableDepth();
- }
- }
- }
-
- Minecraft.getMinecraft().renderEngine.bindTexture(texture);
-
- // Fixes the bug that caused the slot hightlight to render opaque. I don't know why it works, it just works!
- GlStateManager.disableBlend();
- GlStateManager.enableAlpha();
- }
-
- @Override
- protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY){
-
- this.fontRenderer
- .drawString(this.arcaneWorkbenchInventory.hasCustomName() ? this.arcaneWorkbenchInventory.getName()
- : I18n.format(this.arcaneWorkbenchInventory.getName()), 8, 6, 4210752);
- this.fontRenderer.drawString(this.playerInventory.hasCustomName() ? this.playerInventory.getName()
- : I18n.format(this.playerInventory.getName()), 8, this.ySize - 96 + 2, 4210752);
-
- if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.WAND_SLOT).getHasStack() && this.inventorySlots
- .getSlot(ContainerArcaneWorkbench.WAND_SLOT).getStack().getItem() instanceof ItemWand){
-
- ItemStack wand = this.inventorySlots.getSlot(ContainerArcaneWorkbench.WAND_SLOT).getStack();
-
- this.fontRenderer.drawStringWithShadow("\u00A7f" + wand.getDisplayName(), xSizeNoTip + 6, 6, 0);
- this.fontRenderer.drawStringWithShadow(
- "\u00A77" + I18n.format("container." + Wizardry.MODID + ":arcane_workbench.mana") + " "
- + (wand.getMaxDamage() - wand.getItemDamage()) + "/" + wand.getMaxDamage(),
- xSizeNoTip + 6, 20, 0);
-
- Spell[] spells = WandHelper.getSpells(wand);
-
- int y = 34;
-
- for(Spell spell : spells){
-
- boolean discovered = true;
-
- if(!this.mc.player.capabilities.isCreativeMode && WizardData.get(this.mc.player) != null){
- discovered = WizardData.get(this.mc.player).hasSpellBeenDiscovered(spell);
- }
-
- if(discovered){
- this.fontRenderer.drawStringWithShadow(spell.getDisplayNameWithFormatting(), xSizeNoTip + 16, y, 0);
- }else{
- this.mc.standardGalacticFontRenderer.drawStringWithShadow(
- "\u00A79" + SpellGlyphData.getGlyphName(spell, this.mc.world), xSizeNoTip + 16, y, 0);
- }
- y += 10;
- }
-
- if(WandHelper.getTotalUpgrades(wand) > 0){
-
- this.fontRenderer.drawStringWithShadow(
- "\u00A7f" + I18n.format("container." + Wizardry.MODID + ":arcane_workbench.upgrades"), xSizeNoTip + 6, y + 6, 0);
-
- int x = 0;
- y = 50 + spells.length * 10;
- // Wand upgrade tooltips
- for(Item item : WandHelper.getSpecialUpgrades()){
-
- int level = WandHelper.getUpgradeLevel(wand, item);
-
- if(level > 0){
- // The javadoc for isPointInRegion is ambiguous; what it means is that the REGION is
- // relative to the GUI but the POINT isn't.
- if(isPointInRegion(xSizeNoTip + 6 + x, y, 16, 16, mouseX, mouseY)){
- ItemStack stack = new ItemStack(item, level);
- this.renderToolTip(stack, mouseX - guiLeft, mouseY - guiTop);
- }
- x += 18;
- }
- }
- }
- }
- }
-
- @Override
- public void initGui(){
- this.mc.player.openContainer = this.inventorySlots;
- this.guiLeft = (this.width - this.xSize) / 2;
- this.guiTop = (this.height - this.ySize) / 2;
- Keyboard.enableRepeatEvents(true);
- this.buttonList.clear();
- this.buttonList.add(this.applyBtn = new GuiButtonApply(0, this.width / 2 + 48, this.height / 2 + 3));
- }
-
- @Override
- public void onGuiClosed(){
- super.onGuiClosed();
- Keyboard.enableRepeatEvents(false);
- }
-
- @Override
- protected void actionPerformed(GuiButton button){
- if(button.enabled){
- if(button.id == 0){
- // Packet building
- IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.APPLY_BUTTON);
- WizardryPacketHandler.net.sendToServer(msg);
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/GuiButtonApply.java b/src/main/java/electroblob/wizardry/client/GuiButtonApply.java
deleted file mode 100644
index 195327e8..00000000
--- a/src/main/java/electroblob/wizardry/client/GuiButtonApply.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package electroblob.wizardry.client;
-
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiButton;
-import net.minecraft.client.resources.I18n;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-@SideOnly(Side.CLIENT)
-class GuiButtonApply extends GuiButton {
-
- public GuiButtonApply(int id, int x, int y){
- super(id, x, y, 32, 16, I18n.format("container." + Wizardry.MODID + ":arcane_workbench.apply"));
- }
-
- @Override
- public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
-
- // Whether the button is highlighted
- this.hovered = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
-
- int k = 36;
- int l = 220;
- int colour = 14737632;
-
- if(this.enabled){
- if(this.hovered){
- k += this.width * 2;
- colour = 16777120;
- }
- }else{
- k += this.width;
- colour = 10526880;
- }
-
- WizardryUtilities.drawTexturedRect(this.x, this.y, k, l, this.width, this.height, 256, 256);
- this.drawCenteredString(minecraft.fontRenderer, this.displayString, this.x + this.width / 2,
- this.y + (this.height - 8) / 2, colour);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/GuiButtonTurnPage.java b/src/main/java/electroblob/wizardry/client/GuiButtonTurnPage.java
deleted file mode 100644
index 2bdeb44e..00000000
--- a/src/main/java/electroblob/wizardry/client/GuiButtonTurnPage.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package electroblob.wizardry.client;
-
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiButton;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-@SideOnly(Side.CLIENT)
-class GuiButtonTurnPage extends GuiButton {
-
- /** True for pointing right (next page), false for pointing left (previous page). */
- private final boolean nextPage;
-
- private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
-
- public GuiButtonTurnPage(int id, int x, int y, boolean isNextPage){
- super(id, x, y, 23, 13, "");
- this.nextPage = isNextPage;
- }
-
- @Override
- public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
-
- if(this.visible){
-
- boolean flag = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
- GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
- minecraft.getTextureManager().bindTexture(texture);
- int k = 0;
- int l = 192;
-
- if(flag){
- k += 23;
- }
-
- if(!this.nextPage){
- l += 13;
- }
-
- WizardryUtilities.drawTexturedRect(this.x, this.y, k, l, 23, 13, 288, 256);
- }
- }
-}
diff --git a/src/main/java/electroblob/wizardry/client/GuiSpellBook.java b/src/main/java/electroblob/wizardry/client/GuiSpellBook.java
deleted file mode 100644
index fcfb2366..00000000
--- a/src/main/java/electroblob/wizardry/client/GuiSpellBook.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package electroblob.wizardry.client;
-
-import org.lwjgl.input.Keyboard;
-
-import electroblob.wizardry.SpellGlyphData;
-import electroblob.wizardry.WizardData;
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.constants.Tier;
-import electroblob.wizardry.registry.Spells;
-import electroblob.wizardry.spell.Spell;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiScreen;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.util.ResourceLocation;
-
-public class GuiSpellBook extends GuiScreen {
-
- private int xSize, ySize;
- private Spell spell;
-
- private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/spellbook.png");
-
- public GuiSpellBook(Spell spell){
- super();
- xSize = 288;
- ySize = 180;
- this.spell = spell;
- }
-
- /**
- * Draws the screen and all the components in it.
- */
- public void drawScreen(int par1, int par2, float par3){
-
- int xPos = this.width / 2 - xSize / 2;
- int yPos = this.height / 2 - this.ySize / 2;
-
- EntityPlayer player = Minecraft.getMinecraft().player;
-
- boolean discovered = true;
- if(Wizardry.settings.discoveryMode && !player.capabilities.isCreativeMode && WizardData.get(player) != null
- && !WizardData.get(player).hasSpellBeenDiscovered(spell)){
- discovered = false;
- }
-
- // Draws spell illustration on opposite page, underneath the book so it shows through the hole.
- Minecraft.getMinecraft().renderEngine.bindTexture(discovered ? spell.getIcon() : Spells.none.getIcon());
- WizardryUtilities.drawTexturedRect(xPos + 145, yPos + 20, 0, 0, 128, 128, 128, 128);
-
- Minecraft.getMinecraft().renderEngine.bindTexture(texture);
- WizardryUtilities.drawTexturedRect(xPos, yPos, 0, 0, xSize, ySize, xSize, 256);
-
- super.drawScreen(par1, par2, par3);
-
- if(discovered){
- this.fontRenderer.drawString(spell.getDisplayName(), xPos + 17, yPos + 14, 0);
- this.fontRenderer.drawString(spell.type.getDisplayName(), xPos + 17, yPos + 25, 0x777777);
- }else{
- this.mc.standardGalacticFontRenderer.drawString(SpellGlyphData.getGlyphName(spell, player.world), xPos + 17,
- yPos + 14, 0);
- this.mc.standardGalacticFontRenderer.drawString(spell.type.getDisplayName(), xPos + 17, yPos + 25,
- 0x777777);
- }
-
- this.fontRenderer.drawString("-------------------", xPos + 17, yPos + 34, 0);
-
- if(spell.tier == Tier.BASIC){
- // Basic is usually white but this doesn't show up.
- this.fontRenderer.drawString("Tier: \u00A77" + Tier.BASIC.getDisplayName(), xPos + 17, yPos + 44, 0);
- }else{
- this.fontRenderer.drawString("Tier: " + spell.tier.getDisplayNameWithFormatting(), xPos + 17, yPos + 44,
- 0);
- }
-
- String element = "Element: " + spell.element.getFormattingCode() + spell.element.getDisplayName();
- if(!discovered) element = "Element: ?";
- this.fontRenderer.drawString(element, xPos + 17, yPos + 56, 0);
-
- String manaCost = "Mana Cost: " + spell.cost;
- if(spell.isContinuous) manaCost = "Mana Cost: " + spell.cost + "/second";
- if(!discovered) manaCost = "Mana Cost: ?";
- this.fontRenderer.drawString(manaCost, xPos + 17, yPos + 68, 0);
-
- if(discovered){
- this.fontRenderer.drawSplitString(spell.getDescription(), xPos + 17, yPos + 82, 118, 0);
- }else{
- this.mc.standardGalacticFontRenderer.drawSplitString(
- SpellGlyphData.getGlyphDescription(spell, player.world), xPos + 17, yPos + 82, 118, 0);
- }
-
- /* // Word wrapping int charNumber = 0; int lineNumber = 0;
- *
- * while(charNumber < spell.desc.length()){ int lineLength = 0; String line; if(spell.desc.length() - charNumber
- * > 22){ for(int i = charNumber; i < charNumber+23; i++){ if(spell.desc.charAt(i) == ' '){ lineLength = i -
- * charNumber; } } line = spell.desc.substring(charNumber, charNumber + lineLength); }else{ line =
- * spell.desc.substring(charNumber, spell.desc.length()); charNumber = spell.desc.length(); }
- * this.fontRendererObj.drawString("\u00A7o" + line, xPos+17, yPos+82+10*lineNumber, 0);
- * charNumber+=(lineLength+1); lineNumber++; } */
- }
-
- public void initGui(){
- super.initGui();
- Keyboard.enableRepeatEvents(true);
- this.buttonList.clear();
- }
-
- public void onGuiClosed(){
- super.onGuiClosed();
- Keyboard.enableRepeatEvents(false);
- }
-}
diff --git a/src/main/java/electroblob/wizardry/client/GuiSpellDisplay.java b/src/main/java/electroblob/wizardry/client/GuiSpellDisplay.java
deleted file mode 100644
index 0f905882..00000000
--- a/src/main/java/electroblob/wizardry/client/GuiSpellDisplay.java
+++ /dev/null
@@ -1,152 +0,0 @@
-package electroblob.wizardry.client;
-
-import java.util.List;
-
-import electroblob.wizardry.Settings.GuiPosition;
-import electroblob.wizardry.SpellGlyphData;
-import electroblob.wizardry.WizardData;
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.constants.Constants;
-import electroblob.wizardry.item.ItemWand;
-import electroblob.wizardry.registry.Spells;
-import electroblob.wizardry.registry.WizardryItems;
-import electroblob.wizardry.registry.WizardryPotions;
-import electroblob.wizardry.spell.Spell;
-import electroblob.wizardry.util.WandHelper;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.FontRenderer;
-import net.minecraft.client.gui.Gui;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.GlStateManager.DestFactor;
-import net.minecraft.client.renderer.GlStateManager.SourceFactor;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.client.event.RenderGameOverlayEvent;
-import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
-
-public class GuiSpellDisplay extends Gui {
-
- private Minecraft mc;
-
- private static final ResourceLocation hudTexture = new ResourceLocation(Wizardry.MODID, "textures/gui/spell_hud.png");
-
- public GuiSpellDisplay(Minecraft par1Minecraft){
- super();
- this.mc = par1Minecraft;
- }
-
- @SubscribeEvent
- public void draw(RenderGameOverlayEvent event){
-
- EntityPlayer player = this.mc.player;
-
- // If the player has a wand in each hand, only displays for the one in the main hand.
-
- ItemStack wand = player.getHeldItemMainhand();
-
- if(!(wand.getItem() instanceof ItemWand)){
- wand = player.getHeldItemOffhand();
- // If the player isn't holding a wand, then nothing else needs to be done.
- if(!(wand.getItem() instanceof ItemWand)) return;
- }
-
- int width = event.getResolution().getScaledWidth();
- int height = event.getResolution().getScaledHeight();
-
- Spell spell = WandHelper.getCurrentSpell(wand);
- int cooldown = WandHelper.getCurrentCooldown(wand);
-
- float cooldownMultiplier = 1.0f - WandHelper.getUpgradeLevel(wand, WizardryItems.cooldown_upgrade) * Constants.COOLDOWN_REDUCTION_PER_LEVEL;
-
- if(player.isPotionActive(WizardryPotions.font_of_mana)){
- // Dividing by this rather than setting it takes upgrades and font of mana into account simultaneously
- cooldownMultiplier /= 2 + player.getActivePotionEffect(WizardryPotions.font_of_mana).getAmplifier();
- }
-
- // Coordinates of the top left corner of the HUD.
- int left = 0;
- int top = 0;
- boolean mirror = false;
-
- if(Wizardry.settings.spellHUDPosition == GuiPosition.BOTTOM_LEFT){
- left = 0;
- top = height - 36;
- }else if(Wizardry.settings.spellHUDPosition == GuiPosition.TOP_LEFT){
- left = 0;
- top = 0;
- }else if(Wizardry.settings.spellHUDPosition == GuiPosition.TOP_RIGHT){
- left = width - 128;
- top = 0;
- mirror = true;
- }else if(Wizardry.settings.spellHUDPosition == GuiPosition.BOTTOM_RIGHT){
- left = width - 128;
- top = height - 36;
- mirror = true;
- }
-
- boolean discovered = true;
-
- if(!player.capabilities.isCreativeMode && WizardData.get(player) != null){
- discovered = WizardData.get(player).hasSpellBeenDiscovered(spell);
- }
-
- if(event.getType() == RenderGameOverlayEvent.ElementType.TEXT){
-
- // Makes spells greyed out if they are in cooldown or if the player has the arcane jammer effect
- String colour = cooldown > 0 || player.isPotionActive(WizardryPotions.arcane_jammer) ? "\u00A78" : spell.element.getFormattingCode();
- if(!discovered) colour = "\u00A79";
- String spellName = discovered ? spell.getDisplayName() : SpellGlyphData.getGlyphName(spell, player.world);
- FontRenderer font = discovered ? this.mc.fontRenderer : this.mc.standardGalacticFontRenderer;
-
- int maxWidth = 90;
-
- if(font.getStringWidth(spellName) <= maxWidth){
- // Single line is rendered more centrally
- font.drawStringWithShadow(colour + spellName, mirror ? left + 5 : left + 41, top + 13, 0xffffffff);
-
- }else{
-
- int lineNumber = 0;
-
- List lines = font.listFormattedStringToWidth(spellName, maxWidth);
-
- for(Object line : lines){
- if(line instanceof String){
- font.drawStringWithShadow(colour + (String)line, mirror ? left + 5 : left + 41, top + 6 + 11 * lineNumber, 0xffffffff);
- }
- lineNumber++;
- }
- }
-
- }else if(event.getType() == RenderGameOverlayEvent.ElementType.HOTBAR){
-
- GlStateManager.enableBlend();
- GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
- GlStateManager.color(1, 1, 1);
-
- this.mc.renderEngine.bindTexture(hudTexture);
-
- // Background of spell hud
- this.drawTexturedModalRect(left, top, 0, mirror ? 36 : 0, 128, 36);
-
- // Cooldown bar
- if(cooldown > 0){
- this.drawTexturedModalRect(mirror ? left + 5 : left + 41, top + 28, 128, 6, 82, 6);
-
- int l = (int)(((double)(spell.cooldown * cooldownMultiplier - cooldown) / (double)(spell.cooldown * cooldownMultiplier)) * 82);
-
- this.drawTexturedModalRect(mirror ? left + 5 : left + 41, top + 28, 128, 0, l, 6);
- }
-
- // Spell illustration
- this.mc.renderEngine.bindTexture(discovered ? spell.getIcon() : Spells.none.getIcon());
-
- WizardryUtilities.drawTexturedRect(mirror ? left + 94 : left + 2, top + 2, 0, 0, 32, 32, 32, 32);
-
- // Blend needs to be left enabled here because otherwise the hotbar becomes opaque
- }
- }
-
-}
diff --git a/src/main/java/electroblob/wizardry/client/GuiWizardHandbook.java b/src/main/java/electroblob/wizardry/client/GuiWizardHandbook.java
deleted file mode 100644
index 98fa241e..00000000
--- a/src/main/java/electroblob/wizardry/client/GuiWizardHandbook.java
+++ /dev/null
@@ -1,694 +0,0 @@
-package electroblob.wizardry.client;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.apache.commons.lang3.tuple.ImmutablePair;
-import org.apache.commons.lang3.tuple.Pair;
-import org.lwjgl.input.Keyboard;
-import org.lwjgl.opengl.GL11;
-
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.constants.Constants;
-import electroblob.wizardry.constants.Element;
-import electroblob.wizardry.constants.Tier;
-import electroblob.wizardry.registry.WizardryBlocks;
-import electroblob.wizardry.registry.WizardryItems;
-import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiButton;
-import net.minecraft.client.gui.GuiScreen;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.RenderHelper;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.init.Blocks;
-import net.minecraft.init.Items;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.NonNullList;
-import net.minecraft.util.ResourceLocation;
-
-public class GuiWizardHandbook extends GuiScreen {
-
- private int xSize, ySize;
- private int pageNumber = 0;
-
- private static final int PAGE_WIDTH = 120;
- /**
- * The integer colour for black passed into the font renderer methods. This used to be 0 but that's now white for
- * some reason, so I've made a it a constant in case it changes again.
- */
- // I think this is actually ever-so-slightly lighter than pure black, but the difference is unnoticeable.
- private static final int BLACK = 1;
-
- public static final ResourceLocation regularHandbook = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
- public static final ResourceLocation ore = new ResourceLocation(Wizardry.MODID, "textures/gui/ore_picture.png");
- public static final ResourceLocation crystal = new ResourceLocation(Wizardry.MODID, "textures/items/magic_crystal.png");
- public static final ResourceLocation workbenchGui = new ResourceLocation(Wizardry.MODID, "textures/gui/arcane_workbench.png");
- public static final ResourceLocation craftingGrids = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook_recipes.png");
-
- private List> text;
- private List sections;
-
- private static final List>>> RECIPES = new ArrayList<>();
-
- private int guiPage, imagePage;
-
- public GuiWizardHandbook(){
- super();
- xSize = 288;
- ySize = 180;
- }
-
- @Override
- public void drawScreen(int mouseX, int mouseY, float par3){
-
- int xPos = this.width / 2 - xSize / 2;
- int yPos = this.height / 2 - this.ySize / 2;
-
- // Tests for crafting recipes section
- if(pageNumber >= (sections.get(sections.size() - 1).pageNumber - 1) / 2
- && pageNumber < (sections.get(sections.size() - 1).pageNumber - 1) / 2 + 4){
- Minecraft.getMinecraft().renderEngine.bindTexture(craftingGrids);
- }else{
- Minecraft.getMinecraft().renderEngine.bindTexture(regularHandbook);
- }
-
- WizardryUtilities.drawTexturedRect(xPos, yPos, 0, 0, xSize, ySize, xSize, 256);
-
- // Arcane workbench gui picture
- if(pageNumber == (this.guiPage - 1) / 2){
- Minecraft.getMinecraft().renderEngine.bindTexture(workbenchGui);
- this.drawTexturedModalRect(this.guiPage % 2 == 1 ? xPos + 17 : this.width / 2 + 7, yPos + 14, 28, 12, 120,
- 118);
- }
-
- // Magic crystal and crystal ore images
- if(pageNumber == (this.imagePage - 1) / 2){
-
- Minecraft.getMinecraft().renderEngine.bindTexture(ore);
- WizardryUtilities.drawTexturedRect(this.imagePage % 2 == 1 ? xPos + 17 : this.width / 2 + 7, yPos + 80, 0,
- 0, 64, 64, 64, 64);
-
- Minecraft.getMinecraft().renderEngine.bindTexture(crystal);
- drawTexturedStretchedRect(this.imagePage % 2 == 1 ? xPos + 17 + 64 : this.width / 2 + 7 + 62, yPos + 80, 0,
- 0, 64, 64, 1, 1);
-
- }
-
- this.fontRenderer.drawString("" + (pageNumber * 2 + 1), xPos + xSize / 4 - 3, yPos + ySize - 20, 0);
- this.fontRenderer.drawString("" + (pageNumber * 2 + 2), xPos + 3 * xSize / 4 - 5, yPos + ySize - 20, 0);
-
- super.drawScreen(mouseX, mouseY, par3);
-
- int lineNumber = 0;
-
- if(pageNumber == 1){
- for(Section s : sections){
- s.drawContents();
- }
- }else{
- for(Section s : sections){
- s.hideButton();
- }
- }
-
- for(String paragraph : text.get(pageNumber * 2)){
-
- this.fontRenderer.drawSplitString(paragraph, xPos + 17,
- yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
-
- List list = new ArrayList(
- this.fontRenderer.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
-
- lineNumber += list.size();
- }
-
- lineNumber = 0;
-
- // Prevents crash when the last page is blank (and hence is not in the list of pages)
- if(text.size() > pageNumber * 2 + 1){
- for(String paragraph : text.get(pageNumber * 2 + 1)){
-
- // First page is centred
- if(pageNumber == 0){
- int startX = this.width / 2 + 7 + PAGE_WIDTH / 2
- - this.fontRenderer.getStringWidth(paragraph) / 2;
- this.fontRenderer.drawSplitString(paragraph, startX,
- yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
- }else{
- this.fontRenderer.drawSplitString(paragraph, this.width / 2 + 7,
- yPos + 14 + lineNumber * this.fontRenderer.FONT_HEIGHT, PAGE_WIDTH, BLACK);
- }
-
- List list = new ArrayList(
- this.fontRenderer.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
-
- lineNumber += list.size();
- }
- }
-
- // Which page of the recipes this is
- int recipePage = pageNumber - (sections.get(sections.size() - 1).pageNumber - 1) / 2;
-
- if(recipePage >= 0 && recipePage < 4){
- // 4 recipes per page, hence the recipePage*4
- this.renderCraftingRecipe(xPos + 23, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4).getRight(), RECIPES.get(recipePage*4).getLeft());
- this.renderCraftingRecipe(xPos + 23, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+1).getRight(), RECIPES.get(recipePage*4+1).getLeft());
- this.renderCraftingRecipe(xPos + 156, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4+2).getRight(), RECIPES.get(recipePage*4+2).getLeft());
- this.renderCraftingRecipe(xPos + 156, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+3).getRight(), RECIPES.get(recipePage*4+3).getLeft());
-
- // Tooltips are rendered after recipes to prevent tooltips on the left appearing behind items on the right.
- this.renderCraftingTooltips(xPos + 23, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4).getRight(), RECIPES.get(recipePage*4).getLeft());
- this.renderCraftingTooltips(xPos + 23, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+1).getRight(), RECIPES.get(recipePage*4+1).getLeft());
- this.renderCraftingTooltips(xPos + 156, yPos + 39, mouseX, mouseY, RECIPES.get(recipePage*4+2).getRight(), RECIPES.get(recipePage*4+2).getLeft());
- this.renderCraftingTooltips(xPos + 156, yPos + 98, mouseX, mouseY, RECIPES.get(recipePage*4+3).getRight(), RECIPES.get(recipePage*4+3).getLeft());
- }
-
- }
-
- // TODO: In 1.12, this all needs redoing nicely. With the crafting system halfway through changing in 1.11.2, this
- // isn't worth doing until then.
-
- private void renderCraftingRecipe(int xPos, int yPos, int mouseX, int mouseY, NonNullList> craftingGrid,
- ItemStack craftingResult){
-
- GlStateManager.pushMatrix();
- RenderHelper.enableGUIStandardItemLighting();
- GlStateManager.disableLighting();
- GlStateManager.enableRescaleNormal();
- GlStateManager.enableColorMaterial();
- GlStateManager.enableLighting();
- itemRender.zLevel = 100.0F;
-
- for(int i = 0; i < craftingGrid.size(); i++){
- for(int j = 0; j < craftingGrid.get(i).size(); j++){
- ItemStack stack = craftingGrid.get(i).get(j);
- if(!stack.isEmpty()){
- itemRender.renderItemAndEffectIntoGUI(stack, xPos + 18 * i, yPos + 18 * j);
- itemRender.renderItemOverlays(this.fontRenderer, stack, xPos + 18 * i,
- yPos + 18 * j);
- }
- }
- }
-
- if(!craftingResult.isEmpty()){
- itemRender.renderItemAndEffectIntoGUI(craftingResult, xPos + 86, yPos + 18);
- itemRender.renderItemOverlays(this.fontRenderer, craftingResult, xPos + 86, yPos + 18);
- }
-
- GlStateManager.popMatrix();
- GlStateManager.enableLighting();
- GlStateManager.enableDepth();
- RenderHelper.enableStandardItemLighting();
-
- }
-
- private void renderCraftingTooltips(int xPos, int yPos, int mouseX, int mouseY, NonNullList> craftingGrid,
- ItemStack craftingResult){
-
- int guiLeft = this.width / 2 - xSize / 2;
- int guiTop = this.height / 2 - this.ySize / 2;
-
- GlStateManager.pushMatrix();
- RenderHelper.enableGUIStandardItemLighting();
- GlStateManager.disableLighting();
- GlStateManager.enableRescaleNormal();
- GlStateManager.enableColorMaterial();
- itemRender.zLevel = 0.0F;
- GlStateManager.disableLighting();
-
- for(int i = 0; i < craftingGrid.size(); i++){
- for(int j = 0; j < craftingGrid.get(i).size(); j++){
- ItemStack stack = craftingGrid.get(i).get(j);
- if(!stack.isEmpty()
- && isPointInRegion(xPos + 18 * i, yPos + 18 * j, 16, 16, mouseX + guiLeft, mouseY + guiTop)){
- this.renderToolTip(stack, mouseX, mouseY);
- }
- }
- }
-
- if(!craftingResult.isEmpty() && isPointInRegion(xPos + 86, yPos + 18, 16, 16, mouseX + guiLeft, mouseY + guiTop)){
- this.renderToolTip(craftingResult, mouseX, mouseY);
- }
-
- GlStateManager.popMatrix();
- GlStateManager.enableLighting();
- GlStateManager.enableDepth();
- RenderHelper.enableStandardItemLighting();
-
- }
-
- @Override
- public void initGui(){
-
- super.initGui();
- Keyboard.enableRepeatEvents(true);
-
- int nextButtonId = 0;
-
- this.buttonList.clear();
- this.buttonList.add(new GuiButtonTurnPage(nextButtonId++, this.width / 2 + this.xSize / 2 - 22 - 23,
- this.height / 2 + this.ySize / 2 - 10 - 13, true));
- this.buttonList.add(new GuiButtonTurnPage(nextButtonId++, this.width / 2 - this.xSize / 2 + 21,
- this.height / 2 + this.ySize / 2 - 10 - 13, false));
-
- text = new ArrayList>(1);
- sections = new ArrayList(1);
-
- BufferedReader bufferedreader = null;
-
- String textFilepath = Wizardry.MODID + ":texts/handbook_"
- + Minecraft.getMinecraft().getLanguageManager().getCurrentLanguage().getLanguageCode() + ".txt";
-
- try{
-
- bufferedreader = new BufferedReader(new InputStreamReader(
- this.mc.getResourceManager().getResource(new ResourceLocation(textFilepath)).getInputStream(),
- StandardCharsets.UTF_8));
-
- }catch (IOException e){
-
- Wizardry.logger.info(
- "Wizard handbook text file missing for the current language. Using default (English - US) instead.");
-
- textFilepath = Wizardry.MODID + ":texts/handbook_en_us.txt";
-
- try {
-
- bufferedreader = new BufferedReader(new InputStreamReader(
- this.mc.getResourceManager().getResource(new ResourceLocation(textFilepath)).getInputStream(),
- StandardCharsets.UTF_8));
-
- } catch (IOException x){
- Wizardry.logger.error("Couldn't find file: " + Wizardry.MODID + "/assets/texts/handbook_en_us.txt. The file may be"
- + "missing; please try re-downloading and reinstalling Wizardry.", x);
- }
- }
-
- if(bufferedreader != null){
-
- try{
-
- String paragraph = bufferedreader.readLine();
- ArrayList page = new ArrayList(1);
-
- int linesPerPage = 16;
-
- int lineNumber = 0;
-
- while(paragraph != null){
-
- // System.out.println(paragraph);
-
- if(paragraph.contains("PAGEBREAK") || lineNumber >= linesPerPage){
-
- text.add(page);
-
- page = new ArrayList(1);
-
- lineNumber = 0;
-
- if(paragraph.contains("PAGEBREAK")) paragraph = bufferedreader.readLine();
-
- }else if(paragraph.contains("LINEBREAK")){
-
- lineNumber++;
-
- page.add("");
-
- paragraph = bufferedreader.readLine();
-
- }else if(paragraph.contains("SECTION")){
-
- sections.add(
- new Section(paragraph.replace("SECTION ", ""), text.size() + 1, this.width / 2 + 7,
- this.height / 2 - this.ySize / 2 + 14
- + (sections.size() + 2) * this.fontRenderer.FONT_HEIGHT,
- nextButtonId++));
- paragraph = bufferedreader.readLine();
-
- }else if(paragraph.contains("IMAGE")){
-
- if(paragraph.contains("WORKBENCH")){
- this.guiPage = text.size() + 1;
- }else if(paragraph.contains("CRYSTAL")){
- this.imagePage = text.size() + 1;
- }
-
- paragraph = bufferedreader.readLine();
-
- }else{
-
- paragraph = paragraph.replaceAll("NEXT_SPELL_KEY", ClientProxy.NEXT_SPELL.getDisplayName());
- paragraph = paragraph.replaceAll("PREVIOUS_SPELL_KEY", ClientProxy.PREVIOUS_SPELL.getDisplayName());
- paragraph = paragraph.replaceAll("MANA_PER_CRYSTAL_MINUS_30", "" + (Constants.MANA_PER_CRYSTAL - 30));
- paragraph = paragraph.replaceAll("MANA_PER_CRYSTAL", "" + Constants.MANA_PER_CRYSTAL);
- paragraph = paragraph.replaceAll("BASIC_MAX_CHARGE", "" + Tier.BASIC.maxCharge);
- paragraph = paragraph.replaceAll("APPRENTICE_MAX_CHARGE", "" + Tier.APPRENTICE.maxCharge);
- paragraph = paragraph.replaceAll("ADVANCED_MAX_CHARGE", "" + Tier.ADVANCED.maxCharge);
- paragraph = paragraph.replaceAll("MASTER_MAX_CHARGE", "" + Tier.MASTER.maxCharge);
- paragraph = paragraph.replaceAll("BASIC_COLOUR", "\u00A77");
- paragraph = paragraph.replaceAll("APPRENTICE_COLOUR", Tier.APPRENTICE.getFormattingCode());
- paragraph = paragraph.replaceAll("ADVANCED_COLOUR", Tier.ADVANCED.getFormattingCode());
- paragraph = paragraph.replaceAll("MASTER_COLOUR", Tier.MASTER.getFormattingCode());
- paragraph = paragraph.replaceAll("FIRE_COLOUR", Element.FIRE.getFormattingCode());
- paragraph = paragraph.replaceAll("ICE_COLOUR", Element.ICE.getFormattingCode());
- paragraph = paragraph.replaceAll("LIGHTNING_COLOUR", Element.LIGHTNING.getFormattingCode());
- paragraph = paragraph.replaceAll("NECROMANCY_COLOUR", Element.NECROMANCY.getFormattingCode());
- paragraph = paragraph.replaceAll("EARTH_COLOUR", Element.EARTH.getFormattingCode());
- paragraph = paragraph.replaceAll("SORCERY_COLOUR", Element.SORCERY.getFormattingCode());
- paragraph = paragraph.replaceAll("HEALING_COLOUR", Element.HEALING.getFormattingCode());
- paragraph = paragraph.replaceAll("RESET_COLOUR", "\u00A70");
- paragraph = paragraph.replaceAll("MCVERSION", "1.12.2");
- paragraph = paragraph.replaceAll("VERSION", Wizardry.VERSION);
-
- int linesInParagraph = this.fontRenderer
- .listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH).size();
-
- // Ignores empty lines at the top of a page.
- if(paragraph.isEmpty() && lineNumber == 0){
-
- paragraph = bufferedreader.readLine();
-
- // Normal paragraph, all on one page
- }else if(lineNumber + linesInParagraph <= linesPerPage){
-
- page.add(paragraph);
-
- lineNumber += linesInParagraph;
-
- paragraph = bufferedreader.readLine();
-
- // Paragraphs split across two pages (or more?)
- }else{
-
- int linesInFirstPart = linesPerPage - lineNumber;
-
- String paragraphFirstPart = "";
- String paragraphLastPart = "";
-
- int i = 0;
-
- List strings = this.fontRenderer.listFormattedStringToWidth(paragraph,
- GuiWizardHandbook.PAGE_WIDTH);
-
- for(Object s : strings){
- if(i < linesInFirstPart){
- paragraphFirstPart = paragraphFirstPart.concat((String)s + " ");
- }else{
- paragraphLastPart = paragraphLastPart.concat((String)s + " ");
- }
- i++;
- }
-
- // System.out.println("Paragraph crosses page boundary; string split into: \"" +
- // paragraphFirstPart + "\" and \"" + paragraphLastPart + "\"");
-
- page.add(paragraphFirstPart);
-
- lineNumber += linesInFirstPart;
-
- paragraph = paragraphLastPart;
- }
- }
- }
-
- text.add(page);
-
- }catch (IOException e){
- Wizardry.logger.error("Something went wrong reading file: " + textFilepath
- + ". The file may be damaged; please try re-downloading and reinstalling wizardry.", e);
- }
- }
- }
-
- private class Section {
-
- /** The integer text colour used for the section when it is moused over. Currently orange. */
- private static final int HIGHLIGHT_COLOUR = 0xdd4c1d;
-
- String name;
- int pageNumber;
- int x, y;
- int buttonId;
-
- Section(String name, int pageNumber, int x, int y, int id){
- this.name = name;
- this.pageNumber = pageNumber;
- this.x = x;
- this.y = y;
- this.buttonId = id;
- GuiWizardHandbook.this.buttonList.add(new GuiButtonInvisible(id, x, y, GuiWizardHandbook.PAGE_WIDTH,
- GuiWizardHandbook.this.fontRenderer.FONT_HEIGHT));
- }
-
- void hideButton(){
- GuiWizardHandbook.this.buttonList.get(buttonId).visible = false;
- }
-
- void drawContents(){
-
- GuiWizardHandbook.this.buttonList.get(buttonId).visible = true;
-
- GuiWizardHandbook.this.fontRenderer.drawString(name, x, y,
- GuiWizardHandbook.this.buttonList.get(buttonId).isMouseOver() ? HIGHLIGHT_COLOUR : BLACK);
-
- int nameWidth = GuiWizardHandbook.this.fontRenderer.getStringWidth(name);
-
- String dotsAndNumber = " " + this.pageNumber;
-
- while(GuiWizardHandbook.this.fontRenderer.getStringWidth(dotsAndNumber) < GuiWizardHandbook.PAGE_WIDTH
- - nameWidth - 2){
- dotsAndNumber = "." + dotsAndNumber;
- }
-
- GuiWizardHandbook.this.fontRenderer.drawString(dotsAndNumber, x + GuiWizardHandbook.PAGE_WIDTH
- - GuiWizardHandbook.this.fontRenderer.getStringWidth(dotsAndNumber), y, BLACK);
- }
- }
-
- @Override
- public void onGuiClosed(){
- super.onGuiClosed();
- Keyboard.enableRepeatEvents(false);
- }
-
- /**
- * Fired when a control is clicked. This is the equivalent of ActionListener.actionPerformed(ActionEvent e).
- */
- @Override
- protected void actionPerformed(GuiButton par1GuiButton){
-
- if(par1GuiButton.enabled){
- if(par1GuiButton.id == 0){
- if(pageNumber < (text.size() - 1) / 2) pageNumber++;
- }else if(par1GuiButton.id == 1){
- if(pageNumber > 0) pageNumber--;
- }else{
- if(pageNumber == 1) pageNumber = (sections.get(par1GuiButton.id - 2).pageNumber - 1) / 2;
- }
- }
- }
-
- /**
- * Args: left, top, width, height, pointX, pointY. Note: left, top are local to Gui, pointX, pointY are local to
- * screen
- */
- protected boolean isPointInRegion(int par1, int par2, int par3, int par4, int par5, int par6){
- int k1 = this.width / 2 - xSize / 2;
- int l1 = this.height / 2 - this.ySize / 2;
- par5 -= k1;
- par6 -= l1;
- return par5 >= par1 - 1 && par5 < par1 + par3 + 1 && par6 >= par2 - 1 && par6 < par2 + par4 + 1;
- }
-
- /**
- * Draws a textured rectangle, stretching the section of the image to fit the size given.
- *
- * @param x The x position of the rectangle
- * @param y The y position of the rectangle
- * @param u The x position of the top left corner of the section of the image wanted, expressed as a fraction of the
- * image width
- * @param v The y position of the top left corner of the section of the image wanted, expressed as a fraction of the
- * image width
- * @param finalWidth The width as rendered
- * @param finalHeight The height as rendered
- * @param width The width of the section, expressed as a fraction of the image width
- * @param height The height of the section, expressed as a fraction of the image width
- */
- public static void drawTexturedStretchedRect(int x, int y, int u, int v, int finalWidth, int finalHeight, int width,
- int height){
-
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
- buffer.pos((x), y + finalHeight, 0).tex(u, v + height).endVertex();
- buffer.pos(x + finalWidth, y + finalHeight, 0).tex(u + width, v + height).endVertex();
- buffer.pos(x + finalWidth, (y), 0).tex(u + width, v).endVertex();
- buffer.pos((x), (y), 0).tex(u, v).endVertex();
- tessellator.draw();
- }
-
- private static NonNullList> createGrid(){
- NonNullList> grid = NonNullList.withSize(3, NonNullList.create());
- for(int i=0; i<3; i++){
- grid.set(i, NonNullList.withSize(3, ItemStack.EMPTY));
- }
- return grid;
- }
-
- /** Called from init() in the main mod class to initialise the recipes for display in the handbook. */
- public static void initDisplayRecipes(){
-
- NonNullList> craftingGrid;
- ItemStack craftingResult;
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(Items.GOLD_NUGGET));
- craftingGrid.get(1).set(0, new ItemStack(Blocks.CARPET, 1, 10));
- craftingGrid.get(2).set(0, new ItemStack(Items.GOLD_NUGGET));
- craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(1, new ItemStack(Blocks.LAPIS_BLOCK));
- craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(0).set(2, new ItemStack(Blocks.STONE));
- craftingGrid.get(1).set(2, new ItemStack(Blocks.STONE));
- craftingGrid.get(2).set(2, new ItemStack(Blocks.STONE));
- craftingResult = new ItemStack(WizardryBlocks.arcane_workbench);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(1, new ItemStack(Items.STICK));
- craftingGrid.get(0).set(2, new ItemStack(Items.GOLD_NUGGET));
- craftingResult = new ItemStack(WizardryItems.magic_wand);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(1, new ItemStack(Items.BOOK));
- craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingResult = new ItemStack(WizardryItems.spell_book, 1, 1);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(Items.BOOK));
- craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
- craftingResult = new ItemStack(WizardryItems.wizard_handbook);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(WizardryBlocks.crystal_flower));
- craftingResult = new ItemStack(WizardryItems.magic_crystal, 2);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(1, new ItemStack(Items.GLASS_BOTTLE));
- craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_crystal));
- craftingResult = new ItemStack(WizardryItems.mana_flask);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(1).set(0, new ItemStack(Blocks.STONE));
- craftingGrid.get(0).set(1, new ItemStack(Blocks.STONE));
- craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(2, new ItemStack(Blocks.STONE));
- craftingGrid.get(2).set(1, new ItemStack(Blocks.STONE));
- craftingResult = new ItemStack(WizardryBlocks.transportation_stone, 2);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(1).set(0, new ItemStack(Items.STRING));
- craftingGrid.get(0).set(1, new ItemStack(Items.STRING));
- craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_crystal));
- craftingGrid.get(1).set(2, new ItemStack(Items.STRING));
- craftingGrid.get(2).set(1, new ItemStack(Items.STRING));
- craftingResult = new ItemStack(WizardryItems.magic_silk, 2);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingResult = new ItemStack(WizardryItems.wizard_hat);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(1).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(1).set(2, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_silk));
- craftingResult = new ItemStack(WizardryItems.wizard_robe);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(1).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(0).set(2, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(2, new ItemStack(WizardryItems.magic_silk));
- craftingResult = new ItemStack(WizardryItems.wizard_leggings);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(0, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(0).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingGrid.get(2).set(1, new ItemStack(WizardryItems.magic_silk));
- craftingResult = new ItemStack(WizardryItems.wizard_boots);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(Items.PAPER));
- craftingGrid.get(1).set(0, new ItemStack(Items.STRING));
- craftingResult = new ItemStack(WizardryItems.blank_scroll);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(Items.BLAZE_POWDER));
- craftingGrid.get(1).set(0, new ItemStack(Items.BLAZE_POWDER));
- craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
- craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
- craftingResult = new ItemStack(WizardryItems.firebomb, 3);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(Items.SPIDER_EYE));
- craftingGrid.get(1).set(0, new ItemStack(Items.SPIDER_EYE));
- craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
- craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
- craftingResult = new ItemStack(WizardryItems.poison_bomb, 3);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
-
- craftingGrid = createGrid();
- craftingGrid.get(0).set(0, new ItemStack(Items.COAL));
- craftingGrid.get(1).set(0, new ItemStack(Items.COAL));
- craftingGrid.get(0).set(1, new ItemStack(Items.GLASS_BOTTLE));
- craftingGrid.get(1).set(1, new ItemStack(Items.GUNPOWDER));
- craftingResult = new ItemStack(WizardryItems.smoke_bomb, 3);
- RECIPES.add(ImmutablePair.of(craftingResult, craftingGrid));
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/MixedFontRenderer.java b/src/main/java/electroblob/wizardry/client/MixedFontRenderer.java
index ee098490..ff637a1c 100644
--- a/src/main/java/electroblob/wizardry/client/MixedFontRenderer.java
+++ b/src/main/java/electroblob/wizardry/client/MixedFontRenderer.java
@@ -5,15 +5,13 @@ import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.renderer.texture.TextureManager;
import net.minecraft.client.settings.GameSettings;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
/**
* Font renderer that renders parts of strings surrounded by '#' (without quotes) in the SGA instead of normal text.
*
* @since Wizardry 1.1
*/
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class MixedFontRenderer extends FontRenderer {
public MixedFontRenderer(GameSettings p_i1035_1_, ResourceLocation p_i1035_2_, TextureManager p_i1035_3_,
diff --git a/src/main/java/electroblob/wizardry/client/MovingSoundEntity.java b/src/main/java/electroblob/wizardry/client/MovingSoundEntity.java
deleted file mode 100644
index 0fdd1165..00000000
--- a/src/main/java/electroblob/wizardry/client/MovingSoundEntity.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package electroblob.wizardry.client;
-
-import net.minecraft.client.audio.MovingSound;
-import net.minecraft.entity.Entity;
-import net.minecraft.util.SoundCategory;
-import net.minecraft.util.SoundEvent;
-import net.minecraft.util.math.MathHelper;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-// Copied from MovingSoundMinecart; if it ever breaks between updates take a look at that.
-@SideOnly(Side.CLIENT)
-public class MovingSoundEntity extends MovingSound {
- private final Entity source;
- private float distance = 0.0F;
-
- public MovingSoundEntity(Entity entity, SoundEvent sound, float volume, float pitch, boolean repeat){
- // Uses BLOCKS because that's the closest thing to inanimate entities. Could use NEUTRAL like
- // MovingSoundMinecart.
- super(sound, SoundCategory.BLOCKS);
- this.source = entity;
- this.repeat = repeat;
- this.volume = volume;
- this.pitch = pitch;
- this.repeatDelay = 0;
- }
-
- /**
- * Updates the JList with a new model.
- */
- @Override
- public void update(){
- if(this.source.isDead && repeat){
- this.donePlaying = true;
- }else{
- this.xPosF = (float)this.source.posX;
- this.yPosF = (float)this.source.posY;
- this.zPosF = (float)this.source.posZ;
- float f = MathHelper.sqrt(this.source.motionX * this.source.motionX
- + this.source.motionY * this.source.motionY + this.source.motionZ * this.source.motionZ);
-
- // Is this something to do with the Doppler effect?
- if((double)f >= 0.01D){
- this.distance = MathHelper.clamp(this.distance + 0.0025F, 0.0F, 1.0F);
- this.volume = 0.0F + MathHelper.clamp(f, 0.0F, 0.5F) * 0.7F;
- }else{
- // this.pitch = 0.0F;
- // this.volume = 0.0F;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java b/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java
index 52d561bb..a024f95e 100644
--- a/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java
+++ b/src/main/java/electroblob/wizardry/client/WizardryClientEventHandler.java
@@ -1,108 +1,179 @@
package electroblob.wizardry.client;
-import org.lwjgl.opengl.GL11;
-
-import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.block.BlockMagicLight;
import electroblob.wizardry.constants.Constants;
+import electroblob.wizardry.data.DispenserCastingData;
+import electroblob.wizardry.data.SpellEmitterData;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.item.ItemArtefact;
import electroblob.wizardry.item.ItemSpectralBow;
-import electroblob.wizardry.item.ItemWand;
-import electroblob.wizardry.packet.PacketControlInput;
-import electroblob.wizardry.packet.WizardryPacketHandler;
+import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.registry.WizardryItems;
import electroblob.wizardry.registry.WizardryPotions;
-import electroblob.wizardry.spell.Flight;
-import electroblob.wizardry.spell.ShadowWard;
-import electroblob.wizardry.spell.Shield;
-import electroblob.wizardry.tileentity.ContainerArcaneWorkbench;
-import electroblob.wizardry.util.WandHelper;
+import electroblob.wizardry.spell.*;
+import electroblob.wizardry.util.RayTracer;
import electroblob.wizardry.util.WizardryUtilities;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiMerchant;
-import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.OpenGlHelper;
-import net.minecraft.client.renderer.RenderHelper;
-import net.minecraft.client.renderer.RenderItem;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.client.settings.KeyBinding;
+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.item.EntityArmorStand;
-import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.tileentity.TileEntityDispenser;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.RayTraceResult;
+import net.minecraft.util.math.Vec3d;
import net.minecraft.village.MerchantRecipe;
-import net.minecraftforge.client.event.FOVUpdateEvent;
-import net.minecraftforge.client.event.GuiContainerEvent;
-import net.minecraftforge.client.event.MouseEvent;
-import net.minecraftforge.client.event.RenderGameOverlayEvent;
-import net.minecraftforge.client.event.RenderLivingEvent;
-import net.minecraftforge.client.event.RenderPlayerEvent;
-import net.minecraftforge.client.event.RenderWorldLastEvent;
-import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.*;
import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
-import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import org.lwjgl.opengl.GL11;
+
+import java.lang.reflect.Method;
/**
- * Event handler responsible for all client-side only events, mostly rendering.
+ * Event handler responsible for client-side only events, mostly rendering.
*
* @author Electroblob
* @since Wizardry 1.0
*/
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
@Mod.EventBusSubscriber(Side.CLIENT)
public final class WizardryClientEventHandler {
- private static final ResourceLocation shieldTexture = new ResourceLocation(Wizardry.MODID, "textures/entity/shield.png");
- private static final ResourceLocation wingTexture = new ResourceLocation(Wizardry.MODID, "textures/entity/wing.png");
- private static final ResourceLocation shadowWardTexture = new ResourceLocation(Wizardry.MODID, "textures/entity/shadow_ward.png");
private static final ResourceLocation sixthSenseTexture = new ResourceLocation(Wizardry.MODID, "textures/entity/sixth_sense.png");
private static final ResourceLocation sixthSenseOverlayTexture = new ResourceLocation(Wizardry.MODID, "textures/gui/sixth_sense_overlay.png");
private static final ResourceLocation frostOverlayTexture = new ResourceLocation(Wizardry.MODID, "textures/gui/frost_overlay.png");
+ private static final ResourceLocation blinkOverlayTexture = new ResourceLocation(Wizardry.MODID, "textures/gui/blink_overlay.png");
private static final ResourceLocation pointerTexture = new ResourceLocation(Wizardry.MODID, "textures/entity/pointer.png");
private static final ResourceLocation targetPointerTexture = new ResourceLocation(Wizardry.MODID, "textures/entity/target_pointer.png");
+ /** The remaining time for which the blink screen overlay effect will be displayed in first-person. Since this is
+ * only for the first-person player (the instance of which is itself stored in a static variable), this can simply
+ * be stored statically here, rather than needing to be in {@code WizardData}. */
+ private static int blinkEffectTimer;
+ /** The number of ticks the blink effect lasts for. */
+ private static final int BLINK_EFFECT_DURATION = 8;
+
+ private static final Method unpressKey;
+
+ static {
+ unpressKey = ObfuscationReflectionHelper.findMethod(KeyBinding.class, "func_74505_d", void.class);
+ }
+
+ /** Starts the first person blink overlay effect. */
+ public static void playBlinkEffect(){
+ blinkEffectTimer = BLINK_EFFECT_DURATION;
+ }
+
@SubscribeEvent
- public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
- event.getMap().registerSprite(ContainerArcaneWorkbench.EMPTY_SLOT_CRYSTAL);
- event.getMap().registerSprite(ContainerArcaneWorkbench.EMPTY_SLOT_UPGRADE);
+ public static void onPlayerTickEvent(TickEvent.PlayerTickEvent event){
+
+ if(event.player == Minecraft.getMinecraft().player){
+
+ if(blinkEffectTimer > 0) blinkEffectTimer--;
+
+ // Only seems to work here...
+// EntityLiving victim = Possession.getPossessee(Minecraft.getMinecraft().player);
+// if(victim != null && victim.getHeldItemMainhand().isEmpty()){
+// Minecraft.getMinecraft().player.setHeldItem(EnumHand.MAIN_HAND, ItemStack.EMPTY);
+// }
+
+ // Reset shaders if their respective potions aren't active
+ // This is a player so the potion effects are synced by vanilla
+ if(Minecraft.getMinecraft().entityRenderer.getShaderGroup() != null){
+
+ String activeShader = Minecraft.getMinecraft().entityRenderer.getShaderGroup().getShaderGroupName();
+
+ if((activeShader.equals(SlowTime.SHADER.toString()) && !Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.slow_time))
+ || (activeShader.equals(SixthSense.SHADER.toString()) && !Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.sixth_sense))
+ || (activeShader.equals(Transience.SHADER.toString()) && !Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.transience))){
+
+ if(activeShader.equals(SixthSense.SHADER.toString())
+ || activeShader.equals(Transience.SHADER.toString())) playBlinkEffect();
+
+ Minecraft.getMinecraft().entityRenderer.stopUseShader();
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public static void onRenderHandEvent(RenderHandEvent event){
+
+ EntityLiving victim = Possession.getPossessee(Minecraft.getMinecraft().player);
+
+ if(victim != null){
+
+ victim.rotationYawHead = Minecraft.getMinecraft().player.rotationYaw;
+
+ if(Minecraft.getMinecraft().player.getHeldItemMainhand().isEmpty()){
+ event.setCanceled(true);
+ }
+ }
}
- // Shift-scrolling to change spells
+ // This event is called every tick, not just when a movement key is pressed
+ @SubscribeEvent
+ public static void onInputUpdateEvent(InputUpdateEvent event){
+ // Prevents the player moving when paralysed
+ if(event.getEntityPlayer().isPotionActive(WizardryPotions.paralysis)){
+ event.getMovementInput().moveForward = 0;
+ event.getMovementInput().moveStrafe = 0;
+ event.getMovementInput().jump = false;
+ event.getMovementInput().sneak = false;
+ }
+ }
+
+ @SubscribeEvent
+ public static void onClientTickEvent(TickEvent.ClientTickEvent event){
+
+ if(event.phase == TickEvent.Phase.END && !net.minecraft.client.Minecraft.getMinecraft().isGamePaused()){
+
+ World world = net.minecraft.client.Minecraft.getMinecraft().world;
+
+ if(world == null) return;
+
+ for(TileEntity tileentity : world.loadedTileEntityList){
+ if(tileentity instanceof TileEntityDispenser){
+ if(DispenserCastingData.get((TileEntityDispenser)tileentity) != null){
+ DispenserCastingData.get((TileEntityDispenser)tileentity).update();
+ }
+ }
+ }
+
+ SpellEmitterData.update(world);
+ }
+ }
+
@SubscribeEvent
public static void onMouseEvent(MouseEvent event){
-
- EntityPlayer player = Minecraft.getMinecraft().player;
- ItemStack wand = player.getHeldItemMainhand();
-
- if(!(wand.getItem() instanceof ItemWand)){
- wand = player.getHeldItemOffhand();
- // If the player isn't holding a wand, then nothing else needs to be done.
- if(!(wand.getItem() instanceof ItemWand)) return;
- }
-
- if(Minecraft.getMinecraft().inGameHasFocus && !wand.isEmpty() && event.getDwheel() != 0 && player.isSneaking()
- && Wizardry.settings.enableShiftScrolling){
-
+
+ // Prevents the player looking around when paralysed
+ if(Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.paralysis)
+ && Minecraft.getMinecraft().inGameHasFocus){
event.setCanceled(true);
-
- if(event.getDwheel() > 0){
- // Packet building
- IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.PREVIOUS_SPELL_KEY);
- WizardryPacketHandler.net.sendToServer(msg);
-
- }else if(event.getDwheel() < 0){
- // Packet building
- IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.NEXT_SPELL_KEY);
- WizardryPacketHandler.net.sendToServer(msg);
- }
+ Minecraft.getMinecraft().player.prevRotationYaw = 0;
+ Minecraft.getMinecraft().player.prevRotationPitch = 0;
+ Minecraft.getMinecraft().player.rotationYaw = 0;
+ Minecraft.getMinecraft().player.rotationPitch = 0;
}
}
@@ -124,12 +195,32 @@ public final class WizardryClientEventHandler {
event.setNewfov(event.getFov() * 1.0F - maxUseSeconds * 0.15F);
}
+
+ if(blinkEffectTimer > 0){
+ float f = ((float)Math.max(blinkEffectTimer - 2, 0))/BLINK_EFFECT_DURATION;
+ event.setNewfov(event.getFov() + f * f * 0.7f);
+ }
}
-
+
+ @SubscribeEvent
+ public static void onDrawBlockHighlightEvent(DrawBlockHighlightEvent event){
+ // Hide the block outline for magic light blocks unless the player can dispel them
+ if(event.getTarget().typeOfHit == RayTraceResult.Type.BLOCK
+ && event.getPlayer().world.getBlockState(event.getTarget().getBlockPos()).getBlock() instanceof BlockMagicLight){
+
+ if((!(event.getPlayer().getHeldItemMainhand().getItem() instanceof ISpellCastingItem)
+ && !(event.getPlayer().getHeldItemOffhand().getItem() instanceof ISpellCastingItem))
+ || !ItemArtefact.isArtefactActive(event.getPlayer(), WizardryItems.charm_light)){
+
+ event.setCanceled(true);
+ }
+ }
+ }
+
// Brute-force fix for crystals not showing up when a wizard is given a spell book in the trade GUI.
@SubscribeEvent
public static void onGuiDrawForegroundEvent(GuiContainerEvent.DrawForeground event){
-
+
if(event.getGuiContainer() instanceof GuiMerchant){
GuiMerchant gui = (GuiMerchant)event.getGuiContainer();
@@ -142,120 +233,79 @@ public final class WizardryClientEventHandler {
for(MerchantRecipe trade : gui.getMerchant().getRecipes(Minecraft.getMinecraft().player)){
if(trade.getItemToBuy().getItem() == WizardryItems.spell_book && trade.getSecondItemToBuy().isEmpty()){
Slot slot = gui.inventorySlots.getSlot(2);
- // Uses reflection to draw the itemstack
// It still doesn't look quite right because the slot highlight is behind the item, but it'll do
// until/unless I find a better solution.
- renderItemAndTooltip(gui, trade.getItemToSell(), slot.xPos, slot.yPos, event.getMouseX(), event.getMouseY(),
+ DrawingUtils.drawItemAndTooltip(gui, trade.getItemToSell(), slot.xPos, slot.yPos, event.getMouseX(), event.getMouseY(),
gui.getSlotUnderMouse() == slot);
}
}
}
}
}
-
- private static void renderItemAndTooltip(GuiContainer gui, ItemStack stack, int x, int y, int mouseX, int mouseY, boolean tooltip){
-
- RenderItem renderItem = Minecraft.getMinecraft().getRenderItem();
- GlStateManager.pushMatrix();
- RenderHelper.enableGUIStandardItemLighting();
- GlStateManager.disableLighting();
- GlStateManager.enableRescaleNormal();
- GlStateManager.enableColorMaterial();
- GlStateManager.enableLighting();
- renderItem.zLevel = 100.0F;
-
- if(!stack.isEmpty()){
- renderItem.renderItemAndEffectIntoGUI(stack, x, y);
- renderItem.renderItemOverlays(Minecraft.getMinecraft().fontRenderer, stack, x, y);
-
- if(tooltip){
- gui.drawHoveringText(gui.getItemToolTip(stack), mouseX + gui.getXSize()/2 - gui.width/2,
- mouseY + gui.getYSize()/2 - gui.height/2);
- }
- }
-
- GlStateManager.popMatrix();
- GlStateManager.enableLighting();
- GlStateManager.enableDepth();
- RenderHelper.enableStandardItemLighting();
- }
-
- // Third person
- @SubscribeEvent
- public static void onRenderPlayerEvent(RenderPlayerEvent.Post event){
- renderShieldIfActive(event.getEntityPlayer());
- renderWingsIfActive(event.getEntityPlayer(), event.getPartialRenderTick());
- renderShadowWardIfActive(event.getEntityPlayer());
- }
-
- // First person
- @SubscribeEvent
- public static void onRenderWorldLastEvent(RenderWorldLastEvent event){
- // Now only fires in first person.
- if(Minecraft.getMinecraft().gameSettings.thirdPersonView == 0){
- renderShieldFirstPerson(Minecraft.getMinecraft().player);
- renderShadowWardFirstPerson(Minecraft.getMinecraft().player);
- }
- }
@SubscribeEvent
public static void onRenderLivingEvent(RenderLivingEvent.Post event){
Minecraft mc = Minecraft.getMinecraft();
- WizardData properties = WizardData.get(mc.player);
- RayTraceResult rayTrace = WizardryUtilities.standardEntityRayTrace(mc.world, mc.player, 16);
+ WizardData data = WizardData.get(mc.player);
RenderManager renderManager = event.getRenderer().getRenderManager();
ItemStack wand = mc.player.getHeldItemMainhand();
- if(!(wand.getItem() instanceof ItemWand)){
+ if(!(wand.getItem() instanceof ISpellCastingItem)){
wand = mc.player.getHeldItemOffhand();
}
// Target selection pointer
- if(mc.player.isSneaking() && wand.getItem() instanceof ItemWand && rayTrace != null && !(event.getEntity() instanceof EntityArmorStand)
- && rayTrace.entityHit == event.getEntity() && properties != null && properties.selectedMinion != null){
+ if(mc.player.isSneaking() && wand.getItem() instanceof ISpellCastingItem && WizardryUtilities.isLiving(event.getEntity())
+ && data != null && data.selectedMinion != null){
+
+ // -> Moved this in here so it isn't called every tick
+ RayTraceResult rayTrace = RayTracer.standardEntityRayTrace(mc.world, mc.player, 16, false);
+
+ if(rayTrace != null && rayTrace.entityHit == event.getEntity()){
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
- GlStateManager.pushMatrix();
+ GlStateManager.pushMatrix();
- GlStateManager.disableCull();
- GlStateManager.disableLighting();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
- // Disabling depth test allows it to be seen through everything.
- GlStateManager.disableDepth();
- GlStateManager.color(1, 1, 1, 1);
+ GlStateManager.disableCull();
+ GlStateManager.disableLighting();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+ // Disabling depth test allows it to be seen through everything.
+ GlStateManager.disableDepth();
+ GlStateManager.color(1, 1, 1, 1);
- GlStateManager.translate(event.getX(), event.getY() + event.getEntity().height + 0.5, event.getZ());
+ GlStateManager.translate(event.getX(), event.getY() + event.getEntity().height + 0.5, event.getZ());
- // This counteracts the reverse rotation behaviour when in front f5 view.
- // Fun fact: this is a bug with vanilla too! Look at a snowball in front f5 view, for example.
- float yaw = mc.gameSettings.thirdPersonView == 2 ? renderManager.playerViewX : -renderManager.playerViewX;
- GlStateManager.rotate(180 - renderManager.playerViewY, 0.0F, 1.0F, 0.0F);
- GlStateManager.rotate(yaw, 1.0F, 0.0F, 0.0F);
+ // This counteracts the reverse rotation behaviour when in front f5 view.
+ // Fun fact: this is a bug with vanilla too! Look at a snowball in front f5 view, for example.
+ float yaw = mc.gameSettings.thirdPersonView == 2 ? renderManager.playerViewX : -renderManager.playerViewX;
+ GlStateManager.rotate(180 - renderManager.playerViewY, 0.0F, 1.0F, 0.0F);
+ GlStateManager.rotate(yaw, 1.0F, 0.0F, 0.0F);
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
- mc.renderEngine.bindTexture(targetPointerTexture);
+ mc.renderEngine.bindTexture(targetPointerTexture);
- buffer.pos(-0.2, 0.24, 0).tex(0, 0).endVertex();
- buffer.pos(0.2, 0.24, 0).tex(9f / 16f, 0).endVertex();
- buffer.pos(0.2, -0.24, 0).tex(9f / 16f, 11f / 16f).endVertex();
- buffer.pos(-0.2, -0.24, 0).tex(0, 11f / 16f).endVertex();
+ buffer.pos(-0.2, 0.24, 0).tex(0, 0).endVertex();
+ buffer.pos(0.2, 0.24, 0).tex(9f / 16f, 0).endVertex();
+ buffer.pos(0.2, -0.24, 0).tex(9f / 16f, 11f / 16f).endVertex();
+ buffer.pos(-0.2, -0.24, 0).tex(0, 11f / 16f).endVertex();
- tessellator.draw();
+ tessellator.draw();
- GlStateManager.enableCull();
- GlStateManager.enableLighting();
- GlStateManager.enableDepth();
+ GlStateManager.enableCull();
+ GlStateManager.enableLighting();
+ GlStateManager.enableDepth();
- GlStateManager.popMatrix();
+ GlStateManager.popMatrix();
+ }
}
// Summoned creature selection pointer
- if(properties != null && properties.selectedMinion != null && properties.selectedMinion.get() == event.getEntity()){
+ if(data != null && data.selectedMinion != null && data.selectedMinion.get() == event.getEntity()){
Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
@@ -296,8 +346,9 @@ public final class WizardryClientEventHandler {
}
// Sixth sense
- if(mc.player.isPotionActive(WizardryPotions.sixth_sense) && !(event.getEntity() instanceof EntityArmorStand) && event.getEntity() != mc.player
- && mc.player.getActivePotionEffect(WizardryPotions.sixth_sense) != null && event.getEntity().getDistance(mc.player) < 20
+ if(mc.player.isPotionActive(WizardryPotions.sixth_sense) && !(event.getEntity() instanceof EntityArmorStand)
+ && event.getEntity() != mc.player && mc.player.getActivePotionEffect(WizardryPotions.sixth_sense) != null
+ && event.getEntity().getDistance(mc.player) < Spells.sixth_sense.getProperty(Spell.EFFECT_RADIUS).floatValue()
* (1 + mc.player.getActivePotionEffect(WizardryPotions.sixth_sense).getAmplifier() * Constants.RANGE_INCREASE_PER_LEVEL)){
Tessellator tessellator = Tessellator.getInstance();
@@ -344,384 +395,72 @@ public final class WizardryClientEventHandler {
@SubscribeEvent
public static void onRenderGameOverlayEvent(RenderGameOverlayEvent.Post event){
- if(event.getType() == RenderGameOverlayEvent.ElementType.HELMET
- && Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.sixth_sense)){
-
- GlStateManager.pushMatrix();
-
- GlStateManager.disableDepth();
- GlStateManager.depthMask(false);
- OpenGlHelper.glBlendFunc(770, 771, 1, 0);
- GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
- GlStateManager.disableAlpha();
- Minecraft.getMinecraft().renderEngine.bindTexture(sixthSenseOverlayTexture);
-
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
- buffer.pos(0.0D, (double)event.getResolution().getScaledHeight(), -90.0D).tex(0.0D, 1.0D).endVertex();
- buffer.pos((double)event.getResolution().getScaledWidth(), (double)event.getResolution().getScaledHeight(), -90.0D).tex(1.0D, 1.0D)
- .endVertex();
- buffer.pos((double)event.getResolution().getScaledWidth(), 0.0D, -90.0D).tex(1.0D, 0.0D).endVertex();
- buffer.pos(0.0D, 0.0D, -90.0D).tex(0.0D, 0.0D).endVertex();
- tessellator.draw();
-
- GlStateManager.depthMask(true);
- GlStateManager.enableDepth();
- GlStateManager.enableAlpha();
- GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
-
- GlStateManager.popMatrix();
- }
-
- if(event.getType() == RenderGameOverlayEvent.ElementType.HELMET && Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.frost)){
-
- GlStateManager.pushMatrix();
-
- GlStateManager.disableDepth();
- GlStateManager.depthMask(false);
- OpenGlHelper.glBlendFunc(770, 771, 1, 0);
- GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
- GlStateManager.disableAlpha();
- Minecraft.getMinecraft().renderEngine.bindTexture(frostOverlayTexture);
-
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
- buffer.pos(0.0D, (double)event.getResolution().getScaledHeight(), -90.0D).tex(0.0D, 1.0D).endVertex();
- buffer.pos((double)event.getResolution().getScaledWidth(), (double)event.getResolution().getScaledHeight(), -90.0D).tex(1.0D, 1.0D)
- .endVertex();
- buffer.pos((double)event.getResolution().getScaledWidth(), 0.0D, -90.0D).tex(1.0D, 0.0D).endVertex();
- buffer.pos(0.0D, 0.0D, -90.0D).tex(0.0D, 0.0D).endVertex();
-
- tessellator.draw();
- GlStateManager.depthMask(true);
- GlStateManager.enableDepth();
- GlStateManager.enableAlpha();
- GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
-
- GlStateManager.popMatrix();
+ if(event.getType() == RenderGameOverlayEvent.ElementType.HELMET){
+
+ if(Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.sixth_sense)){
+
+ OpenGlHelper.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
+ GlStateManager.color(1, 1, 1, 1);
+ GlStateManager.disableAlpha();
+
+ renderScreenOverlay(event, sixthSenseOverlayTexture);
+
+ GlStateManager.enableAlpha();
+ GlStateManager.color(1, 1, 1, 1);
+ }
+
+ if(Minecraft.getMinecraft().player.isPotionActive(WizardryPotions.frost)){
+
+ OpenGlHelper.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ZERO);
+ GlStateManager.color(1, 1, 1, 1);
+ GlStateManager.disableAlpha();
+
+ renderScreenOverlay(event, frostOverlayTexture);
+
+ GlStateManager.enableAlpha();
+ GlStateManager.color(1, 1, 1, 1);
+ }
+
+ if(blinkEffectTimer > 0){
+
+ float alpha = ((float)blinkEffectTimer)/BLINK_EFFECT_DURATION;
+
+ OpenGlHelper.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE, GL11.GL_ZERO);
+ GlStateManager.color(1, 1, 1, alpha);
+ GlStateManager.disableAlpha();
+
+ renderScreenOverlay(event, blinkOverlayTexture);
+
+ GlStateManager.enableAlpha();
+ GlStateManager.color(1, 1, 1, 1);
+ }
}
}
+
+ private static void renderScreenOverlay(RenderGameOverlayEvent.Post event, ResourceLocation texture){
+
+ GlStateManager.pushMatrix();
- // FIXME: Something in here is making the first person shadow ward rather translucent.
- private static void renderShadowWardFirstPerson(EntityPlayer entityplayer){
- ItemStack wand = entityplayer.getActiveItemStack();
- if(WizardData.get(entityplayer) != null && WizardData.get(entityplayer).currentlyCasting() instanceof ShadowWard
- || (entityplayer.isHandActive() && wand.getItemDamage() < wand.getMaxDamage() && wand.getItem() instanceof ItemWand
- && WandHelper.getCurrentSpell(wand) instanceof ShadowWard)){
-
- GlStateManager.pushMatrix();
-
- GlStateManager.enableBlend();
- GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
- GlStateManager.shadeModel(GL11.GL_SMOOTH);
- GlStateManager.disableLighting();
- GlStateManager.disableAlpha();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
-
- GlStateManager.translate(0, 1.2, 0);
- GlStateManager.rotate(-entityplayer.rotationYaw, 0, 1, 0);
- GlStateManager.rotate(entityplayer.rotationPitch, 1, 0, 0);
-
- Minecraft.getMinecraft().renderEngine.bindTexture(shadowWardTexture);
-
- GlStateManager.pushMatrix();
-
- GlStateManager.translate(0, 0, 1.2);
- GlStateManager.rotate(entityplayer.world.getWorldTime() * -2, 0, 0, 1);
- GlStateManager.scale(1.1, 1.1, 1.1);
-
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
- buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
- buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
- buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
-
- tessellator.draw();
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
- buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
- buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
- buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
-
- tessellator.draw();
-
- GlStateManager.popMatrix();
-
- GlStateManager.shadeModel(GL11.GL_FLAT);
- GlStateManager.enableLighting();
- GlStateManager.disableBlend();
-
- GlStateManager.popMatrix();
-
- }
- }
-
- private static void renderShadowWardIfActive(EntityPlayer entityplayer){
- ItemStack wand = entityplayer.getActiveItemStack();
- if(WizardData.get(entityplayer).currentlyCasting() instanceof ShadowWard
- || (entityplayer.isHandActive() && wand.getItemDamage() < wand.getMaxDamage() && wand.getItem() instanceof ItemWand
- && WandHelper.getCurrentSpell(wand) instanceof ShadowWard)){
-
- GlStateManager.pushMatrix();
-
- GlStateManager.enableBlend();
- GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
- GlStateManager.disableLighting();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
-
- GlStateManager.rotate(180, 0, 1, 0);
- GlStateManager.rotate(-entityplayer.renderYawOffset, 0, 1, 0);
-
- Minecraft.getMinecraft().renderEngine.bindTexture(shadowWardTexture);
-
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
-
- GlStateManager.translate(0, 1.2, 0);
- GlStateManager.rotate(entityplayer.world.getWorldTime() * -2, 0, 0, 1);
- GlStateManager.scale(1.1, 1.1, 1.1);
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
- buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
- buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
- buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
-
- tessellator.draw();
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
- buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
- buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
- buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
-
- tessellator.draw();
-
- GlStateManager.enableLighting();
- GlStateManager.disableBlend();
-
- GlStateManager.popMatrix();
-
- }
- }
-
- private static void renderWingsIfActive(EntityPlayer entityplayer, float partialTickTime){
- ItemStack wand = entityplayer.getActiveItemStack();
- if(WizardData.get(entityplayer).currentlyCasting() instanceof Flight
- || (entityplayer.isHandActive() && wand.getItemDamage() < wand.getMaxDamage() && wand.getItem() instanceof ItemWand
- && WandHelper.getCurrentSpell(wand) instanceof Flight)){
-
- GlStateManager.pushMatrix();
-
- GlStateManager.enableBlend();
- GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
- GlStateManager.disableLighting();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
-
- // GlStateManager.rotate(-entityplayer.rotationYawHead, 0, 1, 0);
- GlStateManager.rotate(-entityplayer.renderYawOffset, 0, 1, 0);
- // GlStateManager.rotate(180, 1, 0, 0);
-
- Minecraft.getMinecraft().renderEngine.bindTexture(wingTexture);
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
-
- GlStateManager.pushMatrix();
-
- GlStateManager.translate(0.1, 0.4, -0.15);
- GlStateManager.rotate(20 + 20 * (float)Math.sin(entityplayer.world.getWorldTime() * 0.3), 0, 1, 0);
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(0, 2, 0).tex(0, 0).endVertex();
- buffer.pos(2, 2, 0).tex(1, 0).endVertex();
- buffer.pos(2, 0, 0).tex(1, 1).endVertex();
- buffer.pos(0, 0, 0).tex(0, 1).endVertex();
-
- tessellator.draw();
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(0, 2, 0).tex(0, 0).endVertex();
- buffer.pos(0, 0, 0).tex(0, 1).endVertex();
- buffer.pos(2, 0, 0).tex(1, 1).endVertex();
- buffer.pos(2, 2, 0).tex(1, 0).endVertex();
-
- tessellator.draw();
-
- GlStateManager.popMatrix();
-
- GlStateManager.pushMatrix();
-
- GlStateManager.translate(-0.1, 0.4, -0.15);
- GlStateManager.rotate(-200 - 20 * (float)Math.sin(entityplayer.world.getWorldTime() * 0.3), 0, 1, 0);
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(0, 2, 0).tex(0, 0).endVertex();
- buffer.pos(2, 2, 0).tex(1, 0).endVertex();
- buffer.pos(2, 0, 0).tex(1, 1).endVertex();
- buffer.pos(0, 0, 0).tex(0, 1).endVertex();
-
- tessellator.draw();
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
-
- buffer.pos(0, 2, 0).tex(0, 0).endVertex();
- buffer.pos(0, 0, 0).tex(0, 1).endVertex();
- buffer.pos(2, 0, 0).tex(1, 1).endVertex();
- buffer.pos(2, 2, 0).tex(1, 0).endVertex();
-
- tessellator.draw();
-
- GlStateManager.popMatrix();
-
- GlStateManager.enableLighting();
- GlStateManager.disableBlend();
-
- GlStateManager.popMatrix();
- }
- }
-
- private static void renderShieldFirstPerson(EntityPlayer entityplayer){
- ItemStack wand = entityplayer.getActiveItemStack();
- if(WizardData.get(entityplayer) != null && WizardData.get(entityplayer).shield != null
- && (WizardData.get(entityplayer).currentlyCasting() instanceof Shield
- || (entityplayer.isHandActive() && wand.getItemDamage() < wand.getMaxDamage() && wand.getItem() instanceof ItemWand
- && WandHelper.getCurrentSpell(wand) instanceof Shield))){
-
- GlStateManager.pushMatrix();
-
- GlStateManager.disableCull();
- GlStateManager.enableBlend();
- GlStateManager.blendFunc(GL11.GL_ONE, GL11.GL_SRC_ALPHA);
- GlStateManager.shadeModel(GL11.GL_SMOOTH);
- GlStateManager.disableLighting();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
-
- GlStateManager.translate(0, 1.4, 0);
-
- GlStateManager.rotate(-entityplayer.rotationYaw, 0, 1, 0);
- GlStateManager.rotate(entityplayer.rotationPitch, 1, 0, 0);
-
- GlStateManager.translate(0, 0, 0.8);
-
- Tessellator tessellator = Tessellator.getInstance();
-
- Minecraft.getMinecraft().renderEngine.bindTexture(shieldTexture);
-
- renderShield(tessellator);
-
- GlStateManager.enableLighting();
-
- GlStateManager.shadeModel(GL11.GL_FLAT);
- GlStateManager.enableCull();
- GlStateManager.disableBlend();
- // RenderHelper.enableStandardItemLighting();
-
- GlStateManager.popMatrix();
- }
- }
-
- private static void renderShieldIfActive(EntityPlayer entityplayer){
- ItemStack wand = entityplayer.getActiveItemStack();
- if(WizardData.get(entityplayer).shield != null && (WizardData.get(entityplayer).currentlyCasting() instanceof Shield
- || (entityplayer.isHandActive() && wand.getItemDamage() < wand.getMaxDamage() && wand.getItem() instanceof ItemWand
- && WandHelper.getCurrentSpell(wand) instanceof Shield))){
-
- GlStateManager.pushMatrix();
-
- GlStateManager.disableCull();
- GlStateManager.enableBlend();
- // For some reason, the old blend function (GL11.GL_SRC_ALPHA, GL11.GL_SRC_ALPHA) caused the inner
- // edges to appear black, so I have changed it to this, which looks very slightly different.
- GlStateManager.blendFunc(GL11.GL_ONE, GL11.GL_SRC_ALPHA);
- GlStateManager.shadeModel(GL11.GL_SMOOTH);
- GlStateManager.disableLighting();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
-
- GlStateManager.translate(0, 1.3, 0);
-
- // GlStateManager.rotate(180, 0, 1, 0);
- GlStateManager.rotate(-entityplayer.renderYawOffset, 0, 1, 0);
- // GlStateManager.rotate(-entityplayer.rotationPitch, 1, 0, 0);
-
- GlStateManager.translate(0, 0, 0.8);
-
- Tessellator tessellator = Tessellator.getInstance();
-
- Minecraft.getMinecraft().renderEngine.bindTexture(shieldTexture);
-
- renderShield(tessellator);
-
- GlStateManager.enableLighting();
-
- GlStateManager.shadeModel(GL11.GL_FLAT);
- GlStateManager.enableCull();
- GlStateManager.disableBlend();
- // RenderHelper.enableStandardItemLighting();
-
- GlStateManager.popMatrix();
- }
- }
-
- private static void renderShield(Tessellator tessellator){
+ GlStateManager.disableDepth();
+ GlStateManager.depthMask(false);
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(texture);
+ Tessellator tessellator = Tessellator.getInstance();
BufferBuilder buffer = tessellator.getBuffer();
- double widthOuter = 0.6d;
- double heightOuter = 0.7d;
- double widthInner = 0.3d;
- double heightInner = 0.4d;
- double depth = 0.2d;
-
- buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR);
-
- buffer.pos(-widthOuter, heightInner, -depth).tex(0, 0.2).color(0, 0, 0, 255).endVertex();
- buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
- buffer.pos(-widthInner, heightOuter, -depth).tex(0.2, 0).color(0, 0, 0, 255).endVertex();
- buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
-
- buffer.pos(widthInner, heightOuter, -depth).tex(0.8, 0).color(0, 0, 0, 255).endVertex();
- buffer.pos(widthInner, heightInner, 0).tex(0.8, 0.2).color(200, 200, 255, 255).endVertex();
- buffer.pos(widthOuter, heightInner, -depth).tex(1, 0.2).color(0, 0, 0, 255).endVertex();
- buffer.pos(widthInner, heightInner, 0).tex(0.8, 0.2).color(200, 200, 255, 255).endVertex();
-
- buffer.pos(widthOuter, -heightInner, -depth).tex(1, 0.8).color(0, 0, 0, 255).endVertex();
- buffer.pos(widthInner, -heightInner, 0).tex(0.8, 0.8).color(200, 200, 255, 255).endVertex();
- buffer.pos(widthInner, -heightOuter, -depth).tex(0.8, 1).color(0, 0, 0, 255).endVertex();
- buffer.pos(widthInner, -heightInner, 0).tex(0.8, 0.8).color(200, 200, 255, 255).endVertex();
-
- buffer.pos(-widthInner, -heightOuter, -depth).tex(0.2, 1).color(0, 0, 0, 255).endVertex();
- buffer.pos(-widthInner, -heightInner, 0).tex(0.2, 0.8).color(200, 200, 255, 255).endVertex();
- buffer.pos(-widthOuter, -heightInner, -depth).tex(0, 0.8).color(0, 0, 0, 255).endVertex();
- buffer.pos(-widthInner, -heightInner, 0).tex(0.2, 0.8).color(200, 200, 255, 255).endVertex();
-
- buffer.pos(-widthOuter, heightInner, -depth).tex(0, 0.2).color(0, 0, 0, 255).endVertex();
- buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
-
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+ buffer.pos(0.0D, (double)event.getResolution().getScaledHeight(), -90.0D).tex(0.0D, 1.0D).endVertex();
+ buffer.pos((double)event.getResolution().getScaledWidth(), (double)event.getResolution().getScaledHeight(), -90.0D).tex(1.0D, 1.0D)
+ .endVertex();
+ buffer.pos((double)event.getResolution().getScaledWidth(), 0.0D, -90.0D).tex(1.0D, 0.0D).endVertex();
+ buffer.pos(0.0D, 0.0D, -90.0D).tex(0.0D, 0.0D).endVertex();
tessellator.draw();
+
+ GlStateManager.depthMask(true);
+ GlStateManager.enableDepth();
- buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR);
-
- buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
- buffer.pos(widthInner, heightInner, 0).tex(0.8, 0.2).color(200, 200, 255, 255).endVertex();
- buffer.pos(-widthInner, -heightInner, 0).tex(0.2, 0.8).color(200, 200, 255, 255).endVertex();
- buffer.pos(widthInner, -heightInner, 0).tex(0.8, 0.8).color(200, 200, 255, 255).endVertex();
-
- tessellator.draw();
+ GlStateManager.popMatrix();
}
}
diff --git a/src/main/java/electroblob/wizardry/client/WizardryControlHandler.java b/src/main/java/electroblob/wizardry/client/WizardryControlHandler.java
new file mode 100644
index 00000000..7b98aca0
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/WizardryControlHandler.java
@@ -0,0 +1,121 @@
+package electroblob.wizardry.client;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.gui.GuiSpellDisplay;
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.packet.PacketControlInput;
+import electroblob.wizardry.packet.WizardryPacketHandler;
+import electroblob.wizardry.registry.WizardrySounds;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.PositionedSoundRecord;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraftforge.client.event.MouseEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
+import net.minecraftforge.fml.relauncher.Side;
+
+/** Event handler class responsible for handling wizardry's controls. */
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class WizardryControlHandler {
+
+ static boolean NkeyPressed = false;
+ static boolean BkeyPressed = false;
+
+ // Changed to a tick event to allow mouse button keybinds
+ // The 'lag' that happened previously was actually because the code only fired when a keyboard key was pressed!
+ @SubscribeEvent
+ public static void onTickEvent(TickEvent.ClientTickEvent event){
+
+ if(event.phase == TickEvent.Phase.END) return; // Only really needs to be once per tick
+
+ if(Wizardry.proxy instanceof ClientProxy){
+
+ EntityPlayer player = Minecraft.getMinecraft().player;
+
+ if(player != null){
+
+ ItemStack wand = getWandInUse(player);
+ if(wand == null) return;
+
+ if(ClientProxy.NEXT_SPELL.isKeyDown() && Minecraft.getMinecraft().inGameHasFocus){
+ if(!NkeyPressed){
+ NkeyPressed = true;
+ selectNextSpell(wand);
+ }
+ }else{
+ NkeyPressed = false;
+ }
+
+ if(ClientProxy.PREVIOUS_SPELL.isKeyDown() && Minecraft.getMinecraft().inGameHasFocus){
+ if(!BkeyPressed){
+ BkeyPressed = true;
+ // Packet building
+ selectPreviousSpell(wand);
+ }
+ }else{
+ BkeyPressed = false;
+ }
+ }
+ }
+ }
+
+ // Shift-scrolling to change spells
+ @SubscribeEvent
+ public static void onMouseEvent(MouseEvent event){
+
+ EntityPlayer player = Minecraft.getMinecraft().player;
+ ItemStack wand = getWandInUse(player);
+ if(wand == null) return;
+
+ if(Minecraft.getMinecraft().inGameHasFocus && !wand.isEmpty() && event.getDwheel() != 0 && player.isSneaking()
+ && Wizardry.settings.shiftScrolling){
+
+ event.setCanceled(true);
+
+ int d = Wizardry.settings.reverseScrollDirection ? -event.getDwheel() : event.getDwheel();
+
+ if(d > 0){
+ selectNextSpell(wand);
+ }else if(d < 0){
+ selectPreviousSpell(wand);
+ }
+ }
+ }
+
+ private static ItemStack getWandInUse(EntityPlayer player){
+
+ ItemStack wand = player.getHeldItemMainhand();
+
+ // Only bother sending packets if the player is holding a spellcasting item with more than one spell slot
+ if(!(wand.getItem() instanceof ISpellCastingItem) || ((ISpellCastingItem)wand.getItem()).getSpells(wand).length < 2){
+ wand = player.getHeldItemOffhand();
+ if(!(wand.getItem() instanceof ISpellCastingItem) || ((ISpellCastingItem)wand.getItem()).getSpells(wand).length < 2) return null;
+ }
+
+ return wand;
+ }
+
+ private static void selectNextSpell(ItemStack wand){
+ // Packet building
+ IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.NEXT_SPELL_KEY);
+ WizardryPacketHandler.net.sendToServer(msg);
+ // GUI switch animation
+ ((ISpellCastingItem)wand.getItem()).selectNextSpell(wand); // Makes sure the spell is set immediately for the client
+ GuiSpellDisplay.playSpellSwitchAnimation(true);
+ Minecraft.getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.ITEM_WAND_SWITCH_SPELL, 1));
+ }
+
+ private static void selectPreviousSpell(ItemStack wand){
+ // Packet building
+ IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.PREVIOUS_SPELL_KEY);
+ WizardryPacketHandler.net.sendToServer(msg);
+ // GUI switch animation
+ ((ISpellCastingItem)wand.getItem()).selectPreviousSpell(wand); // Makes sure the spell is set immediately for the client
+ GuiSpellDisplay.playSpellSwitchAnimation(false);
+ Minecraft.getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.ITEM_WAND_SWITCH_SPELL, 1));
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/WizardryKeyHandler.java b/src/main/java/electroblob/wizardry/client/WizardryKeyHandler.java
deleted file mode 100644
index 09bcadc0..00000000
--- a/src/main/java/electroblob/wizardry/client/WizardryKeyHandler.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package electroblob.wizardry.client;
-
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.item.ItemWand;
-import electroblob.wizardry.packet.PacketControlInput;
-import electroblob.wizardry.packet.WizardryPacketHandler;
-import net.minecraft.client.Minecraft;
-import net.minecraft.entity.player.EntityPlayer;
-import net.minecraft.item.ItemStack;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
-import net.minecraftforge.fml.common.gameevent.TickEvent;
-import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-@SideOnly(Side.CLIENT)
-@Mod.EventBusSubscriber(Side.CLIENT)
-public class WizardryKeyHandler {
-
- static boolean NkeyPressed = false;
- static boolean BkeyPressed = false;
-
- // Changed to a tick event to allow mouse button keybinds
- // The 'lag' that happened previously was actually because the code only fired when a keyboard key was pressed!
- @SubscribeEvent
- public static void onTickEvent(TickEvent.ClientTickEvent event){
-
- if(event.phase == TickEvent.Phase.END) return; // Only really needs to be once per tick
-
- if(Wizardry.proxy instanceof ClientProxy){
-
- EntityPlayer player = Minecraft.getMinecraft().player;
-
- if(player != null){
-
- ItemStack wand = player.getHeldItemMainhand();
-
- if(!(wand.getItem() instanceof ItemWand)){
- wand = player.getHeldItemOffhand();
- // If the player isn't holding a wand, then nothing else needs to be done.
- if(!(wand.getItem() instanceof ItemWand)) return;
- }
- }
-
- if(ClientProxy.NEXT_SPELL.isKeyDown() && Minecraft.getMinecraft().inGameHasFocus){
- if(!NkeyPressed){
- NkeyPressed = true;
- // Packet building
- IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.NEXT_SPELL_KEY);
- WizardryPacketHandler.net.sendToServer(msg);
- }
- }else{
- NkeyPressed = false;
- }
-
- if(ClientProxy.PREVIOUS_SPELL.isKeyDown() && Minecraft.getMinecraft().inGameHasFocus){
- if(!BkeyPressed){
- BkeyPressed = true;
- // Packet building
- IMessage msg = new PacketControlInput.Message(
- PacketControlInput.ControlType.PREVIOUS_SPELL_KEY);
- WizardryPacketHandler.net.sendToServer(msg);
- }
- }else{
- BkeyPressed = false;
- }
- }
- }
-}
diff --git a/src/main/java/electroblob/wizardry/client/audio/MovingSoundEntity.java b/src/main/java/electroblob/wizardry/client/audio/MovingSoundEntity.java
new file mode 100644
index 00000000..11291f26
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/audio/MovingSoundEntity.java
@@ -0,0 +1,35 @@
+package electroblob.wizardry.client.audio;
+
+import net.minecraft.client.audio.MovingSound;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.SoundCategory;
+import net.minecraft.util.SoundEvent;
+
+// Copied from MovingSoundMinecart; if it ever breaks between updates take a look at that.
+//@SideOnly(Side.CLIENT)
+public class MovingSoundEntity extends MovingSound {
+
+ protected final T source;
+ protected float distance = 0.0F;
+
+ public MovingSoundEntity(T entity, SoundEvent sound, SoundCategory category, float volume, float pitch, boolean repeat){
+ super(sound, category);
+ this.source = entity;
+ this.repeat = repeat;
+ this.volume = volume;
+ this.pitch = pitch;
+ this.repeatDelay = 0;
+ }
+
+ @Override
+ public void update(){
+
+ if(this.source.isDead){
+ this.donePlaying = true;
+ }else{
+ this.xPosF = (float)this.source.posX;
+ this.yPosF = (float)this.source.posY;
+ this.zPosF = (float)this.source.posZ;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/audio/SoundLoop.java b/src/main/java/electroblob/wizardry/client/audio/SoundLoop.java
new file mode 100644
index 00000000..651fb7fb
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/audio/SoundLoop.java
@@ -0,0 +1,94 @@
+package electroblob.wizardry.client.audio;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.ISound;
+import net.minecraft.util.ITickable;
+import net.minecraft.util.SoundCategory;
+import net.minecraft.util.SoundEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+import net.minecraftforge.fml.relauncher.Side;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Instances of this class represent a set of sounds which together form a looped sound: start, loop and end.
+ * Currently this is only used for continuous spell sounds in wizardry itself, but feel free to make your own
+ * implementations - this class will take care of the internals.
+ *
+ * @since Wizardry 4.2
+ * @author Electroblob
+ */
+// See MusicTicker, this is the same idea of just storing the sounds statically client-side and ticking them
+@Mod.EventBusSubscriber(Side.CLIENT)
+public abstract class SoundLoop implements ITickable {
+
+ private static final Set activeLoops = new HashSet<>();
+
+ private final ISound start;
+ private final ISound loop;
+ private final ISound end;
+
+ private boolean looping = false;
+ private boolean needsRemoving = false;
+
+ public SoundLoop(SoundEvent start, SoundEvent loop, SoundEvent end, SoundCategory category, ISoundFactory factory){
+ // The reason I've gone to the effort of having a factory for these is that we need SoundLoop to have control
+ // over which sounds are repeated and which aren't whilst keeping them private.
+ this.start = factory.create(start, category, false);
+ this.loop = factory.create(loop, category, true);
+ this.end = factory.create(end, category, false);
+ }
+
+ @Override
+ public void update(){
+ // Check every tick if the start sound is done playing and if so, start the loop sound
+ if(!looping && !Minecraft.getMinecraft().getSoundHandler().isSoundPlaying(start)){
+ Minecraft.getMinecraft().getSoundHandler().playSound(loop);
+ looping = true;
+ }
+ }
+
+ /** Stops the loop part of the sound immediately and starts playing the end part. This may be called from subclasses
+ * or externally depending on the implementation. */
+ // For continuous spell sounds it's internally
+ public void endLoop(){
+ Minecraft.getMinecraft().getSoundHandler().stopSound(start);
+ Minecraft.getMinecraft().getSoundHandler().stopSound(loop);
+ Minecraft.getMinecraft().getSoundHandler().playSound(end);
+ // Can't modify activeLoops directly since we'll probably be calling this method from update(), which is
+ // during iteration of activeLoops so it could cause a ConcurrentModificationException
+ this.markForRemoval();
+ }
+
+ /** Marks this sound loop to be removed next tick. */
+ protected void markForRemoval(){
+ this.needsRemoving = true;
+ }
+
+ // Static methods
+
+ public static void addLoop(SoundLoop loop){
+ activeLoops.add(loop);
+ // Do this here rather than in the constructor in case someone wants to play the loop later or reuse it
+ Minecraft.getMinecraft().getSoundHandler().playSound(loop.start);
+ }
+
+ @SubscribeEvent
+ public static void tick(TickEvent.ClientTickEvent event){
+ // Using the END phase means we can check for stopped sounds as soon as they are stopped (effectively),
+ // meaning we don't get the 'cut' between the start and loop sounds
+ // FIXME: Apparently this only works for dispensers. What the heck is the difference?!
+ if(event.phase == TickEvent.Phase.END){
+ activeLoops.forEach(SoundLoop::update);
+ activeLoops.removeIf(s -> s.needsRemoving);
+ }
+ }
+
+ @FunctionalInterface
+ public interface ISoundFactory {
+ ISound create(SoundEvent sound, SoundCategory category, boolean repeat);
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/audio/SoundLoopSpell.java b/src/main/java/electroblob/wizardry/client/audio/SoundLoopSpell.java
new file mode 100644
index 00000000..bb4d9b44
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/audio/SoundLoopSpell.java
@@ -0,0 +1,117 @@
+package electroblob.wizardry.client.audio;
+
+import electroblob.wizardry.data.DispenserCastingData;
+import electroblob.wizardry.registry.WizardrySounds;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.audio.PositionedSound;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.tileentity.TileEntityDispenser;
+import net.minecraft.util.SoundEvent;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+
+/** Abstract base class for sound loops associated with spells; see subclasses below for implementations. */
+public abstract class SoundLoopSpell extends SoundLoop {
+
+ private final Spell spell;
+
+ public SoundLoopSpell(SoundEvent start, SoundEvent loop, SoundEvent end, ISoundFactory factory, Spell spell){
+ super(start, loop, end, WizardrySounds.SPELLS, factory);
+ this.spell = spell;
+ }
+
+ @Override
+ public void update(){
+ // This may be a bit overkill but I might as well put functionality in superclasses where possible
+ if(stillCasting(spell)){
+ // This can't called in the same tick as endLoop because otherwise, if the spell casting stops during the
+ // same tick as the transition to the loop sound it will try and stop the loop immediately after it has
+ // started - and for whatever reason, this causes the 'Channel null in method stop' error.
+ super.update();
+ }else{
+ endLoop();
+ }
+ }
+
+ protected abstract boolean stillCasting(Spell spell);
+
+ /** Implements a sound loop for continuous spells cast by entities. */
+ public static class SoundLoopSpellEntity extends SoundLoopSpell {
+
+ private final EntityLivingBase source;
+
+ public SoundLoopSpellEntity(SoundEvent start, SoundEvent loop, SoundEvent end, Spell spell, EntityLivingBase source, float volume, float pitch){
+ super(start, loop, end, (sound, category, repeat) -> new MovingSoundEntity<>(source, sound, category, volume, pitch, repeat), spell);
+ this.source = source;
+ }
+
+ @Override
+ protected boolean stillCasting(Spell spell){
+ return WizardryUtilities.isCasting(source, spell);
+ }
+ }
+
+ public static abstract class SoundLoopSpellPosition extends SoundLoopSpell {
+
+ public SoundLoopSpellPosition (SoundEvent start, SoundEvent loop, SoundEvent end, Spell spell,
+ double x, double y, double z, float sndVolume, float sndPitch){
+ // Huh, I actually found a use for a non-static initialiser block - hence the double curly brackets...
+ super(start, loop, end, (sound, category, r) -> new PositionedSound(sound, category){{
+ // ...et voila, we can just set protected fields as we please using external variables
+ this.xPosF = (float)x;
+ this.yPosF = (float)y;
+ this.zPosF = (float)z;
+ this.repeat = r;
+ this.volume = sndVolume;
+ this.pitch = sndPitch;
+ }}, spell);
+ }
+ }
+
+ /** Implements a sound loop for continuous spells cast by dispensers. */
+ public static class SoundLoopSpellDispenser extends SoundLoopSpellPosition {
+
+ private final TileEntityDispenser source;
+
+ public SoundLoopSpellDispenser(SoundEvent start, SoundEvent loop, SoundEvent end, Spell spell, World world,
+ double x, double y, double z, float sndVolume, float sndPitch){
+ super(start, loop, end, spell, x, y, z, sndVolume, sndPitch);
+
+ TileEntity tileentity = world.getTileEntity(new BlockPos(x, y, z));
+
+ if(tileentity instanceof TileEntityDispenser) this.source = (TileEntityDispenser)tileentity;
+ else throw new NullPointerException(String.format("Playing continuous spell sound: no dispenser found at %s, %s, %s", x, y, z));
+ }
+
+ @Override
+ protected boolean stillCasting(Spell spell){
+ return DispenserCastingData.get(source).currentlyCasting() == spell;
+ }
+
+ }
+
+ /** Implements a sound loop for continuous spells cast at a position (via commands). */
+ public static class SoundLoopSpellPosTimed extends SoundLoopSpellPosition {
+
+ private int timeLeft;
+
+ public SoundLoopSpellPosTimed(SoundEvent start, SoundEvent loop, SoundEvent end, Spell spell, int duration,
+ double x, double y, double z, float sndVolume, float sndPitch){
+ super(start, loop, end, spell, x, y, z, sndVolume, sndPitch);
+ this.timeLeft = duration;
+ }
+
+ @Override
+ public void update(){
+ super.update();
+ timeLeft--;
+ }
+
+ @Override
+ protected boolean stillCasting(Spell spell){
+ return timeLeft > 0;
+ }
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java
new file mode 100644
index 00000000..9fe07cb1
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/GuiArcaneWorkbench.java
@@ -0,0 +1,449 @@
+package electroblob.wizardry.client.gui;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.data.SpellGlyphData;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.item.IManaStoringItem;
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.item.IWorkbenchItem;
+import electroblob.wizardry.packet.PacketControlInput;
+import electroblob.wizardry.packet.WizardryPacketHandler;
+import electroblob.wizardry.registry.WizardrySounds;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.tileentity.ContainerArcaneWorkbench;
+import electroblob.wizardry.tileentity.TileEntityArcaneWorkbench;
+import electroblob.wizardry.util.WandHelper;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.PositionedSoundRecord;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.gui.inventory.GuiContainer;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.GlStateManager.DestFactor;
+import net.minecraft.client.renderer.GlStateManager.SourceFactor;
+import net.minecraft.client.resources.I18n;
+import net.minecraft.entity.player.InventoryPlayer;
+import net.minecraft.inventory.IInventory;
+import net.minecraft.inventory.Slot;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.client.event.TextureStitchEvent;
+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 org.lwjgl.input.Keyboard;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class GuiArcaneWorkbench extends GuiContainer {
+
+ private GuiButton applyBtn;
+ public static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID,
+ "textures/gui/arcane_workbench.png");
+
+ private IInventory playerInventory;
+ private IInventory arcaneWorkbenchInventory;
+
+ private static final int TOOLTIP_WIDTH = 164;
+
+ /** We report the actual size of the GUI to Minecraft when a wand is in so JEI doesn't overdraw it.
+ * For calculations, we use the size without the tooltip, which is stored in this constant. */
+ private static final int MAIN_GUI_WIDTH = 176;
+
+ private static final int RUNE_LEFT = 38;
+ private static final int RUNE_TOP = 22;
+ private static final int RUNE_WIDTH = 100;
+ private static final int RUNE_HEIGHT = 100;
+
+ private static final int HALO_DIAMETER = 156;
+
+ private static final int TEXTURE_WIDTH = 512;
+ private static final int TEXTURE_HEIGHT = 256;
+
+ private int animationTimer = 0;
+ private static final int ANIMATION_DURATION = 20;
+
+ public GuiArcaneWorkbench(InventoryPlayer invPlayer, TileEntityArcaneWorkbench entity){
+ super(new ContainerArcaneWorkbench(invPlayer, entity));
+ this.playerInventory = invPlayer;
+ this.arcaneWorkbenchInventory = entity;
+ xSize = MAIN_GUI_WIDTH;
+ ySize = 220;
+ }
+
+ // Huh, didn't realise this method existed. Pretty neat.
+ @Override
+ public void updateScreen(){
+ if(animationTimer > 0) animationTimer--;
+ }
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks){
+
+ this.drawDefaultBackground();
+
+ GlStateManager.color(1, 1, 1, 1); // Just in case
+
+ Slot slot = this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT);
+
+ // Tests if there is a wand in the workbench and edits the positioning accordingly
+ if(slot.getHasStack() && slot.getStack().getItem() instanceof IWorkbenchItem
+ && ((IWorkbenchItem)slot.getStack().getItem()).showTooltip(slot.getStack())){
+ xSize = MAIN_GUI_WIDTH + TOOLTIP_WIDTH;
+ guiLeft = (this.width - this.xSize) / 2;
+ this.applyBtn.x = (this.width - TOOLTIP_WIDTH) / 2 + 64;
+ }else{
+ xSize = MAIN_GUI_WIDTH;
+ guiLeft = (this.width - this.xSize) / 2;
+ this.applyBtn.x = this.width / 2 + 64;
+ }
+
+ this.applyBtn.enabled = slot.getHasStack();
+
+ super.drawScreen(mouseX, mouseY, partialTicks);
+
+ // Required now, or item mouseover tooltips won't render.
+ this.renderHoveredToolTip(mouseX, mouseY);
+ }
+
+ @Override
+ public void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY){
+
+ GlStateManager.color(1, 1, 1, 1);
+ Minecraft.getMinecraft().renderEngine.bindTexture(texture);
+
+ // Animation
+
+ // Grey background
+ DrawingUtils.drawTexturedRect(guiLeft + RUNE_LEFT, guiTop + RUNE_TOP, MAIN_GUI_WIDTH + TOOLTIP_WIDTH, 0,
+ RUNE_WIDTH, RUNE_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ // Yellow 'halo'
+ if(animationTimer > 0){
+
+ GlStateManager.pushMatrix();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+
+ int x = guiLeft + RUNE_LEFT + RUNE_WIDTH/2;
+ int y = guiTop + RUNE_TOP + RUNE_HEIGHT/2;
+
+ float scale = (animationTimer + partialTicks)/ANIMATION_DURATION;
+ scale = (float)(1 - Math.pow(1-scale, 1.4f)); // Makes it slower at the start and speed up
+ GlStateManager.scale(scale, scale, 1);
+ GlStateManager.translate(x/scale, y/scale, 0);
+
+ DrawingUtils.drawTexturedRect(-HALO_DIAMETER /2, -HALO_DIAMETER /2, MAIN_GUI_WIDTH + TOOLTIP_WIDTH, RUNE_HEIGHT,
+ HALO_DIAMETER, HALO_DIAMETER, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ GlStateManager.disableBlend();
+ GlStateManager.popMatrix();
+ }
+
+ // Main inventory
+ DrawingUtils.drawTexturedRect(guiLeft, guiTop, 0, 0, MAIN_GUI_WIDTH, ySize, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ float opacity = (animationTimer + partialTicks)/ANIMATION_DURATION;
+
+ // Changing slots
+ for(int i = 0; i < ContainerArcaneWorkbench.CRYSTAL_SLOT; i++){
+
+ Slot slot = this.inventorySlots.getSlot(i);
+
+ if(slot.xPos >= 0 && slot.yPos >= 0){
+ // Slot background
+ DrawingUtils.drawTexturedRect(guiLeft + slot.xPos - 10, guiTop + slot.yPos - 10, 0, 220, 36, 36, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ // Slot animation
+ // IDEA: Somehow replace with intelligent check for whether the spell actually got applied
+ if(animationTimer > 0 && slot.getHasStack()){
+
+ GlStateManager.pushMatrix();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+ GlStateManager.color(1, 1, 1, opacity);
+
+ DrawingUtils.drawTexturedRect(guiLeft + slot.xPos - 10, guiTop + slot.yPos - 10, 36, 220, 36, 36, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ GlStateManager.color(1, 1, 1, 1);
+ GlStateManager.disableBlend();
+ GlStateManager.popMatrix();
+ }
+ }
+ }
+
+ // Crystal + upgrade slot animations
+ if(animationTimer > 0){
+
+ Slot crystals = this.inventorySlots.getSlot(ContainerArcaneWorkbench.CRYSTAL_SLOT);
+ Slot upgrades = this.inventorySlots.getSlot(ContainerArcaneWorkbench.UPGRADE_SLOT);
+
+ if(crystals.getHasStack()){
+
+ GlStateManager.pushMatrix();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+ GlStateManager.color(1, 1, 1, opacity);
+
+ DrawingUtils.drawTexturedRect(guiLeft + crystals.xPos - 8, guiTop + crystals.yPos - 8,
+ MAIN_GUI_WIDTH + TOOLTIP_WIDTH + RUNE_WIDTH, 0, 32, 32, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ GlStateManager.color(1, 1, 1, 1);
+ GlStateManager.disableBlend();
+ GlStateManager.popMatrix();
+ }
+
+ if(upgrades.getHasStack()){
+
+ GlStateManager.pushMatrix();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+ GlStateManager.color(1, 1, 1, opacity);
+
+ DrawingUtils.drawTexturedRect(guiLeft + upgrades.xPos - 8, guiTop + upgrades.yPos - 8,
+ MAIN_GUI_WIDTH + TOOLTIP_WIDTH + RUNE_WIDTH, 0, 32, 32, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ GlStateManager.color(1, 1, 1, 1);
+ GlStateManager.disableBlend();
+ GlStateManager.popMatrix();
+ }
+ }
+
+ // Tooltip only drawn if there is a wand
+ if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT).getHasStack()){
+
+ ItemStack stack = this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT).getStack();
+
+ if(!(stack.getItem() instanceof IWorkbenchItem)){
+ Wizardry.logger.warn("Invalid item in central slot of arcane workbench, how did that get there?!");
+ return;
+ }
+
+ if(((IWorkbenchItem)stack.getItem()).showTooltip(stack)){
+
+ // Tooltip box
+ DrawingUtils.drawTexturedRect(guiLeft + MAIN_GUI_WIDTH, guiTop, MAIN_GUI_WIDTH, 0, TOOLTIP_WIDTH, ySize, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ int y = guiTop + 20;
+
+ if(stack.getItem() instanceof IManaStoringItem && ((IManaStoringItem)stack.getItem()).showManaInWorkbench(this.mc.player, stack)){
+ y += 14;
+ }
+
+ if(stack.getItem() instanceof ISpellCastingItem && ((ISpellCastingItem)stack.getItem()).showSpellsInWorkbench(this.mc.player, stack)){
+
+ Spell[] spells = ((ISpellCastingItem)stack.getItem()).getSpells(stack);
+
+ GlStateManager.enableBlend();
+
+ for(Spell spell : spells){
+
+ boolean discovered = true;
+
+ if(!this.mc.player.isCreative() && WizardData.get(this.mc.player) != null){
+ discovered = WizardData.get(this.mc.player).hasSpellBeenDiscovered(spell);
+ }
+ // As of Wizardry 1.2, the icons have been split off into their own texture files to allow for add-on
+ // mods to add their own.
+ Minecraft.getMinecraft().renderEngine
+ .bindTexture(discovered ? spell.getElement().getIcon() : Element.MAGIC.getIcon());
+
+ // Renders the little element icon
+ DrawingUtils.drawTexturedRect(guiLeft + MAIN_GUI_WIDTH + 5, y, 8, 8);
+
+ y += 10;
+ }
+ }
+
+ GlStateManager.disableBlend();
+
+ int x = 0;
+ y += 16;
+
+ // Look how much shorter this is with the WandHelper class!
+ for(Item item : WandHelper.getSpecialUpgrades()){
+
+ int level = WandHelper.getUpgradeLevel(stack, item);
+
+ if(level > 0){
+ ItemStack stack1 = new ItemStack(item, level);
+ GlStateManager.enableDepth();
+ this.itemRender.renderItemAndEffectIntoGUI(stack1, guiLeft + MAIN_GUI_WIDTH + 6 + x, y);
+ this.itemRender.renderItemOverlayIntoGUI(this.fontRenderer, stack1, guiLeft + MAIN_GUI_WIDTH + 6 + x, y,
+ null);
+ x += 18;
+ GlStateManager.disableDepth();
+ }
+ }
+ }
+ }
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(texture);
+
+ // Fixes the bug that caused the slot highlight to render opaque. I don't know why it works, it just works!
+ GlStateManager.disableBlend();
+ GlStateManager.enableAlpha();
+ }
+
+ @Override
+ protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY){
+
+ GlStateManager.color(1, 1, 1, 1); // Just in case
+
+ this.fontRenderer
+ .drawString(this.arcaneWorkbenchInventory.hasCustomName() ? this.arcaneWorkbenchInventory.getName()
+ : I18n.format(this.arcaneWorkbenchInventory.getName()), 8, 6, 4210752);
+ this.fontRenderer.drawString(this.playerInventory.hasCustomName() ? this.playerInventory.getName()
+ : I18n.format(this.playerInventory.getName()), 8, this.ySize - 96 + 2, 4210752);
+
+ if(this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT).getHasStack()){
+
+ ItemStack stack = this.inventorySlots.getSlot(ContainerArcaneWorkbench.CENTRE_SLOT).getStack();
+
+ if(!(stack.getItem() instanceof IWorkbenchItem)){
+ Wizardry.logger.warn("Invalid item in central slot of arcane workbench, how did that get there?!");
+ return;
+ }
+
+ if(((IWorkbenchItem)stack.getItem()).showTooltip(stack)){
+
+ int y = 6;
+
+ this.fontRenderer.drawStringWithShadow("\u00A7f" + stack.getDisplayName(), MAIN_GUI_WIDTH + 6, y, 0);
+
+ if(stack.getItem() instanceof IManaStoringItem && ((IManaStoringItem)stack.getItem()).showManaInWorkbench(this.mc.player, stack)){
+ y += 14;
+ this.fontRenderer.drawStringWithShadow(
+ "\u00A77" + I18n.format("container." + Wizardry.MODID + ":arcane_workbench.mana")
+ + " " + ((IManaStoringItem)stack.getItem()).getMana(stack) + "/"
+ + ((IManaStoringItem)stack.getItem()).getManaCapacity(stack),
+ MAIN_GUI_WIDTH + 6, y, 0);
+ }
+
+ y += 14;
+
+ if(stack.getItem() instanceof ISpellCastingItem && ((ISpellCastingItem)stack.getItem()).showSpellsInWorkbench(this.mc.player, stack)){
+
+ Spell[] spells = ((ISpellCastingItem)stack.getItem()).getSpells(stack);
+
+ for(Spell spell : spells){
+
+ boolean discovered = true;
+
+ if(!this.mc.player.isCreative() && WizardData.get(this.mc.player) != null){
+ discovered = WizardData.get(this.mc.player).hasSpellBeenDiscovered(spell);
+ }
+
+ if(discovered){
+ this.fontRenderer.drawStringWithShadow(spell.getDisplayNameWithFormatting(), MAIN_GUI_WIDTH + 16, y, 0);
+ }else{
+ this.mc.standardGalacticFontRenderer.drawStringWithShadow(
+ "\u00A79" + SpellGlyphData.getGlyphName(spell, this.mc.world), MAIN_GUI_WIDTH + 16, y, 0);
+ }
+ y += 10;
+ }
+ }
+
+ if(WandHelper.getTotalUpgrades(stack) > 0){
+
+ y += 6;
+
+ this.fontRenderer.drawStringWithShadow("\u00A7f" + I18n.format("container."
+ + Wizardry.MODID + ":arcane_workbench.upgrades"), MAIN_GUI_WIDTH + 6, y, 0);
+
+ int x = 0;
+ y += 10;
+
+ // Wand upgrade tooltips
+ for(Item item : WandHelper.getSpecialUpgrades()){
+
+ int level = WandHelper.getUpgradeLevel(stack, item);
+
+ if(level > 0){
+ // The javadoc for isPointInRegion is ambiguous; what it means is that the REGION is
+ // relative to the GUI but the POINT isn't.
+ if(isPointInRegion(MAIN_GUI_WIDTH + 6 + x, y, 16, 16, mouseX, mouseY)){
+ ItemStack stack1 = new ItemStack(item, level);
+ this.renderToolTip(stack1, mouseX - guiLeft, mouseY - guiTop);
+ }
+ x += 18;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void initGui(){
+ this.mc.player.openContainer = this.inventorySlots;
+ this.guiLeft = (this.width - this.xSize) / 2;
+ this.guiTop = (this.height - this.ySize) / 2;
+ Keyboard.enableRepeatEvents(true);
+ this.buttonList.clear();
+ this.buttonList.add(this.applyBtn = new GuiButtonApply(0, this.width / 2 + 64, this.height / 2 + 3));
+ }
+
+ @Override
+ public void onGuiClosed(){
+ super.onGuiClosed();
+ Keyboard.enableRepeatEvents(false);
+ }
+
+ @Override
+ protected void actionPerformed(GuiButton button){
+ if(button.enabled){
+ if(button.id == 0){
+ // Packet building
+ IMessage msg = new PacketControlInput.Message(PacketControlInput.ControlType.APPLY_BUTTON);
+ WizardryPacketHandler.net.sendToServer(msg);
+ // Sound
+ Minecraft.getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(
+ WizardrySounds.BLOCK_ARCANE_WORKBENCH_SPELLBIND, 1));
+ // Animation
+ animationTimer = 20;
+ }
+ }
+ }
+
+ private class GuiButtonApply extends GuiButton {
+
+ public GuiButtonApply(int id, int x, int y){
+ super(id, x, y, 16, 16, I18n.format("container." + Wizardry.MODID + ":arcane_workbench.apply"));
+ }
+
+ @Override
+ public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
+
+ // Whether the button is highlighted
+ this.hovered = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
+
+ int k = 72;
+ int l = 220;
+ //int colour = 14737632;
+
+ if(this.enabled){
+ if(this.hovered){
+ k += this.width * 2;
+ //colour = 16777120;
+ }
+ }else{
+ k += this.width;
+ //colour = 10526880;
+ }
+
+ DrawingUtils.drawTexturedRect(this.x, this.y, k, l, this.width, this.height, 512, 256);
+ //this.drawCenteredString(minecraft.fontRenderer, this.displayString, this.x + this.width / 2,
+ // this.y + (this.height - 8) / 2, colour);
+ }
+ }
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+ event.getMap().registerSprite(ContainerArcaneWorkbench.EMPTY_SLOT_CRYSTAL);
+ event.getMap().registerSprite(ContainerArcaneWorkbench.EMPTY_SLOT_UPGRADE);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/GuiButtonInvisible.java b/src/main/java/electroblob/wizardry/client/gui/GuiButtonInvisible.java
similarity index 67%
rename from src/main/java/electroblob/wizardry/client/GuiButtonInvisible.java
rename to src/main/java/electroblob/wizardry/client/gui/GuiButtonInvisible.java
index 70e9c2e4..7156bb36 100644
--- a/src/main/java/electroblob/wizardry/client/GuiButtonInvisible.java
+++ b/src/main/java/electroblob/wizardry/client/gui/GuiButtonInvisible.java
@@ -1,12 +1,10 @@
-package electroblob.wizardry.client;
+package electroblob.wizardry.client.gui;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiButton;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-class GuiButtonInvisible extends GuiButton {
+//@SideOnly(Side.CLIENT)
+public class GuiButtonInvisible extends GuiButton {
public GuiButtonInvisible(int id, int x, int y, int width, int height){
super(id, x, y, width, height, "");
diff --git a/src/main/java/electroblob/wizardry/client/gui/GuiButtonResurrect.java b/src/main/java/electroblob/wizardry/client/gui/GuiButtonResurrect.java
new file mode 100644
index 00000000..bf50192c
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/GuiButtonResurrect.java
@@ -0,0 +1,90 @@
+package electroblob.wizardry.client.gui;
+
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.item.ItemArtefact;
+import electroblob.wizardry.packet.PacketControlInput;
+import electroblob.wizardry.packet.WizardryPacketHandler;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.registry.WizardryItems;
+import electroblob.wizardry.spell.Resurrection;
+import electroblob.wizardry.util.SpellModifiers;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.gui.GuiGameOver;
+import net.minecraft.client.resources.I18n;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumHand;
+import net.minecraftforge.client.event.GuiScreenEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+import net.minecraftforge.fml.relauncher.Side;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class GuiButtonResurrect extends GuiButton {
+
+ private static int timeSinceDeath = -1;
+
+ private final String translationKey;
+
+ public GuiButtonResurrect(int id, int x, int y, String translationKey){
+ super(id, x, y, I18n.format(translationKey + "_wait", Resurrection.getRemainingWaitTime(timeSinceDeath)));
+ this.translationKey = translationKey;
+ }
+
+ @Override
+ public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks){
+ int waitTime = Resurrection.getRemainingWaitTime(timeSinceDeath);
+ this.enabled = waitTime == 0;
+ this.displayString = I18n.format(translationKey + (waitTime == 0 ? "_ready" : "_wait"), waitTime);
+ super.drawButton(mc, mouseX, mouseY, partialTicks);
+ }
+
+ // Event handlers
+
+ @SubscribeEvent
+ public static void onClientTickEvent(TickEvent.ClientTickEvent event){
+ if(event.phase == TickEvent.Phase.START && timeSinceDeath >= 0) timeSinceDeath++;
+ }
+
+ @SubscribeEvent
+ public static void onGuiScreenInitEvent(GuiScreenEvent.InitGuiEvent event){
+
+ if(event.getGui() instanceof GuiGameOver && ItemArtefact.isArtefactActive(Minecraft.getMinecraft().player, WizardryItems.amulet_resurrection)
+ && WizardryUtilities.getHotbar(Minecraft.getMinecraft().player).stream().anyMatch(s -> Resurrection.canStackResurrect(s, Minecraft.getMinecraft().player))){
+
+ event.getButtonList().add(new GuiButtonResurrect(event.getButtonList().size(), event.getGui().width / 2 - 100,
+ event.getGui().height / 4 + 120, "spell." + Spells.resurrection.getRegistryName() + ".button"));
+ timeSinceDeath = 0;
+ }
+ }
+
+ @SubscribeEvent
+ public static void onGuiScreenActionPerformedEvent(GuiScreenEvent.ActionPerformedEvent event){
+
+ if(event.getGui() instanceof GuiGameOver){
+
+ ItemStack stack = WizardryUtilities.getHotbar(Minecraft.getMinecraft().player).stream()
+ .filter(s -> Resurrection.canStackResurrect(s, Minecraft.getMinecraft().player)).findFirst().orElse(null);
+
+ if(stack != null){
+
+ if(event.getButton() instanceof GuiButtonResurrect && timeSinceDeath >= 0){
+ // Cast resurrection on the client player and notify the server to do the same
+ // ISpellCastingItem#canCast already checked in Resurrection#canStackResurrect
+ ((ISpellCastingItem)stack.getItem()).cast(stack, Spells.resurrection, Minecraft.getMinecraft().player, EnumHand.MAIN_HAND, 0, new SpellModifiers());
+ WizardryPacketHandler.net.sendToServer(new PacketControlInput.Message(PacketControlInput.ControlType.RESURRECT_BUTTON));
+
+ }else if(!Minecraft.getMinecraft().world.getGameRules().getBoolean("keepInventory")){
+ // Any other button drops the wand (N.B. this should be inside the stack != null check or it'll send
+ // packets unnecessarily and generate incorrect warnings
+ WizardryPacketHandler.net.sendToServer(new PacketControlInput.Message(PacketControlInput.ControlType.CANCEL_RESURRECT));
+ }
+
+ timeSinceDeath = -1;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/GuiPortableCrafting.java b/src/main/java/electroblob/wizardry/client/gui/GuiPortableCrafting.java
similarity index 97%
rename from src/main/java/electroblob/wizardry/client/GuiPortableCrafting.java
rename to src/main/java/electroblob/wizardry/client/gui/GuiPortableCrafting.java
index 5fc05f3a..88162dbf 100644
--- a/src/main/java/electroblob/wizardry/client/GuiPortableCrafting.java
+++ b/src/main/java/electroblob/wizardry/client/gui/GuiPortableCrafting.java
@@ -1,4 +1,4 @@
-package electroblob.wizardry.client;
+package electroblob.wizardry.client.gui;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.client.renderer.GlStateManager;
diff --git a/src/main/java/electroblob/wizardry/client/gui/GuiSpellBook.java b/src/main/java/electroblob/wizardry/client/gui/GuiSpellBook.java
new file mode 100644
index 00000000..306abffb
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/GuiSpellBook.java
@@ -0,0 +1,121 @@
+package electroblob.wizardry.client.gui;
+
+import com.google.common.collect.ImmutableMap;
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.constants.Tier;
+import electroblob.wizardry.data.SpellGlyphData;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.registry.WizardrySounds;
+import electroblob.wizardry.spell.Spell;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.PositionedSoundRecord;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.util.ResourceLocation;
+import org.lwjgl.input.Keyboard;
+
+import java.util.Map;
+
+public class GuiSpellBook extends GuiScreen {
+
+ private int xSize, ySize;
+ private Spell spell;
+
+ private static final Map textures = ImmutableMap.of(
+ Tier.NOVICE, new ResourceLocation(Wizardry.MODID, "textures/gui/spell_book_novice.png"),
+ Tier.APPRENTICE, new ResourceLocation(Wizardry.MODID, "textures/gui/spell_book_apprentice.png"),
+ Tier.ADVANCED, new ResourceLocation(Wizardry.MODID, "textures/gui/spell_book_advanced.png"),
+ Tier.MASTER, new ResourceLocation(Wizardry.MODID, "textures/gui/spell_book_master.png"));
+
+ public GuiSpellBook(Spell spell){
+ super();
+ xSize = 288;
+ ySize = 180;
+ this.spell = spell;
+ }
+
+ /**
+ * Draws the screen and all the components in it.
+ */
+ public void drawScreen(int par1, int par2, float par3){
+
+ int xPos = this.width / 2 - xSize / 2;
+ int yPos = this.height / 2 - this.ySize / 2;
+
+ EntityPlayer player = Minecraft.getMinecraft().player;
+
+ boolean discovered = true;
+ if(Wizardry.settings.discoveryMode && !player.isCreative() && WizardData.get(player) != null
+ && !WizardData.get(player).hasSpellBeenDiscovered(spell)){
+ discovered = false;
+ }
+
+ GlStateManager.color(1, 1, 1, 1); // Just in case
+
+ // Draws spell illustration on opposite page, underneath the book so it shows through the hole.
+ Minecraft.getMinecraft().renderEngine.bindTexture(discovered ? spell.getIcon() : Spells.none.getIcon());
+ DrawingUtils.drawTexturedRect(xPos + 146, yPos + 20, 0, 0, 128, 128, 128, 128);
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(textures.get(spell.getTier()));
+ DrawingUtils.drawTexturedRect(xPos, yPos, 0, 0, xSize, ySize, xSize, 256);
+
+ super.drawScreen(par1, par2, par3);
+
+ if(discovered){
+ this.fontRenderer.drawString(spell.getDisplayName(), xPos + 17, yPos + 15, 0);
+ this.fontRenderer.drawString(spell.getType().getDisplayName(), xPos + 17, yPos + 26, 0x777777);
+ }else{
+ this.mc.standardGalacticFontRenderer.drawString(SpellGlyphData.getGlyphName(spell, player.world), xPos + 17,
+ yPos + 15, 0);
+ this.mc.standardGalacticFontRenderer.drawString(spell.getType().getDisplayName(), xPos + 17, yPos + 26,
+ 0x777777);
+ }
+
+ //this.fontRenderer.drawString("-------------------", xPos + 17, yPos + 35, 0);
+
+ if(spell.getTier() == Tier.NOVICE){
+ // Basic is usually white but this doesn't show up.
+ this.fontRenderer.drawString("Tier: \u00A77" + Tier.NOVICE.getDisplayName(), xPos + 17, yPos + 45, 0);
+ }else{
+ this.fontRenderer.drawString("Tier: " + spell.getTier().getDisplayNameWithFormatting(), xPos + 17, yPos + 45, 0);
+ }
+
+ String element = "Element: " + spell.getElement().getFormattingCode() + spell.getElement().getDisplayName();
+ if(!discovered) element = "Element: ?";
+ this.fontRenderer.drawString(element, xPos + 17, yPos + 57, 0);
+
+ String manaCost = "Mana Cost: " + spell.getCost();
+ if(spell.isContinuous) manaCost = "Mana Cost: " + spell.getCost() + "/second";
+ if(!discovered) manaCost = "Mana Cost: ?";
+ this.fontRenderer.drawString(manaCost, xPos + 17, yPos + 69, 0);
+
+ if(discovered){
+ this.fontRenderer.drawSplitString(spell.getDescription(), xPos + 17, yPos + 83, 118, 0);
+ }else{
+ this.mc.standardGalacticFontRenderer.drawSplitString(
+ SpellGlyphData.getGlyphDescription(spell, player.world), xPos + 17, yPos + 83, 118, 0);
+ }
+ }
+
+ public void initGui(){
+ super.initGui();
+ Keyboard.enableRepeatEvents(true);
+ this.buttonList.clear();
+
+ this.mc.getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_BOOK_OPEN, 1));
+ }
+
+ public void onGuiClosed(){
+ super.onGuiClosed();
+ Keyboard.enableRepeatEvents(false);
+ }
+
+ @Override
+ public boolean doesGuiPauseGame(){
+ return Wizardry.settings.booksPauseGame;
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/GuiSpellDisplay.java b/src/main/java/electroblob/wizardry/client/gui/GuiSpellDisplay.java
new file mode 100644
index 00000000..85e2aa61
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/GuiSpellDisplay.java
@@ -0,0 +1,589 @@
+package electroblob.wizardry.client.gui;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import electroblob.wizardry.Settings;
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.ClientProxy;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.client.MixedFontRenderer;
+import electroblob.wizardry.data.SpellGlyphData;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.registry.WizardryPotions;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.util.WandHelper;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.GlStateManager.DestFactor;
+import net.minecraft.client.renderer.GlStateManager.SourceFactor;
+import net.minecraft.client.resources.IResource;
+import net.minecraft.client.resources.IResourceManager;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.EnumHandSide;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.*;
+import java.util.Map.Entry;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class GuiSpellDisplay {
+
+ private static final ResourceLocation INDEX = new ResourceLocation(Wizardry.MODID, "textures/gui/spell_hud/_index.json");
+
+ /** A map which stores all loaded HUD skin objects. This gets wiped on resource pack reload and repopulated with
+ * mappings as specified by {@code _index.json} (these stack between resource packs). The keys in the map correspond
+ * to the keys in {@code _index.json}, and are sorted in that order, with skins belonging to resource packs sorted
+ * from lowest to highest priority. The skins in the base mod will therefore always be first. (It should be noted,
+ * however, that in the gui itself the skins are always sorted in alphabetical order for some reason.) */
+ private static final Map skins = new LinkedHashMap<>(14); // 14 is the number of skins packaged with the mod
+
+ private static final Gson gson = new Gson();
+
+ /** Width and height of the spell icon (very unlikely to change!) */
+ private static final int SPELL_ICON_SIZE = 32;
+ /** Number of ticks the spell switching animation plays for. */
+ private static final int SPELL_SWITCH_TIME = 4;
+ /** Scale of the next/previous spell names. */
+ private static final float SPELL_NAME_SCALE = 0.5f;
+ /** Opacity of the next/previous spell names, as a fraction. */
+ private static final float SPELL_NAME_OPACITY = 0.3f;
+
+ private static final int HALF_HOTBAR_WIDTH = 97; // Half the width of the hotbar, plus a bit for clearance
+ private static final int OFFHAND_SLOT_WIDTH = 29; // Width of the offhand slot plus the gap between it and the hotbar
+
+ /** Controls the spell switching animation. Positive when switching to the next spell, negative when switching to
+ * the previous spell. Decremented in magnitude by 1 each tick until it reaches 0 again. */
+ private static int switchTimer = 0;
+
+ /**
+ * Starts the spell switching animation.
+ * @param next True to switch to the next spell, false for the previous spell.
+ */
+ public static void playSpellSwitchAnimation(boolean next){
+ switchTimer = next ? SPELL_SWITCH_TIME : -SPELL_SWITCH_TIME;
+ }
+
+ /** Returns an unmodifiable set of the string keys for all of the loaded spell HUD skins. */
+ public static Set getSkinKeys(){
+ return Collections.unmodifiableSet(skins.keySet());
+ }
+
+ /** Returns an unmodifiable view of the loaded spell HUD skins map. */
+ public static Map getSkins(){
+ return Collections.unmodifiableMap(skins);
+ }
+
+ /** Returns the skin that corresponds to the given key. */
+ public static Skin getSkin(String key){
+ return skins.get(key);
+ }
+
+ // Normally when extending Gui, you'd have to have an instance to access its methods. However, we're not actually
+ // using any of them, so this class may as well not bother and just be a static event handler. Neat!
+ @SubscribeEvent
+ public static void draw(RenderGameOverlayEvent event){
+
+ Minecraft mc = Minecraft.getMinecraft();
+
+ EntityPlayer player = mc.player;
+
+ if(player.isSpectator()) return; // Spectators shouldn't have the spell HUD!
+
+ // If the player has a wand in each hand, only displays for the one in the main hand.
+
+ ItemStack wand = player.getHeldItemMainhand();
+ boolean mainHand = true;
+
+ if(!(wand.getItem() instanceof ISpellCastingItem && ((ISpellCastingItem)wand.getItem()).showSpellHUD(player, wand))){
+ wand = player.getHeldItemOffhand();
+ mainHand = false;
+ // If the player isn't holding a spellcasting item that shows the HUD, then nothing else needs to be done.
+ if(!(wand.getItem() instanceof ISpellCastingItem && ((ISpellCastingItem)wand.getItem()).showSpellHUD(player, wand))) return;
+ }
+
+ int width = event.getResolution().getScaledWidth();
+ int height = event.getResolution().getScaledHeight();
+
+ boolean flipX = Wizardry.settings.spellHUDPosition.flipX;
+ boolean flipY = Wizardry.settings.spellHUDPosition.flipY;
+
+ if(Wizardry.settings.spellHUDPosition.dynamic){
+ // ............. | This bit is true if the wand is on the left, false if it is on the right
+ flipX = flipX == ((mainHand ? player.getPrimaryHand() : player.getPrimaryHand().opposite()) == EnumHandSide.LEFT);
+ }
+
+ Skin skin = skins.get(Wizardry.settings.spellHUDSkin);
+
+ if(skin == null){
+
+ Wizardry.logger.info("The spell HUD skin '" + Wizardry.settings.spellHUDSkin + "' specified in the config"
+ + " did not match any of the loaded skins; using the default skin as a fallback.");
+
+ skin = skins.get(Settings.DEFAULT_HUD_SKIN_KEY);
+
+ if(skin == null){
+ Wizardry.logger.warn("The default spell HUD skin is missing! A resource pack must have overridden it"
+ + " with an invalid JSON file (default.json), please try again without any resource packs.");
+ return;
+ }
+ }
+
+ GlStateManager.pushMatrix();
+
+ // 'Origin' of the spell hud (bottom left corner of the actual texture, always in the corner of the screen)
+ int x = flipX ? width : 0;
+ int y = flipY ? 0: height;
+
+ // The space available to render the spell HUD
+ float xSpace = (float)(width/2 - HALF_HOTBAR_WIDTH);
+ if(!player.getHeldItemOffhand().isEmpty()
+ // Tests whether the offhand slot is rendered on the same side of the hotbar as the spell HUD
+ && (player.getPrimaryHand() == EnumHandSide.LEFT) == flipX){
+ xSpace -= OFFHAND_SLOT_WIDTH;
+ }
+
+ // If the skin is at the bottom and the screen width is too small, scale it to avoid the hotbar and offhand
+ if(!flipY && skin.getWidth() > xSpace){ // width/2 - 91 - 29 taken from GuiInGame line 547
+ float scale = xSpace / skin.getWidth();
+ GlStateManager.scale(scale, scale, 1);
+ x = MathHelper.ceil(x/scale);
+ y = MathHelper.ceil(y/scale);
+ }
+
+ Spell spell = WandHelper.getCurrentSpell(wand);
+ int cooldown = WandHelper.getCurrentCooldown(wand);
+ int maxCooldown = WandHelper.getCurrentMaxCooldown(wand);
+
+ if(event.getType() == RenderGameOverlayEvent.ElementType.TEXT){
+
+ float animationProgress = Math.signum(switchTimer) * ((SPELL_SWITCH_TIME - Math.abs(switchTimer) +
+ event.getPartialTicks()) / SPELL_SWITCH_TIME);
+
+ String prevSpellName = getFormattedSpellName(WandHelper.getPreviousSpell(wand), player, WandHelper.getPreviousCooldown(wand));
+ String spellName = getFormattedSpellName(spell, player, cooldown);
+ String nextSpellName = getFormattedSpellName(WandHelper.getNextSpell(wand), player, WandHelper.getNextCooldown(wand));
+
+ skin.drawText(x, y, flipX, flipY, prevSpellName, spellName, nextSpellName, animationProgress);
+
+ }else if(event.getType() == RenderGameOverlayEvent.ElementType.HOTBAR){
+
+ boolean discovered = true;
+
+ if(!player.isCreative() && WizardData.get(player) != null){
+ discovered = WizardData.get(player).hasSpellBeenDiscovered(spell);
+ }
+
+ ResourceLocation icon = discovered ? spell.getIcon() : Spells.none.getIcon();
+
+ float progress = 1;
+ // Doesn't really matter what progress is when in creative, but we might as well avoid the calculation.
+ if(!player.isCreative() && !spell.isContinuous){
+ // Subtracted partial tick time to make it smoother
+ progress = maxCooldown == 0 ? 1 : (maxCooldown - (float)cooldown + event.getPartialTicks()) / maxCooldown;
+ }
+
+ skin.drawBackground(x, y, flipX, flipY, icon, progress, player.isCreative());
+
+ }
+
+ GlStateManager.popMatrix();
+ }
+
+ /**
+ * Gets the name of the given spell, with formatting added according to its cooldown and whether the given player
+ * has discovered it.
+ * @param spell The spell to get the name of.
+ * @param player The player to test for having discovered the given spell.
+ * @param cooldown The spell's current cooldown.
+ * @return The spell name, with relevant formatting added, for use with the {@link MixedFontRenderer}.
+ */
+ private static String getFormattedSpellName(Spell spell, EntityPlayer player, int cooldown){
+
+ boolean discovered = true;
+
+ if(!player.isCreative() && WizardData.get(player) != null){
+ discovered = WizardData.get(player).hasSpellBeenDiscovered(spell);
+ }
+
+ // Makes spells greyed out if they are in cooldown or if the player has the arcane jammer effect
+ String format = cooldown > 0 || player.isPotionActive(WizardryPotions.arcane_jammer) ? "\u00A78" : spell.getElement().getFormattingCode();
+ if(!discovered) format = "\u00A79";
+
+ String name = discovered ? spell.getDisplayName() : SpellGlyphData.getGlyphName(spell, player.world);
+ name = format + name;
+ if(!discovered) name = "#" + name + "#";
+
+ return name;
+ }
+
+ @SubscribeEvent
+ public static void onLivingUpdateEvent(LivingUpdateEvent event){
+ if(event.getEntity() == Minecraft.getMinecraft().player){ // Makes sure this only gets called once each tick.
+ if(switchTimer > 0) switchTimer--;
+ else if(switchTimer < 0) switchTimer++;
+ }
+ }
+
+ /** Called from preInit in the main mod class (via the proxies) to initialise the HUD skins, and again on each
+ * resource reload. */
+ public static void loadSkins(IResourceManager manager){
+
+ try {
+
+ List indexFiles = manager.getAllResources(INDEX);
+
+ skins.clear(); // Wipes the skins map before repopulating it
+
+ for(IResource indexFile : indexFiles){
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(indexFile.getInputStream()));
+
+ JsonElement je = gson.fromJson(reader, JsonElement.class);
+ JsonObject json = je.getAsJsonObject();
+
+ // Need to iterate over these since we don't know what they're called or how many there are
+ for(Entry entry : json.entrySet()){
+
+ String key = entry.getKey(); // Find out what each element is called, this will be the skins map key
+
+ // It's a good idea to use JsonUtils because it produces more helpful error messages (that pack
+ // makers should understand).
+
+ JsonObject skinData = JsonUtils.getJsonObject(json, key);
+
+ String[] splitName = ResourceLocation.splitObjectName(JsonUtils.getString(skinData, "texture"));
+ ResourceLocation texture = new ResourceLocation(splitName[0], "textures/" + splitName[1] + ".png");
+
+ splitName = ResourceLocation.splitObjectName(JsonUtils.getString(skinData, "metadata"));
+ ResourceLocation metadata = new ResourceLocation(splitName[0], "textures/" + splitName[1] + ".json");
+
+ // The nice thing about this is it overwrites the existing mapping, and since the index files are in
+ // ascending order of resource pack priority, this means resource packs can override existing skins
+ // by specifying one with the same key.
+ skins.put(key, new Skin(texture, metadata));
+ }
+ }
+
+ } catch (IOException e){
+ // If an exception is thrown, chances are the resource pack did not have a spell_hud folder, so nothing
+ // else needs to be done.
+ Wizardry.logger.error("Error reading spell HUD skin index file: ", e);
+ }
+ }
+
+ /**
+ * Instances of this class represent individual HUD skins, complete with texture and all necessary metadata. This
+ * class serves to separate the logic behind the spell HUD from its actual rendering.
+ * All information and processing done within this class relates only to the actual drawing; spells and such like
+ * must be queried outside of this class and fed into the methods as appropriate.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.2
+ */
+ public static class Skin {
+
+ /** The texture file for this skin. */
+ private final ResourceLocation texture;
+
+ /** The display name of the skin in the config menu. */
+ private String name;
+ /** The description of the skin shown when its button is hovered over in the config menu. */
+ private String description;
+
+ /** Width of the entire spell HUD. */
+ private int width;
+ /** Height of the entire spell HUD. */
+ private int height;
+
+ /** Whether the entire HUD is flipped when on the right-hand side of the screen. If this is false, the HUD will
+ * still appear on the right-hand side of the screen, but in the same orientation as on the left-hand side. */
+ private boolean mirrorX;
+ /** Whether the entire HUD is flipped when at the top of the screen. If this is false, the HUD will
+ * still appear at the top of the screen, but in the same orientation as at the bottom. */
+ private boolean mirrorY;
+
+ /** Distance of the spell icon from the left edge of the screen (or right edge when flipped). */
+ private int spellIconInsetX;
+ /** Distance of the spell icon from the bottom edge of the screen (or top edge when flipped). */
+ private int spellIconInsetY;
+
+ /** Distance of the spell name from the left edge of the screen (or right edge when flipped). */
+ private int textInsetX;
+ /** Distance of the spell name from the bottom edge of the screen (or the top edge when flipped). */
+ private int textInsetY;
+
+ /** Horizontal distance between the start of adjacent spell names. */
+ private int cascadeOffsetX;
+ /** Vertical distance between the start of adjacent spell names. */
+ private int cascadeOffsetY;
+
+ /** Distance of the cooldown bar from the left edge of the screen (or right edge when flipped). */
+ private int cooldownBarX;
+ /** Distance of the cooldown bar from the bottom edge of the screen (or top edge when flipped). */
+ private int cooldownBarY;
+ /** Length of the cooldown bar. */
+ private int cooldownBarLength;
+ /** Height of the cooldown bar. */
+ private int cooldownBarHeight;
+
+ /** Whether the cooldown bar is flipped horizontally when the HUD is on the right-hand side of the screen. */
+ private boolean cooldownBarMirrorX;
+ /** Whether the cooldown bar is flipped vertically when the HUD is at the top of the screen. */
+ private boolean cooldownBarMirrorY;
+
+ /** Whether the cooldown bar progress overlay is shown when the cooldown bar is full (i.e. when progress = 1). */
+ private boolean showCooldownWhenFull;
+
+ private final Minecraft mc;
+
+ /** Creates a new skin with the given texture and reads its values from the given metadata json file. */
+ public Skin(ResourceLocation texture, ResourceLocation metadata){
+
+ mc = Minecraft.getMinecraft();
+
+ this.texture = texture;
+
+ try {
+ // This time we only want the highest priority file
+ IResource metadataFile = Minecraft.getMinecraft().getResourceManager().getResource(metadata);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(metadataFile.getInputStream()));
+
+ JsonElement je = gson.fromJson(reader, JsonElement.class);
+
+ parseJson(je.getAsJsonObject());
+
+ } catch (IOException e){
+ // If an exception is thrown, chances are the resource pack did not have a spell_hud folder, so nothing
+ // else needs to be done.
+ Wizardry.logger.error("Error reading spell HUD skin metadata file: ", e);
+ }
+ }
+
+ /** Returns the display name of this HUD skin, which is shown in the config GUI. */
+ public String getName(){
+ return name;
+ }
+
+ /** Returns the description of this HUD skin, which is shown in its tooltip in the config GUI. */
+ public String getDescription(){
+ return description;
+ }
+
+ /** Returns the overall width of this spell HUD skin. */
+ public int getWidth(){
+ return width;
+ }
+
+ /** Returns the overall height of this spell HUD skin. */
+ public int getHeight(){
+ return height;
+ }
+
+ /** Actually reads the metadata values for this skin from the json file. */
+ private void parseJson(JsonObject json){
+
+ // For now, all the keys must be present for the metadata file to work (the only ones that could reasonably
+ // have a default anyway are the mirror values).
+
+ name = JsonUtils.getString(json, "name");
+ description = JsonUtils.getString(json, "description");
+
+ width = JsonUtils.getInt(json, "width");
+ if(width > 128) Wizardry.logger.warn("The width of the spell HUD skin " + name + " exceeds 128, this may cause it to render strangely.");
+ height = JsonUtils.getInt(json, "height");
+
+ JsonObject mirror = JsonUtils.getJsonObject(json, "mirror");
+ mirrorX = JsonUtils.getBoolean(mirror, "x");
+ mirrorY = JsonUtils.getBoolean(mirror, "y");
+
+ JsonObject spellIconInset = JsonUtils.getJsonObject(json, "spell_icon_inset");
+ spellIconInsetX = JsonUtils.getInt(spellIconInset, "x");
+ spellIconInsetY = JsonUtils.getInt(spellIconInset, "y");
+
+ JsonObject textInset = JsonUtils.getJsonObject(json, "text_inset");
+ textInsetX = JsonUtils.getInt(textInset, "x");
+ textInsetY = JsonUtils.getInt(textInset, "y");
+
+ JsonObject cascadeOffset = JsonUtils.getJsonObject(json, "spell_cascade_offset");
+ cascadeOffsetX = JsonUtils.getInt(cascadeOffset, "x");
+ cascadeOffsetY = JsonUtils.getInt(cascadeOffset, "y");
+
+ JsonObject cooldownBar = JsonUtils.getJsonObject(json, "cooldown_bar");
+ cooldownBarX = JsonUtils.getInt(cooldownBar, "x");
+ cooldownBarY = JsonUtils.getInt(cooldownBar, "y");
+ cooldownBarLength = JsonUtils.getInt(cooldownBar, "length");
+ cooldownBarHeight = JsonUtils.getInt(cooldownBar, "height");
+
+ JsonObject cooldownBarMirror = JsonUtils.getJsonObject(cooldownBar, "mirror");
+ cooldownBarMirrorX = JsonUtils.getBoolean(cooldownBarMirror, "x");
+ cooldownBarMirrorY = JsonUtils.getBoolean(cooldownBarMirror, "y");
+
+ showCooldownWhenFull = JsonUtils.getBoolean(cooldownBar, "show_when_full");
+
+ }
+
+ // The idea of these methods is that everything in here relates only to the actual drawing of the HUD. In other
+ // words, all processing of which spells to draw and so on is done outside of here. This means that the config
+ // GUI can easily display its preview without having a player or wand stack object to query.
+
+ /**
+ * Draws the background layer of this HUD skin at the given position with the given orientations, with the given
+ * spell icon and cooldown bar progress.
+ *
+ * @param x The x-coordinate of the corner of the spell HUD. The bottom left corner of the actual texture
+ * will always be at this position unless mirrorX/Y is false, so for example if flipX is false and flipY is true,
+ * this will be the corner of the HUD that is closest to the top left corner of the screen.
+ * @param y The y-coordinate of the corner of the spell HUD; see above.
+ * @param flipX Whether to flip the HUD horizontally.
+ * @param flipY Whether to flip the HUD vertically.
+ * @param icon A {@code ResourceLocation} corresponding to the icon of the selected spell.
+ * @param cooldownBarProgress The fraction of the cooldown bar to draw; must be between 0 and 1 (inclusive).
+ * @param creativeMode True to draw the creative mode HUD, false for the survival mode version.
+ */
+ public void drawBackground(int x, int y, boolean flipX, boolean flipY, ResourceLocation icon, float cooldownBarProgress, boolean creativeMode){
+
+ // Moves the origin if the HUD does not mirror; neatens the rest of the code.
+ if(flipX && !mirrorX) x -= width;
+ if(flipY && !mirrorY) y += height;
+
+ GlStateManager.pushMatrix();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+ GlStateManager.color(1, 1, 1);
+
+ // Spell illustration - this is now done first so it is behind the HUD texture
+ mc.renderEngine.bindTexture(icon);
+
+ int x1 = flipX && mirrorX ? x - spellIconInsetX - SPELL_ICON_SIZE : x + spellIconInsetX;
+ // y is upside-down so this is the other way round
+ int y1 = flipY && mirrorY ? y + spellIconInsetY : y - spellIconInsetY - SPELL_ICON_SIZE;
+
+ DrawingUtils.drawTexturedRect(x1, y1, 0, 0, SPELL_ICON_SIZE, SPELL_ICON_SIZE, SPELL_ICON_SIZE, SPELL_ICON_SIZE);
+
+ // Background of spell hud
+ mc.renderEngine.bindTexture(texture);
+
+ x1 = flipX && mirrorX ? x - width : x;
+ y1 = flipY && mirrorY ? y : y - height;
+ // The 128 here is a uv value, not a dimension, and hence is left as a hardcoded number.
+ // TODO: Since the HUD is wider than it is tall, perhaps the creative mode texture should be in the bottom half instead of the right half?
+ DrawingUtils.drawTexturedFlippedRect(x1, y1, creativeMode ? 128 : 0, 0, width, height, 256, 256, flipX && mirrorX, flipY && mirrorY);
+
+ // Cooldown bar
+ if(!creativeMode && cooldownBarProgress > 0 && (showCooldownWhenFull || cooldownBarProgress < 1)){
+
+ int l = (int)(cooldownBarProgress * cooldownBarLength);
+
+ x1 = flipX && mirrorX ? x - cooldownBarX - (cooldownBarMirrorX ? l : cooldownBarLength) : x + cooldownBarX;
+ y1 = flipY && mirrorY ? y + cooldownBarY : y - cooldownBarY - cooldownBarHeight;
+
+ int u = cooldownBarX; // This doesn't change, even when cooldownBarMirrorX is true, because it should
+ int v = height; // always start with the left-hand in the actual texture file
+
+ DrawingUtils.drawTexturedFlippedRect(x1, y1, u, v, l, cooldownBarHeight, 256, 256, flipX && cooldownBarMirrorX, flipY && cooldownBarMirrorY);
+ }
+
+ GlStateManager.popMatrix();
+
+ // Blend needs to be left enabled here because otherwise the hotbar becomes opaque
+ }
+
+ /**
+ * Draws the text layer of this HUD skin at the given position with the given orientations, with the given
+ * spell name strings.
+ *
+ * @param x The x-coordinate of the corner of the spell HUD. The bottom left corner of the actual texture
+ * will always be at this position, so for example if flipX is false and flipY is true, this will be the corner
+ * of the HUD that is closest to the top left corner of the screen.
+ * @param y The y-coordinate of the corner of the spell HUD; see above.
+ * @param flipX Whether to flip the HUD horizontally.
+ * @param flipY Whether to flip the HUD vertically.
+ * @param prevSpellName The name of the previous spell. This string will be drawn directly using the
+ * {@link MixedFontRenderer}; as such it should be supplied with formatting codes and # characters already
+ * appended.
+ * @param spellName The name of the currently selected spell; see above.
+ * @param nextSpellName The name of the next spell; see above.
+ * @param animationProgress The progress of the spell switching animation, as a fraction between 0 and 1
+ * (inclusive). Positive values indicate switching forwards, negative values indicate switching backwards, and
+ * a value of zero indicates that the spell is not currently being switched.
+ */
+ public void drawText(int x, int y, boolean flipX, boolean flipY, String prevSpellName, String spellName, String nextSpellName, float animationProgress){
+
+ // Moves the origin if the HUD does not mirror; neatens the rest of the code.
+ if(flipX && !mirrorX) x -= width;
+ if(flipY && !mirrorY) y += height;
+
+ FontRenderer font = ClientProxy.mixedFontRenderer; // On this occasion we're client-side so this is OK
+
+ // Position of the selected spell name in normal display, also used for interpolation when animating
+ int x1 = flipX && mirrorX ? x - width : x + textInsetX;
+ // The text is an odd number of pixels high so we need to subtract an extra 1 when not flipped
+ int y1 = flipY && mirrorY ? y + textInsetY - font.FONT_HEIGHT/2 + 2 : y - textInsetY - font.FONT_HEIGHT/2 - 1;
+
+ int maxWidth = width - textInsetX; // Maximum width of the text
+
+ if(animationProgress == 0){ // Normal display
+
+ float xPrev = flipX && mirrorX ? x - width : x + textInsetX - (flipY ? -1 : 1) * cascadeOffsetX;
+ float xNext = flipX && mirrorX ? x - width : x + textInsetX + (flipY ? -1 : 1) * cascadeOffsetX;
+ // Don't ask me why adding 1 to this makes it look more even, it just does!
+ float yPrev = y1 - (cascadeOffsetY + 1); // No need to account for flipY because previous is always above.
+ float yNext = y1 + cascadeOffsetY; // No need to account for flipY because next is always below.
+ float maxWidthPrev = maxWidth + (flipY ? -1 : 1) * cascadeOffsetX;
+ float maxWidthNext = maxWidth - (flipY ? -1 : 1) * cascadeOffsetX;
+ int nextPrevClr = DrawingUtils.makeTranslucent(0xffffff, SPELL_NAME_OPACITY);
+
+ DrawingUtils.drawScaledStringToWidth(font, prevSpellName, xPrev, yPrev, SPELL_NAME_SCALE, nextPrevClr, maxWidthPrev, true, flipX && mirrorX);
+ DrawingUtils.drawScaledStringToWidth(font, spellName, x1, y1, 1, 0xffffffff, maxWidth, true, flipX && mirrorX);
+ DrawingUtils.drawScaledStringToWidth(font, nextSpellName, xNext, yNext, SPELL_NAME_SCALE, nextPrevClr, maxWidthNext, true, flipX && mirrorX);
+
+ }else{ // Switching spells
+
+ boolean reverse = animationProgress < 0;
+ if(reverse) animationProgress = 1 - Math.abs(animationProgress); // Simplest way of reversing the animation
+
+ float xPrev = flipX && mirrorX ? x - width : x + textInsetX - (flipY ? -1 : 1) * cascadeOffsetX * animationProgress;
+ float xNext = flipX && mirrorX ? x - width : x + textInsetX + (flipY ? -1 : 1) * cascadeOffsetX * (1 - animationProgress);
+ float yPrev = y1 - (cascadeOffsetY + 1) * animationProgress; // No need to account for flipY because previous is always above.
+ float yNext = y1 + cascadeOffsetY * (1 - animationProgress); // No need to account for flipY because next is always below.
+ float maxWidthPrev = maxWidth + (flipY ? -1 : 1) * cascadeOffsetX * animationProgress;
+ float maxWidthNext = maxWidth - (flipY ? -1 : 1) * cascadeOffsetX * (1 - animationProgress);
+ float scalePrev = SPELL_NAME_SCALE + (1 - SPELL_NAME_SCALE) * (1 - animationProgress);
+ float scaleNext = SPELL_NAME_SCALE + (1 - SPELL_NAME_SCALE) * (animationProgress);
+ int clrPrev = DrawingUtils.makeTranslucent(0xffffff, SPELL_NAME_OPACITY + (1 - SPELL_NAME_OPACITY) * (1 - animationProgress));
+ int clrNext = DrawingUtils.makeTranslucent(0xffffff, SPELL_NAME_OPACITY + (1 - SPELL_NAME_OPACITY) * animationProgress);
+
+ if(reverse){ // Switching to previous spell
+
+ // Only renders the next spell and the current one
+ DrawingUtils.drawScaledStringToWidth(font, spellName, xPrev, yPrev, scalePrev, clrPrev, maxWidthPrev, true, flipX && mirrorX);
+ DrawingUtils.drawScaledStringToWidth(font, nextSpellName, xNext, yNext, scaleNext, clrNext, maxWidthNext, true, flipX && mirrorX);
+
+ }else{ // Switching to next spell
+
+ // Only renders the previous spell and the current one
+ DrawingUtils.drawScaledStringToWidth(font, prevSpellName, xPrev, yPrev, scalePrev, clrPrev, maxWidthPrev, true, flipX && mirrorX);
+ DrawingUtils.drawScaledStringToWidth(font, spellName, xNext, yNext, scaleNext, clrNext, maxWidthNext, true, flipX && mirrorX);
+
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/EntityNameEntry.java b/src/main/java/electroblob/wizardry/client/gui/config/EntityNameEntry.java
similarity index 90%
rename from src/main/java/electroblob/wizardry/client/EntityNameEntry.java
rename to src/main/java/electroblob/wizardry/client/gui/config/EntityNameEntry.java
index e6b647f5..483c23cb 100644
--- a/src/main/java/electroblob/wizardry/client/EntityNameEntry.java
+++ b/src/main/java/electroblob/wizardry/client/gui/config/EntityNameEntry.java
@@ -1,21 +1,17 @@
-package electroblob.wizardry.client;
-
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.stream.Collectors;
+package electroblob.wizardry.client.gui.config;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.client.config.GuiButtonExt;
-import net.minecraftforge.fml.client.config.GuiEditArray;
-import net.minecraftforge.fml.client.config.GuiEditArrayEntries;
+import net.minecraftforge.fml.client.config.*;
import net.minecraftforge.fml.client.config.GuiEditArrayEntries.StringEntry;
-import net.minecraftforge.fml.client.config.GuiSelectString;
-import net.minecraftforge.fml.client.config.IConfigElement;
import net.minecraftforge.fml.common.registry.EntityEntry;
import net.minecraftforge.fml.common.registry.ForgeRegistries;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+
/**
* [NYI] Intended as a way of choosing entities by name from all those currently registered, within the config file, so
* that users don't have to look up the entity IDs. I can't get this to work correctly at the moment.
diff --git a/src/main/java/electroblob/wizardry/client/GuiConfigWizardry.java b/src/main/java/electroblob/wizardry/client/gui/config/GuiConfigWizardry.java
similarity index 90%
rename from src/main/java/electroblob/wizardry/client/GuiConfigWizardry.java
rename to src/main/java/electroblob/wizardry/client/gui/config/GuiConfigWizardry.java
index 4da979bf..7f76ad1d 100644
--- a/src/main/java/electroblob/wizardry/client/GuiConfigWizardry.java
+++ b/src/main/java/electroblob/wizardry/client/gui/config/GuiConfigWizardry.java
@@ -1,7 +1,4 @@
-package electroblob.wizardry.client;
-
-import java.util.ArrayList;
-import java.util.List;
+package electroblob.wizardry.client.gui.config;
import electroblob.wizardry.Settings;
import electroblob.wizardry.Wizardry;
@@ -15,6 +12,9 @@ import net.minecraftforge.fml.client.config.GuiConfigEntries;
import net.minecraftforge.fml.client.config.GuiConfigEntries.CategoryEntry;
import net.minecraftforge.fml.client.config.IConfigElement;
+import java.util.ArrayList;
+import java.util.List;
+
public class GuiConfigWizardry extends GuiConfig {
public GuiConfigWizardry(GuiScreen parent){
@@ -33,7 +33,8 @@ public class GuiConfigWizardry extends GuiConfig {
configList.add(new DummyCategoryElement("clientConfig", "config." + Wizardry.MODID + ".category." + Settings.CLIENT_CATEGORY, ClientCategory.class));
configList.add(new DummyCategoryElement("spellsConfig", "config." + Wizardry.MODID + ".category." + Settings.SPELLS_CATEGORY, SpellsCategory.class));
configList.add(new DummyCategoryElement("resistancesConfig", "config." + Wizardry.MODID + ".category." + Settings.RESISTANCES_CATEGORY, ResistancesCategory.class));
-
+ configList.add(new DummyCategoryElement("compatibilityConfig", "config." + Wizardry.MODID + ".category." + Settings.COMPATIBILITY_CATEGORY, CompatibilityCategory.class));
+
configList.addAll(new ConfigElement(Wizardry.settings.getConfigCategory(Configuration.CATEGORY_GENERAL)).getChildElements());
return configList;
@@ -41,7 +42,7 @@ public class GuiConfigWizardry extends GuiConfig {
// The reason this system is so convoluted is that it's designed for use with the @Config annotation. The problem is,
// I'm not sure whether that will play well with the load phases. Hmmm...
-
+
public static abstract class CategoryBase extends CategoryEntry {
public CategoryBase(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
@@ -77,6 +78,36 @@ public class GuiConfigWizardry extends GuiConfig {
@Override protected String getCategory() { return Settings.GAMEPLAY_CATEGORY; }
}
+
+ /** Worldgen category of the config gui. */
+ public static class WorldgenCategory extends CategoryBase {
+
+ public WorldgenCategory(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
+ super(owningScreen, owningEntryList, prop);
+ }
+
+ @Override protected String getCategory() { return Settings.WORLDGEN_CATEGORY; }
+ }
+
+ /** Commands category of the config gui. */
+ public static class CommandsCategory extends CategoryBase {
+
+ public CommandsCategory(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
+ super(owningScreen, owningEntryList, prop);
+ }
+
+ @Override protected String getCategory() { return Settings.COMMANDS_CATEGORY; }
+ }
+
+ /** Client category of the config gui. */
+ public static class ClientCategory extends CategoryBase {
+
+ public ClientCategory(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
+ super(owningScreen, owningEntryList, prop);
+ }
+
+ @Override protected String getCategory() { return Settings.CLIENT_CATEGORY; }
+ }
/** Spells category of the config gui. */
public static class SpellsCategory extends CategoryBase {
@@ -97,34 +128,14 @@ public class GuiConfigWizardry extends GuiConfig {
@Override protected String getCategory() { return Settings.RESISTANCES_CATEGORY; }
}
-
- /** Worldgen category of the config gui. */
- public static class WorldgenCategory extends CategoryBase {
-
- public WorldgenCategory(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
- super(owningScreen, owningEntryList, prop);
- }
-
- @Override protected String getCategory() { return Settings.WORLDGEN_CATEGORY; }
- }
-
- /** Client category of the config gui. */
- public static class ClientCategory extends CategoryBase {
-
- public ClientCategory(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
- super(owningScreen, owningEntryList, prop);
- }
-
- @Override protected String getCategory() { return Settings.CLIENT_CATEGORY; }
- }
-
+
/** Commands category of the config gui. */
- public static class CommandsCategory extends CategoryBase {
-
- public CommandsCategory(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
+ public static class CompatibilityCategory extends CategoryBase {
+
+ public CompatibilityCategory(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
super(owningScreen, owningEntryList, prop);
}
-
- @Override protected String getCategory() { return Settings.COMMANDS_CATEGORY; }
+
+ @Override protected String getCategory() { return Settings.COMPATIBILITY_CATEGORY; }
}
}
diff --git a/src/main/java/electroblob/wizardry/client/gui/config/GuiSelectHUDSkin.java b/src/main/java/electroblob/wizardry/client/gui/config/GuiSelectHUDSkin.java
new file mode 100644
index 00000000..985d1594
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/config/GuiSelectHUDSkin.java
@@ -0,0 +1,119 @@
+package electroblob.wizardry.client.gui.config;
+
+import com.google.common.collect.Lists;
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.gui.GuiSpellDisplay;
+import electroblob.wizardry.client.gui.GuiSpellDisplay.Skin;
+import electroblob.wizardry.registry.Spells;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.resources.I18n;
+import net.minecraftforge.fml.client.config.GuiSelectString;
+import net.minecraftforge.fml.client.config.IConfigElement;
+
+import javax.annotation.Nullable;
+import java.util.Map;
+
+public class GuiSelectHUDSkin extends GuiSelectString {
+
+ public GuiSelectHUDSkin(GuiScreen parentScreen, IConfigElement configElement, int slotIndex, Map selectableValues, Object currentValue, boolean enabled){
+ super(parentScreen, configElement, slotIndex, selectableValues, currentValue, enabled);
+ }
+
+ @Override
+ public void initGui(){
+ super.initGui();
+ setEntryListDimensions();
+ }
+
+ @Override
+ protected void actionPerformed(GuiButton button){
+ super.actionPerformed(button);
+ setEntryListDimensions(); // Stops the entry list from resizing when a button is pressed
+ }
+
+ private void setEntryListDimensions(){
+ this.entryList.setDimensions(150, height, 43, height-43);
+ this.entryList.left = 10;
+ this.entryList.maxEntryWidth = 120;
+ this.entryList.headerPadding = 5;
+ }
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks){
+
+ super.drawScreen(mouseX, mouseY, partialTicks);
+
+ GlStateManager.disableLighting();
+
+ if(this.currentValue instanceof String){
+
+ this.drawString(this.fontRenderer, I18n.format("config." + Wizardry.MODID + ".spell_hud_skin.preview"), 170, 44, 0xffffff);
+
+ int previewLeft = 170;
+ int previewRight = width-10;
+ int previewTop = 60;
+ int previewBottom = height-43;
+
+ this.drawGradientRect(previewLeft, previewTop, previewRight, previewBottom, 0x88000000, 0x88000000);
+
+ int previewBorder = 10;
+
+ Skin skin = GuiSpellDisplay.getSkin((String)this.currentValue);
+
+ float scale = Math.min((previewRight - previewLeft - 2*previewBorder)/(float)skin.getWidth(),
+ (previewBottom - previewTop - 2*previewBorder)/(float)skin.getHeight());
+
+ float x = (previewLeft + previewRight)/2 - (skin.getWidth()*scale)/2;
+ float y = (previewBottom + previewTop)/2 + (skin.getHeight()*scale)/2;
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.scale(scale, scale, scale);
+
+ skin.drawBackground((int)(x/scale), (int)(y/scale), false, false,
+ Spells.magic_missile.getIcon(), 0.6f, false);
+
+ skin.drawText((int)(x/scale), (int)(y/scale), false, false,
+ Spells.none.getDisplayNameWithFormatting(),
+ Spells.magic_missile.getDisplayNameWithFormatting(),
+ Spells.none.getDisplayNameWithFormatting(), 0);
+
+ GlStateManager.popMatrix();
+
+ Skin hovered = getHoveredSkin(mouseX, mouseY);
+
+ if(hovered != null){
+ this.drawToolTip(Lists.newArrayList("\u00A7a" + hovered.getName(), "\u00A7e" + hovered.getDescription()),
+ mouseX, mouseY);
+ }
+ }
+
+ GlStateManager.enableLighting();
+ }
+
+ /** Returns the skin corresponding to the list entry being hovered over, or null if there is none. */
+ @Nullable
+ private Skin getHoveredSkin(int mouseX, int mouseY){
+
+ int index = this.entryList.getSlotIndexFromScreenCoords(mouseX, mouseY);
+
+ if(index >= 0 && index <= this.entryList.listEntries.size() && mouseY <= this.entryList.bottom){
+
+ Object object = entryList.getListEntry(index).getValue();
+
+ if(object instanceof String){
+ return GuiSpellDisplay.getSkin((String)object);
+ }
+ }
+
+ return null;
+ }
+
+ @Override // Stops the world being visible behind the GUI when configuring from within a world
+ public void drawWorldBackground(int tint){
+ this.drawBackground(tint);
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/config/NamedBooleanEntry.java b/src/main/java/electroblob/wizardry/client/gui/config/NamedBooleanEntry.java
new file mode 100644
index 00000000..0e9fd5ed
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/config/NamedBooleanEntry.java
@@ -0,0 +1,93 @@
+package electroblob.wizardry.client.gui.config;
+
+import net.minecraft.client.resources.I18n;
+import net.minecraftforge.fml.client.config.GuiConfig;
+import net.minecraftforge.fml.client.config.GuiConfigEntries;
+import net.minecraftforge.fml.client.config.GuiUtils;
+import net.minecraftforge.fml.client.config.IConfigElement;
+
+/**
+ * Same as {@link net.minecraftforge.fml.client.config.GuiConfigEntries.BooleanEntry}, but instead of simply
+ * displaying 'true' or 'false', allows the two display strings to be specified in the lang file.
+ */
+// BooleanEntry's constructors are private, so I had to copy the whole goddamn class to change one method. Thanks Forge.
+public class NamedBooleanEntry extends GuiConfigEntries.ButtonEntry {
+
+ protected final boolean beforeValue;
+ protected boolean currentValue;
+
+ private static final String DEFAULT_KEY = "config.ebwizardry.generic";
+
+ public NamedBooleanEntry(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement configElement){
+ super(owningScreen, owningEntryList, configElement);
+ this.beforeValue = Boolean.valueOf(configElement.get().toString());
+ this.currentValue = beforeValue;
+ this.btnValue.enabled = enabled();
+ updateValueButtonText();
+ }
+
+ // This is the only method that's any different
+ @Override
+ public void updateValueButtonText(){
+
+ String langKey = configElement.getLanguageKey() + "." + currentValue;
+ this.btnValue.displayString = I18n.format(langKey);
+ // If the key is unspecified, it defaults to the generic 'Enabled'/'Disabled' keys and adds a red/green colour
+ if(this.btnValue.displayString.equals(langKey)){
+ this.btnValue.displayString = I18n.format(DEFAULT_KEY + "." + currentValue);
+ btnValue.packedFGColour = currentValue ? GuiUtils.getColorCode('a', true) : GuiUtils.getColorCode('c', true);
+ }
+ }
+
+ // Everything from here down is the same as BooleanEntry
+
+ @Override
+ public void valueButtonPressed(int slotIndex){
+ if(enabled()) currentValue = !currentValue;
+ }
+
+ @Override
+ public boolean isDefault(){
+ return currentValue == Boolean.valueOf(configElement.getDefault().toString());
+ }
+
+ @Override
+ public void setToDefault(){
+ if(enabled()){
+ currentValue = Boolean.valueOf(configElement.getDefault().toString());
+ updateValueButtonText();
+ }
+ }
+
+ @Override
+ public boolean isChanged(){
+ return currentValue != beforeValue;
+ }
+
+ @Override
+ public void undoChanges(){
+ if(enabled()){
+ currentValue = beforeValue;
+ updateValueButtonText();
+ }
+ }
+
+ @Override
+ public boolean saveConfigElement(){
+ if(enabled() && isChanged()){
+ configElement.set(currentValue);
+ return configElement.requiresMcRestart();
+ }
+ return false;
+ }
+
+ @Override
+ public Boolean getCurrentValue(){
+ return currentValue;
+ }
+
+ @Override
+ public Boolean[] getCurrentValues(){
+ return new Boolean[]{getCurrentValue()};
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/config/SpellHUDSkinChooserEntry.java b/src/main/java/electroblob/wizardry/client/gui/config/SpellHUDSkinChooserEntry.java
new file mode 100644
index 00000000..e2ddb3ed
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/config/SpellHUDSkinChooserEntry.java
@@ -0,0 +1,35 @@
+package electroblob.wizardry.client.gui.config;
+
+import electroblob.wizardry.client.gui.GuiSpellDisplay;
+import net.minecraftforge.client.gui.ForgeGuiFactory.ForgeConfigGui.ModIDEntry;
+import net.minecraftforge.fml.client.config.GuiConfig;
+import net.minecraftforge.fml.client.config.GuiConfigEntries;
+import net.minecraftforge.fml.client.config.GuiConfigEntries.SelectValueEntry;
+import net.minecraftforge.fml.client.config.IConfigElement;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+
+/**
+ * Custom config GUI for spell HUD skin selection; displays a list of all the loaded skins and a preview of the currently
+ * selected skin. based off of {@link ModIDEntry} from Forge.
+ */
+public class SpellHUDSkinChooserEntry extends SelectValueEntry {
+
+ public SpellHUDSkinChooserEntry(GuiConfig owningScreen, GuiConfigEntries owningEntryList, IConfigElement prop){
+ super(owningScreen, owningEntryList, prop, getSelectableValues());
+ if(this.selectableValues.size() == 0) this.btnValue.enabled = false;
+ }
+
+ private static Map getSelectableValues(){
+ return GuiSpellDisplay.getSkins().entrySet().stream().collect(Collectors.toMap(Entry::getKey,
+ e -> e.getValue().getName()));
+ }
+
+ @Override // Copied from superclass to use custom child screen GUI class
+ public void valueButtonPressed(int slotIndex){
+ mc.displayGuiScreen(new GuiSelectHUDSkin(this.owningScreen, configElement, slotIndex, selectableValues, currentValue, enabled()));
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/Contents.java b/src/main/java/electroblob/wizardry/client/gui/handbook/Contents.java
new file mode 100644
index 00000000..1853e5a1
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/Contents.java
@@ -0,0 +1,194 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.util.JsonUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Instances of this class represent tables of contents in the wizard's handbook. Each {@link Section} can have a
+ * single table of contents, which can reference any other sections in the handbook (though it is normal to list
+ * top-level sections in a main contents and have subsections listed in their respective parent sections' contents).
+ *
+ * This class handles JSON parsing, formatting and drawing of the contents itself, working on a line-by-line basis
+ * (as opposed to sections, which work on a page-by-page basis). It also stores its own list of buttons.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.2
+ */
+class Contents {
+
+ // Final fields are mandatory, the rest are optional
+ final String id;
+ final Section section;
+ private boolean hyperlinks = true;
+ private boolean pageNumbers = true;
+ private String separator = ".";
+ // Derived fields, not specifically defined in JSON
+ private int startPage;
+ private int startLine;
+ private final List> buttons;
+
+ private final List entries;
+
+ private List visibleEntries;
+
+ private Contents(String id, Section section){
+ this.id = id;
+ this.section = section;
+ this.entries = new ArrayList<>();
+ this.buttons = new ArrayList<>();
+ this.visibleEntries = new ArrayList<>();
+ }
+
+ /** Returns an unmodifiable, flattened collection of all the buttons in this contents. */
+ Collection getButtons(){
+ return WizardryUtilities.flatten(buttons);
+ }
+
+ void addEntry(Section section){
+ entries.add(section);
+ }
+
+ /**
+ * Draws this contents for the given double-page spread and shows/hides buttons accordingly. Will draw nothing
+ * if the given page is outside of this contents.
+ *
+ * @param font The font renderer object.
+ * @param doublePage The index of the double-page to be drawn.
+ * @param left The x coordinate of the left side of the GUI.
+ * @param top The y coordinate of the top of the GUI.
+ */
+ void draw(FontRenderer font, int doublePage, int left, int top){
+
+ // Show/hide buttons
+
+ int i = 0;
+
+ for(List list : buttons){
+ final int i1 = i++;
+ list.forEach(b -> b.visible = GuiWizardHandbook.singleToDoublePage(startPage + i1) == doublePage);
+ }
+
+ if(!pageNumbers) return; // No page numbers means only the buttons are drawn
+
+ // FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
+ final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
+
+ int leftIndex = GuiWizardHandbook.doubleToSinglePage(doublePage, false);
+ // Relative indices of the pages to be rendered - often these will be outside the section entirely
+ int[] visiblePages = {leftIndex - startPage, leftIndex - startPage + 1};
+
+ for(int page : visiblePages){
+
+ if(page >= 0 && page < visibleEntries.size() / maxLineNumber + 1){
+
+ int x = left + (GuiWizardHandbook.isRightPage(startPage + page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X);
+ int y = top + GuiWizardHandbook.TEXT_INSET_Y + startLine * font.FONT_HEIGHT;
+
+ for(Section entry : this.visibleEntries){
+
+ if(entry.isUnlocked()){
+
+ int nameWidth = font.getStringWidth(entry.title);
+
+ String dotsAndNumber = " " + entry.startPage;
+
+ while(font.getStringWidth(dotsAndNumber) < GuiWizardHandbook.PAGE_WIDTH - nameWidth - 2){
+ dotsAndNumber = separator + dotsAndNumber;
+ }
+
+ font.drawString(dotsAndNumber, x + GuiWizardHandbook.PAGE_WIDTH - font.getStringWidth(dotsAndNumber), y, DrawingUtils.BLACK, false);
+
+ if(!hyperlinks) font.drawString(entry.title, x, y, DrawingUtils.BLACK, false);
+
+ y += font.FONT_HEIGHT;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Called on GUI load to format the section and all subsections, contents tables and other elements. Does not
+ * perform any actual drawing.
+ *
+ * @param font The font renderer object, for measurement purposes.
+ * @param startPage The index of the first page (single side, not double-page) of this section.
+ * @param startLine The index of the first line of this contents.
+ * @param left The x coordinate of the left side of the GUI.
+ * @param top The y coordinate of the top of the GUI.
+ * @return The number of lines this contents takes up.
+ * @throws JsonSyntaxException if at any point the formatting is found to be invalid.
+ */
+ int format(FontRenderer font, int startPage, int startLine, int left, int top){
+
+ this.buttons.clear();
+
+ this.visibleEntries = new ArrayList<>(entries); // Need to copy the collection first!
+
+ this.visibleEntries.removeIf(s -> !s.isUnlocked());
+
+ if(hyperlinks){
+
+ // FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
+ final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
+
+ this.startPage = startPage;
+ this.startLine = startLine;
+
+ List list = new ArrayList<>(maxLineNumber);
+
+ for(Section entry : this.visibleEntries){
+
+ int x = GuiWizardHandbook.isRightPage(startPage) ? left + GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : left + GuiWizardHandbook.TEXT_INSET_X;
+ int y = top + GuiWizardHandbook.TEXT_INSET_Y + startLine * font.FONT_HEIGHT;
+
+ list.add(new GuiButtonHyperlink.Internal(0, x, y, font, entry.title, entry, 0, "", maxLineNumber-startLine, GuiWizardHandbook.isRightPage(startPage)));
+
+ startLine++;
+
+ if(startLine == maxLineNumber){
+ startLine = 0;
+ startPage++;
+ buttons.add(list);
+ list = new ArrayList<>(maxLineNumber); // If there are no more entries this will be discarded anyway
+ }
+ }
+
+ buttons.add(list);
+ }
+
+ // Returning this is kind of trivial at the moment but if we ever wanted to add a header or something,
+ // it would be more useful.
+ return visibleEntries.size();
+ }
+
+ /**
+ * Parses the given JSON object and constructs a new {@code Contents} from it, setting all the relevant fields
+ * and references.
+ *
+ * @param parent The parent section for this contents.
+ * @param json A JSON object representing the contents to be constructed. This must contain at least an "id"
+ * string.
+ * @return The resulting {@code Contents} object.
+ * @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
+ */
+ static Contents fromJson(Section parent, JsonObject json){
+
+ Contents contents = new Contents(JsonUtils.getString(json, "id"), parent);
+
+ contents.hyperlinks = JsonUtils.getBoolean(json, "hyperlinks", true);
+ contents.pageNumbers = JsonUtils.getBoolean(json, "page_numbers", true);
+ contents.separator = JsonUtils.getString(json, "separator", ".");
+
+ return contents;
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/CraftingRecipe.java b/src/main/java/electroblob/wizardry/client/gui/handbook/CraftingRecipe.java
new file mode 100644
index 00000000..a7b32212
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/CraftingRecipe.java
@@ -0,0 +1,226 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import com.google.common.collect.Streams;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.client.DrawingUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.RenderHelper;
+import net.minecraft.client.renderer.RenderItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.crafting.CraftingManager;
+import net.minecraft.item.crafting.IRecipe;
+import net.minecraft.item.crafting.Ingredient;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+
+import java.util.*;
+
+class CraftingRecipe {
+
+ static final int BORDER = 7;
+ static final int TEXTURE_INSET_X = 40, TEXTURE_INSET_Y = 190;
+ static final int WIDTH = 121, HEIGHT = 66;
+
+ // Final fields are mandatory, the rest are optional
+ private final ResourceLocation[] locations;
+ // Derived fields, not specifically defined in JSON
+ private List recipes;
+ private final Set instances = new HashSet<>();
+
+ private CraftingRecipe(ResourceLocation[] locations){
+ this.locations = locations;
+ }
+
+ /**
+ * Adds an instance of this recipe to the list.
+ *
+ * @param page The index of the single page this image is on.
+ * @param x The x-coordinate of the top-left corner of the image, relative to the top-left corner of the GUI.
+ * @param y The y-coordinate of the top-left corner of the image, relative to the top-left corner of the GUI.
+ */
+ void addInstance(int page, int x, int y){
+ instances.add(new int[]{page, x, y});
+ }
+
+ /** Removes all instances of this recipe from the list. */
+ void clearInstances(){
+ instances.clear();
+ }
+
+ /** Called on GUI open to load the actual recipe object from the registry. This cannot be done on JSON load since
+ * the recipes aren't necessarily loaded at that point. */
+ void load(){
+
+ recipes = new ArrayList<>(locations.length);
+
+ for(ResourceLocation location : locations){
+
+ IRecipe recipe = CraftingManager.getRecipe(location);
+ if(recipe == null) throw new JsonSyntaxException("No such recipe: " + location);
+ recipes.add(recipe);
+ }
+ }
+
+ /**
+ * Draws all instances of this recipe that are located on the given double-page spread.
+ *
+ * @param font The font renderer object.
+ * @param itemRenderer The item renderer object.
+ * @param doublePage The double-page index of the page to be drawn.
+ * @param left The x coordinate of the left side of the GUI.
+ * @param top The y coordinate of the top of the GUI.
+ */
+ void draw(FontRenderer font, RenderItem itemRenderer, int doublePage, int left, int top){
+
+ int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
+
+ for(int[] instance : instances){
+ if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
+ renderCraftingRecipe(font, itemRenderer, left + instance[1], top + instance[2], recipes.get(index % recipes.size()));
+ }
+ }
+ }
+
+ /**
+ * Draws the tooltips for all instances of this recipe that are located on the given double-page spread. This has to
+ * be done separately so that the tooltips are on top of everything else.
+ *
+ * @param itemRenderer The item renderer object.
+ * @param doublePage The double-page index of the page to be drawn.
+ * @param left The x coordinate of the left side of the GUI.
+ * @param top The y coordinate of the top of the GUI.
+ */
+ void drawTooltips(GuiWizardHandbook gui, FontRenderer font, RenderItem itemRenderer, int doublePage, int left, int top, int mouseX, int mouseY){
+
+ int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
+
+ for(int[] instance : instances){
+ if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
+ renderCraftingTooltips(gui, itemRenderer, left + instance[1], top + instance[2], mouseX, mouseY, recipes.get(index % recipes.size()));
+ }
+ }
+ }
+
+ /**
+ * Parses the given JSON object and constructs a new {@code Image} from it, setting all the relevant fields
+ * and references.
+ *
+ * @param json A JSON object representing the image to be constructed. This must contain at least a "locations"
+ * string.
+ * @return The resulting {@code Image} object.
+ * @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
+ */
+ static CraftingRecipe fromJson(JsonObject json){
+
+ ResourceLocation[] locations = Streams.stream(JsonUtils.getJsonArray(json, "locations"))
+ .map(je -> new ResourceLocation(je.getAsString())).toArray(ResourceLocation[]::new);
+ return new CraftingRecipe(locations);
+ }
+
+ static void populate(Map map, JsonObject json){
+
+ JsonObject sectionsObject = JsonUtils.getJsonObject(json, "recipes");
+
+ // Need to iterate over these since we don't know what they're called or how many there are
+ for(Map.Entry entry : sectionsObject.entrySet()){
+
+ String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
+
+ CraftingRecipe recipe = fromJson(entry.getValue().getAsJsonObject());
+ map.put(key, recipe);
+ }
+ }
+
+ private static void renderCraftingRecipe(FontRenderer font, RenderItem itemRenderer, int x, int y, IRecipe recipe){
+
+ ItemStack result = recipe.getRecipeOutput();
+
+ GlStateManager.color(1, 1, 1, 1);
+ Minecraft.getMinecraft().renderEngine.bindTexture(GuiWizardHandbook.texture);
+
+ DrawingUtils.drawTexturedRect(x, y, TEXTURE_INSET_X, TEXTURE_INSET_Y, WIDTH, HEIGHT, GuiWizardHandbook.TEXTURE_WIDTH, GuiWizardHandbook.TEXTURE_HEIGHT);
+
+ GlStateManager.pushMatrix();
+ RenderHelper.enableGUIStandardItemLighting();
+ GlStateManager.disableLighting();
+ GlStateManager.enableRescaleNormal();
+ GlStateManager.enableColorMaterial();
+ itemRenderer.zLevel = 100.0F;
+
+ int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
+
+ int i = 0;
+
+ for(Ingredient ingredient : recipe.getIngredients()){
+
+ if(ingredient != Ingredient.EMPTY){
+ ItemStack stack = ingredient.getMatchingStacks()[index % ingredient.getMatchingStacks().length];
+ if(!stack.isEmpty()){
+ itemRenderer.renderItemAndEffectIntoGUI(stack, x + BORDER + 18 * (i%3), y + BORDER + 18 * (i/3));
+ itemRenderer.renderItemOverlays(font, stack, x + BORDER + 18 * (i%3), y + BORDER + 18 * (i/3));
+ }
+ }
+
+ i++;
+ }
+
+ if(!result.isEmpty()){
+ itemRenderer.renderItemAndEffectIntoGUI(result, x + BORDER + 86, y + BORDER + 18);
+ itemRenderer.renderItemOverlays(font, result, x + BORDER + 86, y + BORDER + 18);
+ }
+
+ GlStateManager.popMatrix();
+ GlStateManager.enableDepth();
+ GlStateManager.disableColorMaterial();
+ itemRenderer.zLevel = 0.0F;
+ RenderHelper.disableStandardItemLighting();
+
+ }
+
+ private static void renderCraftingTooltips(GuiWizardHandbook gui, RenderItem itemRenderer, int x, int y, int mouseX, int mouseY, IRecipe recipe){
+
+ ItemStack result = recipe.getRecipeOutput();
+
+ GlStateManager.pushMatrix();
+ RenderHelper.enableGUIStandardItemLighting();
+ GlStateManager.disableLighting();
+ GlStateManager.enableRescaleNormal();
+ GlStateManager.enableColorMaterial();
+ itemRenderer.zLevel = 0.0F;
+
+ int index = (int)(Minecraft.getSystemTime() % Integer.MAX_VALUE)/2000;
+
+ int i = 0;
+
+ for(Ingredient ingredient : recipe.getIngredients()){
+
+ if(ingredient != Ingredient.EMPTY){
+ ItemStack stack = ingredient.getMatchingStacks()[index % ingredient.getMatchingStacks().length];
+ if(!stack.isEmpty() && isPointInRegion(x + BORDER + 18 * (i%3), y + BORDER + 18 * (i/3), 16, 16, mouseX, mouseY)){
+ gui.renderToolTip(stack, mouseX, mouseY);
+ }
+ }
+
+ i++;
+ }
+
+ if(!result.isEmpty() && isPointInRegion(x + BORDER + 86, y + BORDER + 18, 16, 16, mouseX, mouseY)){
+ gui.renderToolTip(result, mouseX, mouseY);
+ }
+
+ GlStateManager.popMatrix();
+ GlStateManager.enableDepth();
+ GlStateManager.disableColorMaterial();
+ RenderHelper.disableStandardItemLighting();
+
+ }
+
+ private static boolean isPointInRegion(int left, int top, int width, int height, int mouseX, int mouseY){
+ return mouseX >= left - 1 && mouseX < left + width + 1 && mouseY >= top - 1 && mouseY < top + height + 1;
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/GuiButtonHyperlink.java b/src/main/java/electroblob/wizardry/client/gui/handbook/GuiButtonHyperlink.java
new file mode 100644
index 00000000..d5a48d64
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/GuiButtonHyperlink.java
@@ -0,0 +1,227 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.registry.WizardrySounds;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.PositionedSoundRecord;
+import net.minecraft.client.audio.SoundHandler;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.text.ITextComponent;
+import net.minecraft.util.text.TextComponentString;
+import net.minecraft.util.text.TextFormatting;
+import net.minecraft.util.text.event.ClickEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class GuiButtonHyperlink extends GuiButton {
+
+ public static final String URL_REGEX = "^((https?|ftp)://|(www|ftp)\\.)?[a-z0-9-]+(\\.[a-z0-9-]+)+([/?].*)?$";
+
+ /** Pulse period of links to new sections, in milliseconds. */
+ private static final float PULSATION_PERIOD = 1500;
+
+ final int indent;
+ final List lines;
+ final int linesLeft;
+
+ GuiButtonHyperlink(int id, int x, int y, FontRenderer font, String text, int indent, String suffix, int linesLeft, boolean rightPage){
+
+ super(id, x, y, font.getStringWidth(text), font.FONT_HEIGHT, text);
+
+ // Sometimes a link has punctuation or something after it that causes it to wrap onto a new line
+ String linkWithSuffix = text + suffix;
+
+ // If the string won't fit any words at the end of the current line, treat it as if we started a new line
+ if(font.getStringWidth(linkWithSuffix.split("\\s")[0]) > GuiWizardHandbook.PAGE_WIDTH - indent){
+ indent = 0;
+ this.y += font.FONT_HEIGHT;
+ }
+
+ this.indent = indent; // Assigned here in case it was corrected above
+ this.linesLeft = linesLeft;
+
+ String line1 = font.listFormattedStringToWidth(linkWithSuffix, GuiWizardHandbook.PAGE_WIDTH - indent).get(0);
+ // Without trim(), there will be at least 1 leading space due to the custom wrapping
+ String remainder = linkWithSuffix.substring(line1.length()).trim();
+
+ // ... then wrap the rest to the normal width.
+ lines = new ArrayList<>();
+ lines.add(line1);
+ // Some links are only one line, if this wasn't checked they would cause a StackOverflowError
+ if(!remainder.isEmpty()) lines.addAll(font.listFormattedStringToWidth(remainder, GuiWizardHandbook.PAGE_WIDTH));
+
+ // Removes the suffix if it exists (ugly as heck, but it works)
+ if(!suffix.isEmpty()){
+ for(int i=lines.size()-1; i>=0; i--){
+ String line = lines.get(i);
+ if(suffix.endsWith(line)){
+ lines.remove(i);
+ }else if(line.endsWith(suffix)){
+ lines.set(i, line.substring(0, line.length() - suffix.length()));
+ break;
+ }
+ }
+ }
+
+ // Remove any lines that overflowed onto the next double-page
+ if(rightPage){
+ while(lines.size() > linesLeft) lines.remove(lines.size() - 1);
+ }
+ }
+
+ public boolean isHovered(net.minecraft.client.gui.FontRenderer font, int mouseX, int mouseY){
+
+ int i = 0;
+
+ for(String line : lines){
+
+ int l = x;
+ if(i == 0) l += indent;
+
+ int t = y + font.FONT_HEIGHT * i;
+
+ if(i > linesLeft){
+ l = l + GuiWizardHandbook.GUI_WIDTH - 2 * GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH;
+ t -= GuiWizardHandbook.PAGE_HEIGHT - (GuiWizardHandbook.PAGE_HEIGHT % font.FONT_HEIGHT);
+ }
+
+ if(mouseX >= l && mouseY >= t && mouseX < l + font.getStringWidth(line) && mouseY < t + font.FONT_HEIGHT){
+ return true;
+ }
+
+ i++;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY){
+ return this.enabled && this.visible && isHovered(minecraft.fontRenderer, mouseX, mouseY);
+ }
+
+ @Override
+ public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
+
+ if(this.visible){
+
+ this.hovered = isHovered(minecraft.fontRenderer, mouseX, mouseY);
+
+ int i = 0;
+
+ for(String line : lines){
+
+ int l = x;
+ if(i == 0) l += indent;
+
+ int t = y + minecraft.fontRenderer.FONT_HEIGHT * i;
+
+ if(i > linesLeft){
+ l = l + GuiWizardHandbook.GUI_WIDTH - 2 * GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH;
+ t -= GuiWizardHandbook.PAGE_HEIGHT - (GuiWizardHandbook.PAGE_HEIGHT % minecraft.fontRenderer.FONT_HEIGHT);
+ }
+
+ minecraft.fontRenderer.drawString(line, l, t, getColour());
+
+ i++;
+ }
+ }
+ }
+
+ protected int getColour(){
+ return hovered ? GuiWizardHandbook.colours.get("highlight") : GuiWizardHandbook.colours.get("hyperlink");
+ }
+
+ /**
+ * Creates a new hyperlink button from the given arguments, automatically differentiating between URLs and sections.
+ * @param x The x position of the button
+ * @param y The y position of the button
+ * @param font A reference to the FontRenderer object
+ * @param upToLink The paragraph (as a list of lines) up to the link, used to determine positioning and word wrap
+ * @param arguments The link arguments - that is, everything between the two @ signs, split by spaces
+ * @param suffix The text directly after the link, up to the first whitespace; used for word wrap. Usually this is
+ * either empty or contains a single punctuation mark.
+ * @return The resulting button
+ * @throws IllegalArgumentException if the given argument array is empty or contains more than 2 arguments
+ * @throws JsonSyntaxException if the specified link target is not a URL or a valid section ID
+ */
+ public static GuiButtonHyperlink create(int x, int y, FontRenderer font, List upToLink, String[] arguments, String suffix, int linesLeft, boolean rightPage){
+
+ if(arguments.length == 0 || arguments.length > 2) throw new IllegalArgumentException("Incorrect array length!");
+
+ GuiButtonHyperlink button;
+
+ if(arguments[0].matches(URL_REGEX)){
+
+ button = new GuiButtonHyperlink.External(0, x, y, font, arguments[arguments.length - 1], arguments[0],
+ font.getStringWidth(upToLink.get(upToLink.size() - 1)), suffix, linesLeft, rightPage);
+
+ }else{
+
+ Section target = GuiWizardHandbook.sections.get(arguments[0]);
+
+ if(target == null) throw new JsonSyntaxException("Hyperlink points to nonexistent section id " + arguments[0]);
+
+ button = new GuiButtonHyperlink.Internal(0, x, y, font, arguments[arguments.length - 1],
+ target, font.getStringWidth(upToLink.get(upToLink.size() - 1)), suffix, linesLeft, rightPage);
+ }
+
+ return button;
+ }
+
+ static class Internal extends GuiButtonHyperlink {
+
+ final Section target;
+
+ Internal(int id, int x, int y, FontRenderer font, String text, Section target, int indent, String suffix, int linesLeft, boolean rightPage){
+ super(id, x, y, font, text, indent, suffix, linesLeft, rightPage);
+ this.target = target;
+ }
+
+ @Override
+ public boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY){
+ if(!target.isUnlocked()) return false;
+ return super.mousePressed(minecraft, mouseX, mouseY);
+ }
+
+ @Override
+ public void playPressSound(SoundHandler soundHandler){
+ soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
+ }
+
+ @Override
+ protected int getColour(){
+
+ if(!target.isUnlocked()) return GuiWizardHandbook.colours.get("text");
+
+ if(!hovered && target.isNew() && !Minecraft.getMinecraft().player.isCreative()){
+
+ int c = GuiWizardHandbook.colours.get("new_section");
+ int d = GuiWizardHandbook.colours.get("hyperlink");
+ float f = (MathHelper.sin((Minecraft.getSystemTime() % PULSATION_PERIOD) / PULSATION_PERIOD * 2 * (float)Math.PI) + 1) / 2f;
+
+ return DrawingUtils.mix(c, d, f);
+ }
+
+ return super.getColour();
+ }
+
+ }
+
+ static class External extends GuiButtonHyperlink {
+
+ final ITextComponent link;
+
+ External(int id, int x, int y, FontRenderer font, String text, String url, int indent, String suffix, int linesLeft, boolean rightPage){
+ super(id, x, y, font, text, indent, suffix, linesLeft, rightPage);
+ this.link = new TextComponentString(text);
+ link.getStyle().setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, url)).setColor(TextFormatting.DARK_BLUE);
+ }
+
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/GuiButtonTurnPage.java b/src/main/java/electroblob/wizardry/client/gui/handbook/GuiButtonTurnPage.java
new file mode 100644
index 00000000..662e71a1
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/GuiButtonTurnPage.java
@@ -0,0 +1,61 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.registry.WizardrySounds;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.PositionedSoundRecord;
+import net.minecraft.client.audio.SoundHandler;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.util.ResourceLocation;
+
+//@SideOnly(Side.CLIENT)
+class GuiButtonTurnPage extends GuiButton {
+
+ static final int WIDTH = 20;
+ static final int HEIGHT = 12;
+
+ enum Type {
+
+ NEXT_PAGE(0, 196),
+ PREVIOUS_PAGE(0, 208),
+ NEXT_SECTION(0, 220),
+ PREVIOUS_SECTION(0, 232),
+ CONTENTS(0, 244);
+
+ private final int u, v;
+
+ Type(int u, int v){
+ this.u = u;
+ this.v = v;
+ }
+ }
+
+ public final Type type;
+
+ private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
+
+ public GuiButtonTurnPage(int id, int x, int y, Type type){
+ super(id, x, y, WIDTH, HEIGHT, "");
+ this.type = type;
+ }
+
+ @Override
+ public void playPressSound(SoundHandler soundHandler){
+ soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
+ }
+
+ @Override
+ public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partialTicks){
+
+ if(this.visible){
+
+ boolean flag = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height;
+ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
+ minecraft.getTextureManager().bindTexture(texture);
+
+ DrawingUtils.drawTexturedRect(this.x, this.y, flag ? type.u + width : type.u, type.v, width, height, 512, 256);
+ }
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/GuiWizardHandbook.java b/src/main/java/electroblob/wizardry/client/gui/handbook/GuiWizardHandbook.java
new file mode 100644
index 00000000..dcb54387
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/GuiWizardHandbook.java
@@ -0,0 +1,564 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.ClientProxy;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.client.gui.GuiButtonInvisible;
+import electroblob.wizardry.client.gui.handbook.GuiButtonTurnPage.Type;
+import electroblob.wizardry.constants.Constants;
+import electroblob.wizardry.constants.Element;
+import electroblob.wizardry.constants.Tier;
+import electroblob.wizardry.packet.PacketRequestAdvancementSync;
+import electroblob.wizardry.packet.WizardryPacketHandler;
+import electroblob.wizardry.registry.WizardrySounds;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.audio.PositionedSoundRecord;
+import net.minecraft.client.audio.SoundHandler;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.gui.GuiScreen;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.resources.IResource;
+import net.minecraft.client.resources.IResourceManager;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+import org.lwjgl.input.Keyboard;
+
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.*;
+
+/**
+ * GUI class for the wizard's handbook. Like any GUI class, this is instantiated each time the book is opened. As of
+ * Wizardry 4.2, the handbook text is defined as a JSON file rather than a plain text file, and is loaded only on
+ * resource pack reload, rather than every time the book is opened. This means all the data structures (sections, images,
+ * etc.) are built before the GUI instance exists at all. However, since some things depend on positioning, these have to
+ * be initialised on GUI creation. (Previously, everything was done on GUI load)
+ *
+ * @author Electroblob
+ * @since Wizardry 1.0
+ * @see Section
+ * @see Contents
+ * @see Image
+ * @see CraftingRecipe
+ */
+public class GuiWizardHandbook extends GuiScreen {
+
+ private static final ResourceLocation DEFAULT = new ResourceLocation(Wizardry.MODID, "texts/handbook_en_us.json");
+
+ static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/gui/handbook.png");
+
+ /** Global Gson instance for the handbook. */
+ private static final Gson gson = new Gson();
+
+ // Formatting markup
+
+ static final char FORMAT_MARKER = '#';
+ static final char HYPERLINK_MARKER = '@';
+
+ static final String IMAGE_TAG = "image";
+ static final String RECIPE_TAG = "recipe";
+ static final String RULER_TAG = "ruler";
+
+ static final Map FORMAT_TAGS = new HashMap<>();
+
+ // Dimension constants
+ // Private constants are not relevant to book elements, package-protected ones are
+
+ /** The dimensions of the rendered GUI area. */
+ static final int GUI_WIDTH = 288, GUI_HEIGHT = 180;
+ /** The dimensions of the GUI texture itself. */
+ static final int TEXTURE_WIDTH = 512, TEXTURE_HEIGHT = 256;
+ /** The dimensions of the area of a single page in which text can be drawn. */
+ static final int PAGE_WIDTH = 120, PAGE_HEIGHT = 140;
+ /** The distance of the text from the top outside corner of each page. */
+ static final int TEXT_INSET_X = 17, TEXT_INSET_Y = 16;
+ /** The distance of the buttons from the bottom outside corners of the GUI. */
+ private static final int BUTTON_INSET_X = 22, BUTTON_INSET_Y = 13;
+ /** The distance between adjacent buttons. */
+ private static final int BUTTON_SPACING = 20;
+ /** The distance of the page numbers from the bottom of the GUI. */
+ private static final int PAGE_NUMBER_INSET = 22;
+
+ // IDEA: Constant dimensions could be converted to JSON like the spell HUD ones
+
+ // Global variables
+
+ /**
+ * The double-page currently being viewed. Each double-page spread counts as a single page, with the inside
+ * of the front cover being page 0.
+ */
+ private int currentPage = 0;
+ /**
+ * The number of single pages currently in the book. This is calculated on GUI load based on visible sections.
+ */
+ private int pageCount = 1; // Starts at 1 because the first single-page is the inside of the cover
+ /**
+ * The double-page number where the bookmark is currently set, relative to the section stored in
+ * {@link GuiWizardHandbook#bookmarkSection} . Static because it persists when the book is closed.
+ */
+ private static int bookmarkPage = 0;
+ /**
+ * The key corresponding to the section in which the bookmark is currently set. Static because it persists when the
+ * book is closed. Storing a section means the bookmark doesn't change location when new sections are unlocked.
+ */
+ private static String bookmarkSection;
+
+ // Buttons
+ private GuiButton bookmark, next, previous, nextSection, previousSection, menu;
+
+ // Handbook content
+
+ // As a general rule, I prefer to make static final fields lowercase if they're collections that change, because even
+ // though the collection itself is constant, the stuff in it is not, so being lowercase highlights this difference.
+
+ /**
+ * A map which stores all loaded section objects, including subsections. This gets wiped on resource pack reload and
+ * repopulated with mappings as specified by the handbook JSON file for the current language. The keys in the map
+ * correspond to the keys in the sections object in that file, and are sorted in that order.
+ */
+ static final Map sections = new LinkedHashMap<>();
+
+ /**
+ * A list which stores all loaded section objects, including subsections. This is an unmodifiable list view of the
+ * values in {@link GuiWizardHandbook#sections}, sorted in the same (page number) order. This exists only to allow
+ * sections to be accessed by ordinal index for the various navigation buttons, hence why it is private.
+ */
+ private static List sectionList;
+
+ /**
+ * A map which stores all loaded contents objects. This gets wiped on resource pack reload and repopulated with
+ * mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
+ * id strings for the contents objects in that file. This map is not sorted.
+ */
+ static final Map contentsList = new HashMap<>();
+
+ /**
+ * A map which stores all loaded hex colour values. This gets wiped on resource pack reload and repopulated with
+ * mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
+ * keys in the colours object in that file. This map is not sorted.
+ */
+ static final Map colours = new HashMap<>();
+
+ /**
+ * A map which stores all loaded image objects. This gets wiped on resource pack reload and repopulated with
+ * mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to the
+ * keys in the images object in that file. This map is not sorted.
+ */
+ static final Map images = new HashMap<>();
+
+ /**
+ * A map which stores all loaded crafting recipe objects. This gets wiped on resource pack reload and repopulated
+ * with mappings as specified by the handbook JSON file for the current language. The keys in the map correspond to
+ * the keys in the recipes object in that file. This map is not sorted.
+ */
+ static final Map recipes = new HashMap<>();
+
+ /**
+ * Adds a format tag to the handbook. All occurrences of the given tag string preceded by a # will be replaced with
+ * the result of the given value string on GUI load. The value string, therefore, can be anything that should be
+ * input dynamically, as long as it does not change while the GUI is open. Examples include wizardry's version,
+ * the various element colours and the keys assigned to wizardry's controls.
+ * @param tag The tag string, as defined in the handbook JSON file, excluding the # character. Cannot include spaces.
+ * @param value The string to replace occurrences of the given format tag with. Can include spaces but not the # character.
+ */
+ public static void addFormatTag(String tag, String value){
+ FORMAT_TAGS.put(tag, value);
+ }
+
+ private static void initFormatTags(){
+
+ addFormatTag("next_spell_key", ClientProxy.NEXT_SPELL.getDisplayName());
+ addFormatTag("previous_spell_key", ClientProxy.PREVIOUS_SPELL.getDisplayName());
+ addFormatTag("example_charging_loss", "" + (Constants.MANA_PER_CRYSTAL - 30));
+ addFormatTag("mana_per_crystal", "" + Constants.MANA_PER_CRYSTAL);
+ addFormatTag("novice_max_charge", "" + Tier.NOVICE.maxCharge);
+ addFormatTag("apprentice_max_charge", "" + Tier.APPRENTICE.maxCharge);
+ addFormatTag("advanced_max_charge", "" + Tier.ADVANCED.maxCharge);
+ addFormatTag("master_max_charge", "" + Tier.MASTER.maxCharge);
+ addFormatTag("version", Wizardry.VERSION);
+ addFormatTag("mcversion", Minecraft.getMinecraft().getVersion());
+
+ addFormatTag("colour_novice", "\u00A77");
+ addFormatTag("colour_apprentice", Tier.APPRENTICE.getFormattingCode());
+ addFormatTag("colour_advanced", Tier.ADVANCED.getFormattingCode());
+ addFormatTag("colour_master", Tier.MASTER.getFormattingCode());
+
+ addFormatTag("colour_fire", Element.FIRE.getFormattingCode());
+ addFormatTag("colour_ice", Element.ICE.getFormattingCode());
+ addFormatTag("colour_lightning", Element.LIGHTNING.getFormattingCode());
+ addFormatTag("colour_necromancy", Element.NECROMANCY.getFormattingCode());
+ addFormatTag("colour_earth", Element.EARTH.getFormattingCode());
+ addFormatTag("colour_sorcery", Element.SORCERY.getFormattingCode());
+ addFormatTag("colour_healing", Element.HEALING.getFormattingCode());
+
+ addFormatTag("colour_reset", "\u00A70");
+ }
+
+ // Helper methods
+
+ /**
+ * Converts the given single page index to a double-page index. Inverse of
+ * {@link GuiWizardHandbook#doubleToSinglePage(int, boolean)}.
+ *
+ * @param singlePageIndex The single-page index, which is the same as the page numbers actually displayed.
+ * @return The corresponding double-page index.
+ */
+ static int singleToDoublePage(int singlePageIndex){
+ // Yes, this is trivial, but if I ever change the numbering it'll be useful. It's also more descriptive.
+ return singlePageIndex / 2;
+ }
+
+ /**
+ * Converts the given double-page index to a single-page index. Inverse of
+ * {@link GuiWizardHandbook#singleToDoublePage(int)}.
+ *
+ * @param doublePageIndex The double-page index, as stored in {@link GuiWizardHandbook#currentPage}.
+ * @param rightHandPage True to return the page on the right (1 greater), false for the left-hand page.
+ * @return The corresponding single-page index.
+ */
+ static int doubleToSinglePage(int doublePageIndex, boolean rightHandPage){
+ return rightHandPage ? doublePageIndex * 2 + 1 : doublePageIndex * 2;
+ }
+
+ /**
+ * Returns whether the given page index refers to a right-hand page or a left-hand page.
+ *
+ * @param page The single-page index, which is the same as the page number actually displayed.
+ * @return True if the given page index refers to a right-hand page, false if it is a left-hand page.
+ */
+ static boolean isRightPage(int page){
+ return page % 2 == 1;
+ }
+
+ // Drawing
+
+ @Override
+ public void drawScreen(int mouseX, int mouseY, float partialTicks){
+
+ int left = this.width / 2 - GUI_WIDTH / 2;
+ int top = this.height / 2 - GUI_HEIGHT / 2;
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(texture);
+
+ GlStateManager.color(1, 1, 1, 1);
+
+ // Main background
+ DrawingUtils.drawTexturedRect(left, top, 0, 0, GUI_WIDTH, GUI_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+
+ // First page background
+ if(currentPage == 0){
+ DrawingUtils.drawTexturedRect(left, top, 368, 0, GUI_WIDTH / 2, GUI_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+ previous.visible = false;
+ previousSection.visible = false; // Not worth testing if we're in the first section every frame
+ menu.visible = false;
+ }else{
+ previous.visible = true;
+ previousSection.visible = true;
+ menu.visible = true;
+ }
+
+ // Last page background
+ if(currentPage == singleToDoublePage(pageCount)){
+ DrawingUtils.drawTexturedFlippedRect(left + GUI_WIDTH / 2, top, 368, 0, GUI_WIDTH / 2, GUI_HEIGHT, TEXTURE_WIDTH, TEXTURE_HEIGHT, true, false);
+ next.visible = false;
+ nextSection.visible = false;
+ }else{
+ next.visible = true;
+ nextSection.visible = true;
+ }
+
+ // Page numbers
+ if(currentPage > 0){
+ String pageNumber = "" + doubleToSinglePage(currentPage, false);
+ this.fontRenderer.drawString(pageNumber, left + TEXT_INSET_X + PAGE_WIDTH / 2
+ - fontRenderer.getStringWidth(pageNumber)/2, top + GUI_HEIGHT - PAGE_NUMBER_INSET, DrawingUtils.BLACK);
+ }
+ if(currentPage < singleToDoublePage(pageCount)){
+ String pageNumber = "" + doubleToSinglePage(currentPage, true);
+ this.fontRenderer.drawString(pageNumber, left + GUI_WIDTH - TEXT_INSET_X - PAGE_WIDTH / 2
+ - fontRenderer.getStringWidth(pageNumber)/2, top + GUI_HEIGHT - PAGE_NUMBER_INSET, DrawingUtils.BLACK);
+ }
+
+ // Main content
+ contentsList.values().forEach(c -> { if(c.section.isUnlocked()) c.draw(fontRenderer, currentPage, left, top); } );
+ sections.values().forEach(s -> { if(s.isUnlocked()) s.draw(fontRenderer, currentPage, left, top); } );
+ // These only get populated if the sections are unlocked so no checks are necessary
+ images.values().forEach(i -> i.draw(fontRenderer, currentPage, left, top));
+ recipes.values().forEach(r -> r.draw(fontRenderer, itemRender, currentPage, left, top));
+
+ // Buttons
+ super.drawScreen(mouseX, mouseY, partialTicks);
+
+ // Bookmark
+ GlStateManager.color(1, 1, 1, 1);
+ Minecraft.getMinecraft().renderEngine.bindTexture(texture);
+
+ if(currentPage == singleToDoublePage(sections.get(bookmarkSection).startPage) + bookmarkPage){
+ // If the current page is the bookmarked page, the (invisible) bookmark button is disabled
+ bookmark.visible = false;
+ DrawingUtils.drawTexturedRect(left + 138, top, 299, 0, 11, 191, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+ }else{
+ bookmark.visible = true;
+ bookmark.x = left + (currentPage > singleToDoublePage(sections.get(bookmarkSection).startPage) + bookmarkPage ? 130 : 147);
+ DrawingUtils.drawTexturedRect(bookmark.x, top,
+ bookmark.isMouseOver() ? 310 : 288, 0, 11, 191, TEXTURE_WIDTH, TEXTURE_HEIGHT);
+ }
+
+ // Recipe tooltips
+ recipes.values().forEach(r -> r.drawTooltips(this, fontRenderer, itemRender, currentPage, left, top, mouseX, mouseY));
+
+ }
+
+ // GUI Initialisation / Close
+
+ @Override
+ public void onResize(Minecraft minecraft, int width, int height){
+ initGui();
+ }
+
+ @Override
+ public void onGuiClosed(){
+ super.onGuiClosed();
+ Keyboard.enableRepeatEvents(false);
+ }
+
+ @Override
+ public void initGui(){
+
+ super.initGui();
+ Keyboard.enableRepeatEvents(true);
+
+ initFormatTags();
+
+ final int left = this.width / 2 - GUI_WIDTH / 2;
+ final int top = this.height / 2 - GUI_HEIGHT / 2;
+
+ recipes.values().forEach(CraftingRecipe::load);
+
+ int nextButtonId = 0;
+
+ this.buttonList.clear();
+
+ this.buttonList.add(next = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH - BUTTON_INSET_X - GuiButtonTurnPage.WIDTH,
+ top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.NEXT_PAGE));
+
+ this.buttonList.add(previous = new GuiButtonTurnPage(nextButtonId++, left + BUTTON_INSET_X,
+ top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.PREVIOUS_PAGE));
+
+ this.buttonList.add(nextSection = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH - BUTTON_INSET_X - GuiButtonTurnPage.WIDTH - BUTTON_SPACING,
+ top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.NEXT_SECTION));
+
+ this.buttonList.add(previousSection = new GuiButtonTurnPage(nextButtonId++, left + BUTTON_INSET_X + BUTTON_SPACING,
+ top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.PREVIOUS_SECTION));
+
+ this.buttonList.add(menu = new GuiButtonTurnPage(nextButtonId++, left + GUI_WIDTH/2 - 28,
+ top + GUI_HEIGHT - BUTTON_INSET_Y - GuiButtonTurnPage.HEIGHT, Type.CONTENTS));
+
+ this.buttonList.add(bookmark = new GuiButtonInvisible(nextButtonId++, left + 130, top + 172, 11, 19) {
+ @Override
+ public void playPressSound(SoundHandler soundHandler){
+ soundHandler.playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_PAGE_TURN, 1));
+ }
+ });
+
+ pageCount = 1;
+
+ // Clears instances of all images and recipes
+ images.values().forEach(Image::clearInstances);
+ recipes.values().forEach(CraftingRecipe::clearInstances);
+
+ // Formats all the unlocked sections in order
+ for(Section section : sections.values()){
+ if(section.isUnlocked()){
+ pageCount = section.format(this.fontRenderer, pageCount, left, top);
+ buttonList.addAll(section.getButtons());
+ }
+ }
+
+ contentsList.values().forEach(c -> buttonList.addAll(c.getButtons()));
+
+ this.mc.getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(WizardrySounds.MISC_BOOK_OPEN, 1));
+ }
+
+ // JSON Parsing / Data Construction
+
+ /**
+ * Called from preInit in the main mod class (via the proxies) to initialise the handbook (parses the JSON file
+ * and constructs the relevant data structures), and again on each resource reload (changing the language triggers
+ * a resource reload).
+ */
+ public static void loadHandbookFile(IResourceManager manager){
+
+ IResource handbookFile = getHandbookResource(manager);
+
+ if(handbookFile != null){
+
+ // Wipes all the maps before repopulating them
+ images.clear();
+ sections.clear();
+ contentsList.clear();
+ colours.clear();
+
+ bookmarkSection = null; // Also need to wipe the reference to the old bookmarked section
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(handbookFile.getInputStream()));
+
+ JsonElement je = gson.fromJson(reader, JsonElement.class);
+ JsonObject json = je.getAsJsonObject();
+
+ JsonUtils.getJsonObject(json, "colours").entrySet().forEach(e -> colours.put(e.getKey(),
+ Color.decode(e.getValue().getAsString()).getRGB()));
+
+ // Repopulates the remaining maps
+ Image.populate(images, json);
+ CraftingRecipe.populate(recipes, json);
+ Section.populate(sections, json);
+
+ sectionList = Collections.unmodifiableList(new ArrayList<>(sections.values()));
+
+ if(sections.isEmpty()){
+ Wizardry.logger.warn("Handbook has no sections! Aborting loading...");
+ return;
+ }
+
+ bookmarkSection = JsonUtils.getString(json, "bookmark_start_section");
+ if(!sections.containsKey(bookmarkSection)) throw new JsonSyntaxException("Section with id " + bookmarkSection + " is undefined");
+ }
+
+ // The first resource load on startup is done before the packet handler is loaded
+ if(WizardryPacketHandler.net != null) WizardryPacketHandler.net.sendToServer(new PacketRequestAdvancementSync.Message());
+ }
+
+ /**
+ * Retrieves the handbook JSON file for the current language and returns its IResource object. If a handbook file
+ * cannot be found for the current language, a message is printed to the console and the method attempts to retrieve
+ * the default file instead (English-US). If this file cannot be found, the resulting error is printed to the
+ * console and the method returns null.
+ *
+ * @param manager The resource manager instance to use.
+ * @return The handbook JSON file, as an IResource, or null if it was not found.
+ */
+ private static IResource getHandbookResource(IResourceManager manager){
+
+ // TODO: Implement resource pack stacking to allow addon mods and texture packs to add/overwrite content
+
+ IResource handbookFile = null;
+
+ try{
+ handbookFile = manager.getResource(new ResourceLocation(Wizardry.MODID, "texts/handbook_"
+ + Minecraft.getMinecraft().getLanguageManager().getCurrentLanguage().getLanguageCode() + ".json"));
+ }catch(IOException e){
+
+ Wizardry.logger.info("Wizard handbook JSON file missing for the current language (" + Minecraft.getMinecraft()
+ .getLanguageManager().getCurrentLanguage() + "). Using default (English-US) instead.");
+
+ try{
+ handbookFile = manager.getResource(DEFAULT);
+ }catch(IOException x){
+ Wizardry.logger.error("Couldn't find file: " + DEFAULT + ". The file may be missing; please try re-downloading and reinstalling Wizardry.", x);
+ }
+ }
+
+ return handbookFile;
+ }
+
+ // Controls
+
+ @Override
+ protected void actionPerformed(GuiButton button){
+
+ if(button.enabled){
+
+ if(button == next){
+ if(currentPage < singleToDoublePage(pageCount)) currentPage++;
+
+ }else if(button == previous){
+ if(currentPage > 0) currentPage--;
+
+ }else if(button == nextSection || button == previousSection){
+
+ Section currentSection = null;
+
+ for(Section section : sections.values()){
+ // We always want this button to do something, and taking the right-hand page means it always does
+ if(section.containsPage(doubleToSinglePage(currentPage, true))){
+ currentSection = section;
+ break;
+ }
+ }
+
+ if(currentSection != null){
+
+ List visibleSections = new ArrayList<>(sectionList);
+ visibleSections.removeIf(s -> !s.isUnlocked());
+
+ int index = visibleSections.indexOf(currentSection);
+
+ if(button == nextSection && index + 1 < visibleSections.size()){
+ currentPage = singleToDoublePage(visibleSections.get(index + 1).startPage);
+ }else if(index > 0){
+ currentPage = singleToDoublePage(visibleSections.get(index - 1).startPage);
+ }
+ }
+
+ }else if(button == menu){
+ currentPage = singleToDoublePage(sections.get("main_contents").startPage);
+
+ }else if(button == bookmark && bookmarkSection != null){
+ currentPage = singleToDoublePage(sections.get(bookmarkSection).startPage) + bookmarkPage;
+
+ }else{
+ if(button instanceof GuiButtonHyperlink.Internal){
+ currentPage = singleToDoublePage(((GuiButtonHyperlink.Internal)button).target.startPage);
+ }else if(button instanceof GuiButtonHyperlink.External){
+ this.handleComponentClick(((GuiButtonHyperlink.External)button).link);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException{
+ if(mouseButton == 1){
+ // Right-clicking of bookmark
+ if(bookmark.mousePressed(this.mc, mouseX, mouseY)){
+
+ this.selectedButton = bookmark;
+
+ for(String key : sections.keySet()){
+ // The bookmark is assumed to bookmark the left-hand page
+ if(sections.get(key).containsPage(doubleToSinglePage(currentPage, false))) bookmarkSection = key;
+ }
+
+ bookmarkPage = currentPage - singleToDoublePage(sections.get(bookmarkSection).startPage);
+ }
+ }else{
+ super.mouseClicked(mouseX, mouseY, mouseButton);
+ }
+ }
+
+ // Overridden to make it public
+ @Override
+ public void renderToolTip(ItemStack stack, int x, int y){
+ super.renderToolTip(stack, x, y);
+ }
+
+ @Override
+ public boolean doesGuiPauseGame(){
+ return Wizardry.settings.booksPauseGame;
+ }
+
+
+ public static void updateUnlockStatus(boolean showToasts, ResourceLocation... completedAdvancements){
+ sections.values().forEach(s -> s.updateUnlockStatus(showToasts, completedAdvancements));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/HandbookToast.java b/src/main/java/electroblob/wizardry/client/gui/handbook/HandbookToast.java
new file mode 100644
index 00000000..2db6e666
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/HandbookToast.java
@@ -0,0 +1,54 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import electroblob.wizardry.registry.WizardryItems;
+import net.minecraft.client.gui.toasts.GuiToast;
+import net.minecraft.client.gui.toasts.IToast;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.RenderHelper;
+import net.minecraft.client.resources.I18n;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.List;
+
+//@SideOnly(Side.CLIENT)
+public class HandbookToast implements IToast {
+
+ private final Section section;
+
+ public HandbookToast(Section section){
+ this.section = section;
+ }
+
+ public IToast.Visibility draw(GuiToast toastGui, long delta){
+
+ toastGui.getMinecraft().getTextureManager().bindTexture(TEXTURE_TOASTS);
+
+ GlStateManager.color(1.0F, 1.0F, 1.0F);
+ toastGui.drawTexturedModalRect(0, 0, 0, 32, 160, 32);
+
+ boolean firstPart = delta < 1500L;
+
+ int a = firstPart ? MathHelper.floor(MathHelper.clamp((float)(1500L - delta) / 300.0F, 0.0F, 1.0F) * 255.0F) << 24 | 67108864
+ : MathHelper.floor(MathHelper.clamp((float)(delta - 1500L) / 300.0F, 0.0F, 1.0F) * 252.0F) << 24 | 67108864;
+
+ String s = firstPart ? I18n.format("handbook.toast.title") : section.title;
+
+ int c = firstPart ? -11534256 : -16777216;
+
+ List list = toastGui.getMinecraft().fontRenderer.listFormattedStringToWidth(s, 125);
+
+ int h = 16 - list.size() * toastGui.getMinecraft().fontRenderer.FONT_HEIGHT / 2;
+
+ for(String line : list){
+ toastGui.getMinecraft().fontRenderer.drawString(line, 30, h, c | a);
+ h += toastGui.getMinecraft().fontRenderer.FONT_HEIGHT;
+ }
+
+ RenderHelper.enableGUIStandardItemLighting();
+ toastGui.getMinecraft().getRenderItem().renderItemAndEffectIntoGUI(null, new ItemStack(WizardryItems.wizard_handbook), 8, 8);
+
+ return delta >= 5000L ? IToast.Visibility.HIDE : IToast.Visibility.SHOW;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/Image.java b/src/main/java/electroblob/wizardry/client/gui/handbook/Image.java
new file mode 100644
index 00000000..732bb10b
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/Image.java
@@ -0,0 +1,148 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.client.DrawingUtils;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+class Image {
+
+ // Final fields are mandatory, the rest are optional
+ private final ResourceLocation location;
+ private final int width, height;
+ private int textureWidth, textureHeight;
+ private int u = 0, v = 0;
+ private String caption = "";
+ private boolean border = true;
+ // Derived fields, not specifically defined in JSON
+ private final Set instances = new HashSet<>();
+
+ private static final int CAPTION_OFFSET = 4;
+
+ private static final int TEXTURE_INSET_X = 180;
+ private static final int BORDER = 1;
+
+ private Image(ResourceLocation location, int width, int height){
+ this.location = location;
+ this.width = width;
+ this.height = height;
+ }
+
+ /** Returns the width of the image. */
+ int getWidth(){
+ return width;
+ }
+
+ /** Returns the total height of the image, including caption if it has one. */
+ int getHeight(FontRenderer font){
+ return caption.isEmpty() ? height : height + CAPTION_OFFSET + font.FONT_HEIGHT;
+ }
+
+ /**
+ * Adds an instance of this image to the list.
+ *
+ * @param page The index of the single page this image is on.
+ * @param x The x-coordinate of the top-left corner of the image, relative to the top-left corner of the GUI.
+ * @param y The y-coordinate of the top-left corner of the image, relative to the top-left corner of the GUI.
+ */
+ void addInstance(int page, int x, int y){
+ instances.add(new int[]{page, x, y});
+ }
+
+ /** Removes all instances of this image from the list. */
+ void clearInstances(){
+ instances.clear();
+ }
+
+ /**
+ * Draws all instances of this image that are located on the given double-page spread.
+ *
+ * @param font The font renderer object.
+ * @param doublePage The double-page index of the page to be drawn.
+ * @param left The x coordinate of the left side of the GUI.
+ * @param top The y coordinate of the top of the GUI.
+ */
+ void draw(FontRenderer font, int doublePage, int left, int top){
+ // Images
+ for(int[] instance : instances){
+ if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
+ Minecraft.getMinecraft().renderEngine.bindTexture(location);
+ GlStateManager.color(1, 1, 1, 1);
+ DrawingUtils.drawTexturedRect(left + instance[1], top + instance[2], u, v, width, height, textureWidth, textureHeight);
+ font.drawString("\u00A7o" + caption, left + instance[1] + width/ 2 - font.getStringWidth(caption)/2,
+ top + instance[2] + height + CAPTION_OFFSET, GuiWizardHandbook.colours.get("caption"));
+ }
+ }
+
+ if(border){
+ // Borders - do this after all the images are drawn so we only have to bind the handbook texture again once
+ Minecraft.getMinecraft().renderEngine.bindTexture(GuiWizardHandbook.texture);
+ GlStateManager.color(1, 1, 1, 1);
+ for(int[] instance : instances){
+ if(GuiWizardHandbook.singleToDoublePage(instance[0]) == doublePage){
+ // Math.ceil accounts for odd-numbered image dimensions
+ DrawingUtils.drawTexturedFlippedRect(left + instance[1] - BORDER, top + instance[2] - BORDER,
+ TEXTURE_INSET_X, GuiWizardHandbook.GUI_HEIGHT, width / 2 + BORDER, height / 2 + BORDER,
+ GuiWizardHandbook.TEXTURE_WIDTH, GuiWizardHandbook.TEXTURE_HEIGHT, false, false);
+ DrawingUtils.drawTexturedFlippedRect(left + instance[1] + width / 2, top + instance[2] - BORDER,
+ TEXTURE_INSET_X, GuiWizardHandbook.GUI_HEIGHT, MathHelper.ceil(width / 2f) + BORDER, height / 2 + BORDER,
+ GuiWizardHandbook.TEXTURE_WIDTH, GuiWizardHandbook.TEXTURE_HEIGHT, true, false);
+ DrawingUtils.drawTexturedFlippedRect(left + instance[1] - BORDER, top + instance[2] + height / 2,
+ TEXTURE_INSET_X, GuiWizardHandbook.GUI_HEIGHT, width / 2 + BORDER, MathHelper.ceil(height / 2f) + BORDER,
+ GuiWizardHandbook.TEXTURE_WIDTH, GuiWizardHandbook.TEXTURE_HEIGHT, false, true);
+ DrawingUtils.drawTexturedFlippedRect(left + instance[1] + width / 2, top + instance[2] + height / 2,
+ TEXTURE_INSET_X, GuiWizardHandbook.GUI_HEIGHT, MathHelper.ceil(width / 2f) + BORDER, MathHelper.ceil(height / 2f) + BORDER,
+ GuiWizardHandbook.TEXTURE_WIDTH, GuiWizardHandbook.TEXTURE_HEIGHT, true, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Parses the given JSON object and constructs a new {@code Image} from it, setting all the relevant fields
+ * and references.
+ *
+ * @param json A JSON object representing the image to be constructed. This must contain at least a "location"
+ * string.
+ * @return The resulting {@code Image} object.
+ * @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
+ */
+ static Image fromJson(JsonObject json){
+
+ Image image = new Image(new ResourceLocation(JsonUtils.getString(json, "location")),
+ JsonUtils.getInt(json, "width"), JsonUtils.getInt(json, "height"));
+
+ image.u = JsonUtils.getInt(json, "u", 0);
+ image.v = JsonUtils.getInt(json, "v", 0);
+ image.textureWidth = JsonUtils.getInt(json, "texture_width", image.width);
+ image.textureHeight = JsonUtils.getInt(json, "texture_height", image.height);
+ image.caption = JsonUtils.getString(json, "caption", "");
+ image.border = JsonUtils.getBoolean(json, "border", true);
+
+ return image;
+ }
+
+ static void populate(Map map, JsonObject json){
+
+ JsonObject sectionsObject = JsonUtils.getJsonObject(json, "images");
+
+ // Need to iterate over these since we don't know what they're called or how many there are
+ for(Map.Entry entry : sectionsObject.entrySet()){
+
+ String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
+
+ Image image = fromJson(entry.getValue().getAsJsonObject());
+ map.put(key, image);
+ }
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/gui/handbook/Section.java b/src/main/java/electroblob/wizardry/client/gui/handbook/Section.java
new file mode 100644
index 00000000..c057d5e5
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/gui/handbook/Section.java
@@ -0,0 +1,451 @@
+package electroblob.wizardry.client.gui.handbook;
+
+import com.google.common.collect.Streams;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.DrawingUtils;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.GuiButton;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.util.JsonUtils;
+import net.minecraft.util.ResourceLocation;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.*;
+
+/**
+ * Instances of this class represent sections in the wizard's handbook. As of wizardry 4.2, this class handles
+ * everything within the section itself, including JSON parsing, unlock triggers and drawing the actual rawText.
+ * Sections may now also be nested and have other elements within them, such as images and a table of contents, a
+ * behaviour which is also handled within this class.
+ *
+ * The formatting of the book is now done 'dynamically' - that is, the exact positions and page numbers of
+ * sections, images and so on are determined on GUI load and depend on which of the previous sections have been
+ * unlocked, amongst other factors. This means that all of the unlocked sections must be formatted in order on GUI
+ * load, so that each section knows the previous section's length and therefore where to start.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.2
+ */
+// Because these are now generated on resource pack reload (not on handbook open, as before), this class can no longer
+// be a non-static inner class
+class Section {
+
+ // Final fields are mandatory (none here though), the rest are optional
+ String title;
+ private String[] rawText;
+ private Contents contents;
+ private ResourceLocation[] triggers;
+ private Map subsections;
+ private boolean centreX, centreY;
+
+ // Derived fields, not explicitly defined in JSON
+ /** The single-page index of the first page of this section. */
+ int startPage;
+ private final List> buttons;
+ /**
+ * A list of single pages, which are themselves lists of paragraphs (each paragraph is a single
+ * string which may include line breaks and other escape characters).
+ */
+ private final List> pages;
+
+ private boolean unlocked = false;
+ private boolean isNew = false;
+
+ private Section(){
+ this.buttons = new ArrayList<>();
+ this.pages = new ArrayList<>();
+ this.subsections = new LinkedHashMap<>();
+ }
+
+ Collection getButtons(){
+ return WizardryUtilities.flatten(buttons);
+ }
+
+ /**
+ * Returns true if the given page is within this section, false if not (or if the section is locked).
+ */
+ boolean containsPage(int page){
+ return this.isUnlocked() && startPage <= page && startPage + pages.size() > page;
+ }
+
+ /**
+ * Returns true if this section is unlocked for the client player, false if not. Always returns true if
+ * handbook progression is disabled in the config.
+ */
+ boolean isUnlocked(){
+
+ if(Minecraft.getMinecraft().player.isCreative()) return true;
+ if(!Wizardry.settings.handbookProgression) return true; // Always unlocked if handbook progression is off
+ if(triggers == null) return true; // If no triggers were defined, the section is unlocked from the start
+
+ // A section is automatically unlocked if one of its subsections is unlocked
+ for(Section subsection : subsections.values()){
+ if(subsection.isUnlocked()) return true;
+ }
+
+ return unlocked;
+ }
+
+ /**
+ * Returns true if this section has been unlocked and not read yet. Also returns true if any subsections are new.
+ */
+ boolean isNew(){
+ if(!Wizardry.settings.handbookProgression) return false;
+ return isNew || this.subsections.values().stream().anyMatch(Section::isNew);
+ }
+
+ /**
+ * Actually draws the contents of the given section for the given double-page spread. Will do nothing if the
+ * given page is outside of this section.
+ *
+ * @param font The font renderer object.
+ * @param doublePage The index of the double-page to be drawn.
+ * @param left The x coordinate of the left side of the GUI.
+ * @param top The y coordinate of the top of the GUI.
+ */
+ // This method is supposed to be 'idiot-proof' in the sense that the code calling it need not check whether the
+ // section actually needs drawing, so it can just dumbly call draw(...) for all the sections in order.
+ void draw(FontRenderer font, int doublePage, int left, int top){
+
+ // Show/hide buttons
+
+ int i = 0;
+
+ for(List list : buttons){
+ final int i1 = i++;
+ list.forEach(b -> b.visible = GuiWizardHandbook.singleToDoublePage(startPage + i1) == doublePage);
+ }
+
+ int leftIndex = GuiWizardHandbook.doubleToSinglePage(doublePage, false);
+ // Relative indices of the pages to be rendered - often these will be outside the section entirely
+ int[] visiblePages = {leftIndex - startPage, leftIndex - startPage + 1};
+
+ for(int page : visiblePages){
+
+ if(page >= 0 && page < pages.size()){
+
+ List lines = pages.get(page);
+
+ int x = left + (GuiWizardHandbook.isRightPage(startPage + page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X);
+ int y = top + GuiWizardHandbook.TEXT_INSET_Y;
+ if(centreY) y += GuiWizardHandbook.PAGE_HEIGHT / 2 - lines.size() / 2 * font.FONT_HEIGHT;
+
+ for(String line : lines){
+
+ if(line.startsWith(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.RULER_TAG)){
+ GlStateManager.color(1, 1, 1, 1);
+ Minecraft.getMinecraft().renderEngine.bindTexture(GuiWizardHandbook.texture);
+ DrawingUtils.drawTexturedRect(x-1, y-1, 0, GuiWizardHandbook.GUI_HEIGHT, GuiWizardHandbook.PAGE_WIDTH + 2, 9, GuiWizardHandbook.TEXTURE_WIDTH, GuiWizardHandbook.TEXTURE_HEIGHT);
+ }else{
+ int lx = centreX ? x + GuiWizardHandbook.PAGE_WIDTH / 2 - font.getStringWidth(line) / 2 : x;
+ font.drawString(line, lx, y, DrawingUtils.BLACK, false);
+ }
+
+ y += font.FONT_HEIGHT;
+ }
+
+ isNew = false; // Now a page has been drawn, the player must have seen it so it's not new any more
+ }
+ }
+ }
+
+ /**
+ * Called on GUI load to format the section, contents tables and other elements, excluding subsections.
+ * Does not perform any actual drawing.
+ *
+ * @param font The font renderer object, for measurement purposes.
+ * @param startPage The index of the first page (single side, not double-page) of this section.
+ * @param left The x coordinate of the left side of the GUI.
+ * @param top The y coordinate of the top of the GUI.
+ * @return The single-page index of the next blank page after the end of this section.
+ * @throws JsonSyntaxException if at any point the formatting is found to be invalid.
+ */
+ int format(FontRenderer font, int startPage, int left, int top){
+
+ this.buttons.clear();
+ this.pages.clear();
+
+ // FONT_HEIGHT may change between fonts, so this is calculated here. With the default font it's 14.
+ final int maxLineNumber = GuiWizardHandbook.PAGE_HEIGHT / font.FONT_HEIGHT;
+
+ this.startPage = startPage;
+
+ // First everything is added to a single list of lines, then it is split into pages.
+ List lines = new ArrayList<>();
+
+ // Adds the header if present
+ if(!this.title.isEmpty()){
+ lines.add(this.title);
+ lines.add(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.RULER_TAG);
+ }
+
+ // Adds space for the contents if it exists
+ if(this.contents != null){
+ lines.addAll(Collections.nCopies(this.contents.format(font, startPage, lines.size(), left, top), ""));
+ // Line break between contents and first paragraph
+ if((lines.size() % maxLineNumber) != 0) lines.add("");
+ }
+
+ if(this.rawText != null){
+ // Paragraphs are defined as a JSON list because it makes it easier to arrange them properly across pages
+ // - using multiple line breaks would mean having to find and remove them when at the top of a page.
+ for(String paragraph : this.rawText){
+
+ // (lines.size() % maxLineNumber) gives the number of lines on the current page
+ // (lines.size() / maxLineNumber) gives the index of the current page minus the value of startPage
+
+ // Formats the paragraph
+
+ String raw = paragraph; // For error messages
+
+ // Images (images must be separate paragraphs)
+
+ if(paragraph.startsWith(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.IMAGE_TAG)){
+
+ String[] arguments = paragraph.split("\\s", 2);
+
+ if(arguments.length < 2) throw new JsonSyntaxException("Missing image name in string "
+ + StringUtils.abbreviate(raw, 50));
+
+ Image image = GuiWizardHandbook.images.get(arguments[1]);
+ if(image == null) throw new JsonSyntaxException("Image with id " + arguments[1] + " is undefined");
+
+ // Starts a new page if the image will not fit on the current one
+ if((lines.size() % maxLineNumber) * font.FONT_HEIGHT + image.getHeight(font) > GuiWizardHandbook.PAGE_HEIGHT){
+ // Remaining number of lines on the page
+ lines.addAll(Collections.nCopies(maxLineNumber - (lines.size() % maxLineNumber), ""));
+ }
+
+ if(image.getWidth() > GuiWizardHandbook.PAGE_WIDTH) Wizardry.logger.warn("Image with id " + arguments[1]
+ + "has a width (" + image.getWidth() + ") greater than the maximum page width (" + GuiWizardHandbook.PAGE_WIDTH
+ + "), it will extend beyond the page area.");
+
+ if(image.getHeight(font) > GuiWizardHandbook.PAGE_HEIGHT) Wizardry.logger.warn("Image with id " + arguments[1]
+ + "has a height (" + image.getHeight(font) + ") greater than the maximum page height (" + GuiWizardHandbook.PAGE_HEIGHT
+ + "), it will extend beyond the page area.");
+
+ int page = startPage + (lines.size() / maxLineNumber);
+
+ image.addInstance(page, GuiWizardHandbook.PAGE_WIDTH / 2 - image.getWidth() / 2
+ + (GuiWizardHandbook.isRightPage(page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X),
+ GuiWizardHandbook.TEXT_INSET_Y + (lines.size() % maxLineNumber) * font.FONT_HEIGHT);
+
+ // Height of the image in lines, rounded up
+ // Uses a single space instead of an empty string so that the page trimming doesn't remove them
+ lines.addAll(Collections.nCopies(image.getHeight(font) / font.FONT_HEIGHT, " "));
+ lines.add(""); // The last one is removable though, since it's actually extra space
+
+ // Recipes (recipes must be separate paragraphs)
+ }else if(paragraph.startsWith(GuiWizardHandbook.FORMAT_MARKER + GuiWizardHandbook.RECIPE_TAG)){
+
+ String[] arguments = paragraph.split("\\s", 2);
+
+ if(arguments.length < 2) throw new JsonSyntaxException("Missing recipe name in string "
+ + StringUtils.abbreviate(raw, 50));
+
+ CraftingRecipe recipe = GuiWizardHandbook.recipes.get(arguments[1]);
+ if(recipe == null) throw new JsonSyntaxException("Recipe with id " + arguments[1] + " is undefined");
+
+ // Starts a new page if the recipe will not fit on the current one
+ if((lines.size() % maxLineNumber) * font.FONT_HEIGHT + CraftingRecipe.HEIGHT > GuiWizardHandbook.PAGE_HEIGHT){
+ // Remaining number of lines on the page, plus the first blank one on the new page
+ lines.addAll(Collections.nCopies(maxLineNumber - (lines.size() % maxLineNumber), " "));
+ }
+
+ int page = startPage + (lines.size() / maxLineNumber);
+
+ if(lines.size() % maxLineNumber == 0) lines.add(" ");
+ int startLine = lines.size() % maxLineNumber - 1;
+
+ recipe.addInstance(page, GuiWizardHandbook.PAGE_WIDTH / 2 - CraftingRecipe.WIDTH / 2
+ + (GuiWizardHandbook.isRightPage(page) ? GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : GuiWizardHandbook.TEXT_INSET_X),
+ GuiWizardHandbook.TEXT_INSET_Y + startLine * font.FONT_HEIGHT);
+
+ // Height of the recipe in lines, rounded up
+ // Uses a single space instead of an empty string so that the page trimming doesn't remove them
+ lines.addAll(Collections.nCopies(CraftingRecipe.HEIGHT / font.FONT_HEIGHT - 1, " "));
+ // This time we're not adding an extra space because it's not really needed
+
+ }else{ // All other paragraphs
+
+ // Formatting
+ for(Map.Entry entry : GuiWizardHandbook.FORMAT_TAGS.entrySet()){
+ paragraph = paragraph.replace(GuiWizardHandbook.FORMAT_MARKER + entry.getKey(), entry.getValue());
+ }
+
+ // Hyperlinks
+
+ int linkStart;
+
+ while((linkStart = paragraph.indexOf(GuiWizardHandbook.HYPERLINK_MARKER)) > -1){ // Ooh an assignment and a comparison in one...
+
+ int linkEnd = paragraph.indexOf(GuiWizardHandbook.HYPERLINK_MARKER, linkStart + 1);
+
+ if(linkEnd < 0) throw new JsonSyntaxException("Un-closed hyperlink marker in string "
+ + StringUtils.abbreviate(raw, 50));
+
+ List upToLink = font.listFormattedStringToWidth(paragraph.substring(0, linkStart), GuiWizardHandbook.PAGE_WIDTH);
+
+ String linkRaw = paragraph.substring(linkStart, linkEnd + 1);
+ String[] arguments = paragraph.substring(linkStart + 1, linkEnd).split("\\s", 2);
+ String suffix = paragraph.substring(linkEnd).split("\\s", 2)[0].substring(1); // substring(1) to remove the @
+
+ // The index of the single page currently being formatted, relative to the section
+ int pageRelative = (lines.size() + upToLink.size() - 1) / maxLineNumber;
+ // The overall index of the single page currently being formatted
+ int page = startPage + pageRelative;
+ // The line number on this page
+ int lineNumber = (lines.size() + upToLink.size() - 1) % maxLineNumber;
+
+ int x = GuiWizardHandbook.isRightPage(page) ? left + GuiWizardHandbook.GUI_WIDTH - GuiWizardHandbook.TEXT_INSET_X - GuiWizardHandbook.PAGE_WIDTH : left + GuiWizardHandbook.TEXT_INSET_X;
+ int y = top + GuiWizardHandbook.TEXT_INSET_Y + lineNumber * font.FONT_HEIGHT;
+
+ // Adds any missing sub-lists
+ while(this.buttons.size() <= pageRelative){
+ this.buttons.add(new ArrayList<>());
+ }
+
+ // The button id only does what you use it for, so we're just not using it at all.
+ this.buttons.get(pageRelative).add(GuiButtonHyperlink.create(x, y, font, upToLink, arguments, suffix, maxLineNumber - lineNumber - 1, GuiWizardHandbook.isRightPage(page)));
+
+ // The link button should exactly overlay the display rawText in the main string
+ // If the link has no display rawText specified, it displays the unformatted target string
+ paragraph = paragraph.replace(linkRaw, arguments[arguments.length - 1]);
+ }
+
+ lines.addAll(font.listFormattedStringToWidth(paragraph, GuiWizardHandbook.PAGE_WIDTH));
+ }
+
+ // Line break between paragraphs (the last one will just be deleted later)
+ if((lines.size() % maxLineNumber) != 0) lines.add("");
+ }
+ }
+
+ // Splits lines into pages
+
+ List page = new ArrayList<>();
+ pages.add(page);
+
+ while(!lines.isEmpty()){
+
+ if(page.size() == maxLineNumber){
+ // Removes blank lines at the end of the page
+ while(page.get(page.size() - 1).isEmpty()) page.remove(page.size() - 1);
+ // Adds a new page
+ pages.add(page = new ArrayList<>());
+ }
+
+ String line = lines.remove(0);
+
+ // Prevents blank lines at the start of the page
+ if(!page.isEmpty() || !line.isEmpty()) page.add(line);
+ }
+
+ return startPage + pages.size();
+ }
+
+ /**
+ * Parses the given JSON object and constructs a new {@code Section} from it, setting all the relevant fields
+ * and references. This method converts the JSON object to a {@code Section} object and retrieves any resources;
+ * the section is not formatted in any way until GUI load, in {@link Section#format(FontRenderer, int, int, int)}.
+ *
+ * @param json A JSON object representing the section to be constructed. This must contain at least a "title"
+ * string.
+ * @return The resulting {@code Section} object.
+ * @throws JsonSyntaxException if at any point the JSON object is found to be invalid.
+ */
+ static Section fromJson(JsonObject json){
+
+ Section section = new Section();
+
+ section.title = JsonUtils.getString(json, "title", "");
+
+ if(JsonUtils.hasField(json, "include_in_contents")){
+
+ String id = JsonUtils.getString(json, "include_in_contents");
+
+ Contents belongsTo = GuiWizardHandbook.contentsList.get(id);
+
+ if(belongsTo == null){
+ throw new JsonSyntaxException("Expected include_in_contents to be the id of a previously defined contents, but no contents with the id " + id + " exists yet.");
+ }else{
+ belongsTo.addEntry(section);
+ }
+ }
+
+ if(JsonUtils.hasField(json, "contents")){
+ section.contents = Contents.fromJson(section, JsonUtils.getJsonObject(json, "contents"));
+ GuiWizardHandbook.contentsList.put(section.contents.id, section.contents);
+ }
+
+ if(JsonUtils.hasField(json, "text")){
+ section.rawText = Streams.stream(JsonUtils.getJsonArray(json, "text"))
+ .map(e -> JsonUtils.getString(e, "element of array rawText"))
+ .toArray(String[]::new);
+ }
+
+ if(JsonUtils.hasField(json, "triggers")){
+ section.triggers = Streams.stream(JsonUtils.getJsonArray(json, "triggers"))
+ .map(e -> new ResourceLocation(JsonUtils.getString(e, "element of array triggers")))
+ .toArray(ResourceLocation[]::new);
+ // TODO: Can we validate this and throw a JSON exception if no such advancement exists?
+ }
+
+ if(JsonUtils.hasField(json, "centre")){
+ JsonObject centre = JsonUtils.getJsonObject(json,"centre");
+ section.centreX = JsonUtils.getBoolean(centre, "x", false);
+ section.centreY = JsonUtils.getBoolean(centre, "y", false);
+ }
+
+ // The only benefit of having subsections (other than logical grouping) is that the parent section can
+ // automatically be unlocked if one of the subsections is.
+ if(JsonUtils.hasField(json, "sections")){
+ populate(section.subsections, json);
+ }
+
+ return section;
+ }
+
+ static void populate(Map map, JsonObject json){
+
+ JsonObject sectionsObject = JsonUtils.getJsonObject(json, "sections");
+
+ // Need to iterate over these since we don't know what they're called or how many there are
+ for(Map.Entry entry : sectionsObject.entrySet()){
+
+ String key = entry.getKey(); // Find out what each element is called, this will be the sections map key
+
+ Section section = fromJson(entry.getValue().getAsJsonObject());
+ map.put(key, section);
+ map.putAll(section.subsections);
+ }
+ }
+
+ /**
+ * Called on login and advancement completion to update this section's unlock status and display toast
+ * notifications if applicable.
+ */
+ public void updateUnlockStatus(boolean showToasts, ResourceLocation... completedAdvancements){
+
+ if(triggers == null) return;
+
+ List completed = new ArrayList<>(Arrays.asList(completedAdvancements));
+ completed.retainAll(Arrays.asList(triggers));
+
+ // Only shows the toast when the section was locked before and is now unlocked
+ if(!this.unlocked && !completed.isEmpty() && showToasts && Wizardry.settings.handbookProgression){
+ // Mmmmm toast...
+ Minecraft minecraft = Minecraft.getMinecraft();
+ minecraft.getToastGui().add(new HandbookToast(this));
+ this.isNew = true;
+ }
+
+ // Currently, this will not take subsections into account
+ this.unlocked = !completed.isEmpty();
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/model/BakedModelGlowingOverlay.java b/src/main/java/electroblob/wizardry/client/model/BakedModelGlowingOverlay.java
new file mode 100644
index 00000000..c3324830
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/model/BakedModelGlowingOverlay.java
@@ -0,0 +1,234 @@
+package electroblob.wizardry.client.model;
+
+import net.minecraft.block.state.IBlockState;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.block.model.BakedQuad;
+import net.minecraft.client.renderer.block.model.IBakedModel;
+import net.minecraft.client.renderer.block.model.ItemCameraTransforms;
+import net.minecraft.client.renderer.block.model.ItemOverrideList;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.client.renderer.vertex.VertexFormat;
+import net.minecraft.util.EnumFacing;
+import net.minecraftforge.client.model.pipeline.UnpackedBakedQuad;
+import net.minecraftforge.client.model.pipeline.VertexLighterFlat;
+import net.minecraftforge.common.ForgeModContainer;
+import net.minecraftforge.fml.client.FMLClientHandler;
+import org.apache.commons.lang3.tuple.Pair;
+
+import javax.annotation.Nullable;
+import javax.vecmath.Matrix4f;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Custom baked model that stores a list of texture names and sets the lighting to full brightness for any quads
+ * with one of those textures. Most of this code was copied and adapted from refined storage, which is licensed under
+ * the MIT license. https://github.com/raoulvdberge/refinedstorage
+ *
+ * N.B. This doesn't cover item models, and all of the code/model solutions to this that I have tried haven't worked.
+ * However, I noticed that the shade tag seems to work perfectly for items, so instead I've simply duplicated the block
+ * models into the item models folder and add {@code "shade":false} where appropriate. (If it works, it works, right?)
+ *
+ * @author Electroblob
+ * @author raoulvdberge
+ */
+public class BakedModelGlowingOverlay implements IBakedModel {
+
+ // Something something something cache, says Forge
+ // This breaks randomised block models (because it's cached, duh...) but simply including the rand parameter will
+ // completely defeat the point of the cache so I need some way of storing the randomised model variants... hmmm...
+ // See WeightedBakedModel for more on randomisation (it's pretty similar to my 1.7 implementation from years ago)
+
+// private class CacheKey {
+//
+// private IBakedModel base;
+// private String suffix;
+// private IBlockState state;
+// private EnumFacing side;
+//
+// public CacheKey(IBakedModel base, String suffix, IBlockState state, EnumFacing side){
+// this.base = base;
+// this.suffix = suffix;
+// this.state = state;
+// this.side = side;
+// }
+//
+// @Override
+// public boolean equals(Object o){
+//
+// if(this == o) return true;
+// if(o == null || getClass() != o.getClass()) return false;
+//
+// CacheKey cacheKey = (CacheKey)o;
+//
+// if(cacheKey.side != side) return false;
+// if(!state.equals(cacheKey.state)) return false;
+//
+// return true;
+// }
+//
+// @Override
+// public int hashCode() {
+// return state.hashCode() + (31 * (side != null ? side.hashCode() : 0));
+// }
+// }
+//
+// private static final LoadingCache> CACHE = CacheBuilder.newBuilder().build(new CacheLoader>() {
+// @Override
+// public List load(CacheKey key) {
+// return transformQuads(key.base.getQuads(key.state, key.side, 0), key.suffix);
+// }
+// });
+
+ private final IBakedModel delegate;
+ private String suffix;
+
+ public BakedModelGlowingOverlay(IBakedModel delegate, String suffix){
+ this.delegate = delegate;
+ this.suffix = suffix;
+ }
+
+ @Override
+ public List getQuads(@Nullable IBlockState state, @Nullable EnumFacing side, long rand){
+ if(state == null) return delegate.getQuads(state, side, rand);
+ return transformQuads(delegate.getQuads(state, side, rand), suffix);
+ //return CACHE.getUnchecked(new CacheKey(delegate, suffix, state instanceof IExtendedBlockState ? ((IExtendedBlockState) state).getClean() : state, side));
+ }
+
+ // I would write these myself but I'd end up with almost the exact same thing anyway
+ // They replace the quads from the original (delegate) model with full-brightness quads if their texture has the given suffix
+
+ private static List transformQuads(List oldQuads, String suffix){
+
+ List quads = new ArrayList<>(oldQuads);
+
+ for(int i = 0; i < quads.size(); ++i){
+ BakedQuad quad = quads.get(i);
+
+ if(quad.getSprite().getIconName().endsWith(suffix)){
+ quads.set(i, transformQuad(quad, 0.007F)); // What's the significance of 0.007?
+ }
+ }
+
+ return quads;
+ }
+
+ private static BakedQuad transformQuad(BakedQuad quad, float light){
+
+ if(isLightMapDisabled()){
+ return quad;
+ }
+
+ VertexFormat newFormat = getFormatWithLightMap(quad.getFormat());
+
+ UnpackedBakedQuad.Builder builder = new UnpackedBakedQuad.Builder(newFormat);
+
+ VertexLighterFlat trans = new VertexLighterFlat(Minecraft.getMinecraft().getBlockColors()) {
+ @Override
+ protected void updateLightmap(float[] normal, float[] lightmap, float x, float y, float z){
+ lightmap[0] = light;
+ lightmap[1] = light;
+ }
+
+ @Override
+ public void setQuadTint(int tint){
+ // NO OP
+ }
+ };
+
+ trans.setParent(builder);
+
+ quad.pipe(trans);
+
+ builder.setQuadTint(quad.getTintIndex());
+ builder.setQuadOrientation(quad.getFace());
+ builder.setTexture(quad.getSprite());
+ builder.setApplyDiffuseLighting(false);
+
+ return builder.build();
+ }
+
+ @Override
+ public boolean isAmbientOcclusion(){
+ return delegate.isAmbientOcclusion();
+ }
+
+ @Override
+ public boolean isGui3d(){
+ return delegate.isGui3d();
+ }
+
+ @Override
+ public boolean isBuiltInRenderer(){
+ return delegate.isBuiltInRenderer();
+ }
+
+ @Override
+ public TextureAtlasSprite getParticleTexture(){
+ return delegate.getParticleTexture();
+ }
+
+ @Override
+ public ItemCameraTransforms getItemCameraTransforms(){
+ return delegate.getItemCameraTransforms();
+ }
+
+ @Override
+ public ItemOverrideList getOverrides(){
+ return delegate.getOverrides();//BakedModelItemOverride.instance;
+ }
+
+ @Override
+ public boolean isAmbientOcclusion(IBlockState state){
+ return delegate.isAmbientOcclusion(state);
+ }
+
+ @Override
+ public Pair extends IBakedModel, Matrix4f> handlePerspective(ItemCameraTransforms.TransformType cameraTransformType){
+ return delegate.handlePerspective(cameraTransformType);
+ }
+
+ // Utilities
+
+ private static boolean isLightMapDisabled(){
+ return FMLClientHandler.instance().hasOptifine() || !ForgeModContainer.forgeLightPipelineEnabled;
+ }
+
+ private static final VertexFormat ITEM_FORMAT_WITH_LIGHTMAP = new VertexFormat(DefaultVertexFormats.ITEM).addElement(DefaultVertexFormats.TEX_2S);
+
+ private static VertexFormat getFormatWithLightMap(VertexFormat format){
+
+ if(isLightMapDisabled()){
+ return format;
+ }
+
+ if(format == DefaultVertexFormats.BLOCK){
+ return DefaultVertexFormats.BLOCK;
+ }else if(format == DefaultVertexFormats.ITEM){
+ return ITEM_FORMAT_WITH_LIGHTMAP;
+ }else if(!format.hasUvOffset(1)){
+ VertexFormat result = new VertexFormat(format);
+ result.addElement(DefaultVertexFormats.TEX_2S);
+ return result;
+ }
+
+ return format;
+ }
+
+ // Bit of a weird way of doing things if you ask me, but as far as I can tell it's how you're supposed to do it
+// public static final class BakedModelItemOverride extends ItemOverrideList {
+//
+// // This class doesn't contain any data of its own so it can be a singleton
+// public static final BakedModelItemOverride instance = new BakedModelItemOverride();
+//
+// private BakedModelItemOverride(){
+// super(ImmutableList.of()); // We're not using the list functionality
+// }
+//
+// @Override
+// public IBakedModel handleItemState(IBakedModel originalModel, ItemStack stack, @Nullable World world, @Nullable EntityLivingBase entity){
+// return new BakedModelGlowingOverlay(originalModel, "overlay"); // Bish bash bosh
+// }
+// }
+}
diff --git a/src/main/java/electroblob/wizardry/client/model/ModelWtfMojang.java b/src/main/java/electroblob/wizardry/client/model/ModelArmourFixer.java
similarity index 90%
rename from src/main/java/electroblob/wizardry/client/model/ModelWtfMojang.java
rename to src/main/java/electroblob/wizardry/client/model/ModelArmourFixer.java
index ced31285..793f21e8 100644
--- a/src/main/java/electroblob/wizardry/client/model/ModelWtfMojang.java
+++ b/src/main/java/electroblob/wizardry/client/model/ModelArmourFixer.java
@@ -4,9 +4,14 @@ import net.minecraft.client.model.ModelBiped;
import net.minecraft.entity.Entity;
import net.minecraft.entity.item.EntityArmorStand;
-public class ModelWtfMojang extends ModelBiped {
+/**
+ * Fixes custom armour models 'breathing' on the stand and rotates the helmet properly.
+ * @author Shadows-of-Fire
+ * @since Wizardry 4.1.2
+ */
+public class ModelArmourFixer extends ModelBiped {
- public ModelWtfMojang(float modelSize, float rotationYOffset, int textureWidth, int textureHeight) {
+ public ModelArmourFixer(float modelSize, float rotationYOffset, int textureWidth, int textureHeight) {
super(modelSize, rotationYOffset, textureWidth, textureHeight);
}
diff --git a/src/main/java/electroblob/wizardry/client/model/ModelHammer.java b/src/main/java/electroblob/wizardry/client/model/ModelHammer.java
index 85a164f7..c28c4d6e 100644
--- a/src/main/java/electroblob/wizardry/client/model/ModelHammer.java
+++ b/src/main/java/electroblob/wizardry/client/model/ModelHammer.java
@@ -5,64 +5,71 @@ import net.minecraft.client.model.ModelRenderer;
import net.minecraft.entity.Entity;
public class ModelHammer extends ModelBase {
- ModelRenderer Shape1;
- ModelRenderer Shape2;
- ModelRenderer Shape3;
- ModelRenderer Shape4;
- ModelRenderer Shape5;
- ModelRenderer Shape6;
+
+ ModelRenderer hammerHead;
+ ModelRenderer handle;
+ ModelRenderer handleEnd;
+ ModelRenderer handleBase;
+ ModelRenderer ring1;
+ ModelRenderer ring2;
public ModelHammer(){
+
textureWidth = 64;
textureHeight = 64;
- Shape1 = new ModelRenderer(this, 0, 0);
- Shape1.addBox(0F, 0F, 0F, 20, 12, 12);
- Shape1.setRotationPoint(-10F, 12F, -6F);
- Shape1.setTextureSize(64, 64);
- Shape1.mirror = true;
- setRotation(Shape1, 0F, 0F, 0F);
- Shape2 = new ModelRenderer(this, 0, 24);
- Shape2.addBox(0F, 0F, 0F, 4, 14, 4);
- Shape2.setRotationPoint(-2F, -2F, -2F);
- Shape2.setTextureSize(64, 64);
- Shape2.mirror = true;
- setRotation(Shape2, 0F, 0F, 0F);
- Shape3 = new ModelRenderer(this, 0, 49);
- Shape3.addBox(0F, 0F, 0F, 5, 5, 5);
- Shape3.setRotationPoint(-2.5F, -7F, -2.5F);
- Shape3.setTextureSize(64, 64);
- Shape3.mirror = true;
- setRotation(Shape3, 0F, 0F, 0F);
- Shape4 = new ModelRenderer(this, 0, 42);
- Shape4.addBox(0F, 0F, 0F, 5, 2, 5);
- Shape4.setRotationPoint(-2.5F, 10F, -2.5F);
- Shape4.setTextureSize(64, 64);
- Shape4.mirror = true;
- setRotation(Shape4, 0F, 0F, 0F);
- Shape5 = new ModelRenderer(this, 20, 24);
- Shape5.addBox(0F, 0F, 0F, 2, 14, 14);
- Shape5.setRotationPoint(-8F, 11F, -7F);
- Shape5.setTextureSize(64, 64);
- Shape5.mirror = true;
- setRotation(Shape5, 0F, 0F, 0F);
- Shape6 = new ModelRenderer(this, 20, 24);
- Shape6.addBox(0F, 0F, 0F, 2, 14, 14);
- Shape6.setRotationPoint(6F, 11F, -7F);
- Shape6.setTextureSize(64, 64);
- Shape6.mirror = true;
- setRotation(Shape6, 0F, 0F, 0F);
+ hammerHead = new ModelRenderer(this, 0, 0);
+ hammerHead.addBox(0F, 0F, 0F, 20, 12, 12);
+ hammerHead.setRotationPoint(-10F, 12F, -6F);
+ hammerHead.setTextureSize(64, 64);
+ hammerHead.mirror = true;
+ setRotation(hammerHead, 0F, 0F, 0F);
+
+ handle = new ModelRenderer(this, 0, 24);
+ handle.addBox(0F, 0F, 0F, 4, 14, 4);
+ handle.setRotationPoint(-2F, -2F, -2F);
+ handle.setTextureSize(64, 64);
+ handle.mirror = true;
+ setRotation(handle, 0F, 0F, 0F);
+
+ handleEnd = new ModelRenderer(this, 0, 49);
+ handleEnd.addBox(0F, 0F, 0F, 5, 5, 5);
+ handleEnd.setRotationPoint(-2.5F, -7F, -2.5F);
+ handleEnd.setTextureSize(64, 64);
+ handleEnd.mirror = true;
+ setRotation(handleEnd, 0F, 0F, 0F);
+
+ handleBase = new ModelRenderer(this, 0, 42);
+ handleBase.addBox(0F, 0F, 0F, 5, 2, 5);
+ handleBase.setRotationPoint(-2.5F, 10F, -2.5F);
+ handleBase.setTextureSize(64, 64);
+ handleBase.mirror = true;
+ setRotation(handleBase, 0F, 0F, 0F);
+
+ ring1 = new ModelRenderer(this, 20, 24);
+ ring1.addBox(0F, 0F, 0F, 2, 14, 14);
+ ring1.setRotationPoint(-8F, 11F, -7F);
+ ring1.setTextureSize(64, 64);
+ ring1.mirror = true;
+ setRotation(ring1, 0F, 0F, 0F);
+
+ ring2 = new ModelRenderer(this, 20, 24);
+ ring2.addBox(0F, 0F, 0F, 2, 14, 14);
+ ring2.setRotationPoint(6F, 11F, -7F);
+ ring2.setTextureSize(64, 64);
+ ring2.mirror = true;
+ setRotation(ring2, 0F, 0F, 0F);
}
public void render(Entity entity, float f, float f1, float f2, float f3, float f4, float f5){
super.render(entity, f, f1, f2, f3, f4, f5);
setRotationAngles(f, f1, f2, f3, f4, f5, entity);
- Shape1.render(f5);
- Shape2.render(f5);
- Shape3.render(f5);
- Shape4.render(f5);
- Shape5.render(f5);
- Shape6.render(f5);
+ hammerHead.render(f5);
+ handle.render(f5);
+ handleEnd.render(f5);
+ handleBase.render(f5);
+ ring1.render(f5);
+ ring2.render(f5);
}
private void setRotation(ModelRenderer model, float x, float y, float z){
diff --git a/src/main/java/electroblob/wizardry/client/model/ModelIceGiant.java b/src/main/java/electroblob/wizardry/client/model/ModelIceGiant.java
index 798cfcb4..592f0208 100644
--- a/src/main/java/electroblob/wizardry/client/model/ModelIceGiant.java
+++ b/src/main/java/electroblob/wizardry/client/model/ModelIceGiant.java
@@ -1,18 +1,16 @@
package electroblob.wizardry.client.model;
-import javax.vecmath.Matrix4f;
-import javax.vecmath.Vector3f;
-
import electroblob.wizardry.entity.living.EntityIceGiant;
import net.minecraft.client.model.ModelBase;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.util.math.MathHelper;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
+import javax.vecmath.Matrix4f;
+import javax.vecmath.Vector3f;
+
+//@SideOnly(Side.CLIENT)
public class ModelIceGiant extends ModelBase {
/** The head model for the iron golem. */
public ModelRenderer iceGiantHead;
diff --git a/src/main/java/electroblob/wizardry/client/model/ModelWizard.java b/src/main/java/electroblob/wizardry/client/model/ModelWizard.java
index 0204c86a..8e7a469d 100644
--- a/src/main/java/electroblob/wizardry/client/model/ModelWizard.java
+++ b/src/main/java/electroblob/wizardry/client/model/ModelWizard.java
@@ -97,9 +97,9 @@ public class ModelWizard extends ModelBiped {
setRotation(Shape13, 0F, 0F, 0F);
// Makes head bits move with head
- // bipedHead.addChild(Shape5);
+ // bipedHead.addChild(hatSegment4);
bipedHead.addChild(beard);
- // bipedHead.addChild(Shape7);
+ // bipedHead.addChild(hatSegment6);
// bipedHead.addChild(Shape8);
// bipedHead.addChild(Shape9);
// bipedHead.addChild(Shape10);
@@ -116,8 +116,8 @@ public class ModelWizard extends ModelBiped {
/* public void render(Entity entity, float f, float f1, float f2, float f3, float f4, float f5) {
* super.render(entity, f, f1, f2, f3, f4, f5); setRotationAngles(f, f1, f2, f3, f4, f5, entity);
* bipedRightLeg.render(f5); bipedLeftLeg.render(f5); bipedBody.render(f5); bipedLeftArm.render(f5);
- * bipedRightArm.render(f5); bipedHead.render(f5); Shape5.render(f5); Shape8.render(f5); Shape9.render(f5);
- * Shape10.render(f5); Shape7.render(f5); Shape11.render(f5); Shape12.render(f5); Shape6.render(f5);
+ * bipedRightArm.render(f5); bipedHead.render(f5); hatSegment4.render(f5); Shape8.render(f5); Shape9.render(f5);
+ * Shape10.render(f5); hatSegment6.render(f5); Shape11.render(f5); Shape12.render(f5); hatSegment5.render(f5);
* Shape13.render(f5); } */
private void setRotation(ModelRenderer model, float x, float y, float z){
model.rotateAngleX = x;
diff --git a/src/main/java/electroblob/wizardry/client/model/ModelWizardArmour.java b/src/main/java/electroblob/wizardry/client/model/ModelWizardArmour.java
index ee371d3c..2bebd9c0 100644
--- a/src/main/java/electroblob/wizardry/client/model/ModelWizardArmour.java
+++ b/src/main/java/electroblob/wizardry/client/model/ModelWizardArmour.java
@@ -1,99 +1,110 @@
package electroblob.wizardry.client.model;
+import electroblob.wizardry.block.BlockStatue;
import net.minecraft.client.model.ModelRenderer;
import net.minecraft.entity.Entity;
-public class ModelWizardArmour extends ModelWtfMojang {
- ModelRenderer Shape1;
- ModelRenderer Shape2;
- ModelRenderer Shape3;
- ModelRenderer Shape4;
- ModelRenderer Shape5;
- ModelRenderer Shape6;
- ModelRenderer Shape7;
+public class ModelWizardArmour extends ModelArmourFixer {
+
+ ModelRenderer hatBrim;
+ ModelRenderer hatSegment1;
+ ModelRenderer hatSegment2;
+ ModelRenderer hatSegment3;
+ ModelRenderer hatSegment4;
+ ModelRenderer hatSegment5;
+ ModelRenderer hatSegment6;
ModelRenderer robe;
- public ModelWizardArmour(float scale){
+ public ModelWizardArmour(float delta){
- super(scale, 0, 64, 64);
+ super(delta, 0, 64, 64);
// This is necessary to stop the head from scaling.
this.bipedHead = new ModelRenderer(this, 0, 0);
- this.bipedHead.addBox(-4.0F, -8.0F, -4.0F, 8, 8, 8, 0.1f);
+ // The hat layer has an offset of 0.5, so 0.6 is about the smallest we can get away with
+ this.bipedHead.addBox(-4.0F, -8.0F, -4.0F, 8, 8, 8, 0.6f);
this.bipedHead.setRotationPoint(0.0F, 0.0F + 0, 0.0F);
- Shape1 = new ModelRenderer(this, -16, 32);
- Shape1.addBox(-8F, -7F, -8F, 16, 0, 16);
- Shape1.setRotationPoint(0F, 0F, 0F);
- Shape1.setTextureSize(64, 64);
- Shape1.mirror = true;
- setRotation(Shape1, 0F, 0F, 0F);
+ hatBrim = new ModelRenderer(this, 0, 47);
+ // Making the height 1 stops the top and bottom z-fighting when the hat is enchanted
+ hatBrim.addBox(-8F, -6.85F, -8F, 16, 1, 16, 0.6f);
+ hatBrim.setRotationPoint(0F, 0F, 0F);
+ hatBrim.setTextureSize(64, 64);
+ hatBrim.mirror = true;
+ setRotation(hatBrim, 0F, 0F, 0F);
- Shape2 = new ModelRenderer(this, 0, 48);
- Shape2.addBox(0F, 0F, 0F, 6, 2, 6);
- Shape2.setRotationPoint(-3F, -10F, -3F);
- Shape2.setTextureSize(64, 64);
- Shape2.mirror = true;
- setRotation(Shape2, -0.1396263F, 0F, 0F);
+ hatSegment1 = new ModelRenderer(this, 0, 32);
+ hatSegment1.addBox(0F, 0F, 0F, 6, 2, 6, 0.2f);
+ hatSegment1.setRotationPoint(-3F, -10.6F, -3F);
+ hatSegment1.setTextureSize(64, 64);
+ hatSegment1.mirror = true;
+ setRotation(hatSegment1, -0.1396263F, 0F, 0F);
- Shape3 = new ModelRenderer(this, 0, 56);
- Shape3.addBox(0F, 0F, 0F, 5, 2, 5);
- Shape3.setRotationPoint(-2.5F, -11.53333F, -2F);
- Shape3.setTextureSize(64, 64);
- Shape3.mirror = true;
- setRotation(Shape3, -0.2443461F, 0F, 0F);
+ hatSegment2 = new ModelRenderer(this, 0, 40);
+ hatSegment2.addBox(0F, 0F, 0F, 5, 2, 5, 0.1f);
+ hatSegment2.setRotationPoint(-2.5F, -12.13333F, -2F);
+ hatSegment2.setTextureSize(64, 64);
+ hatSegment2.mirror = true;
+ setRotation(hatSegment2, -0.2443461F, 0F, 0F);
- Shape4 = new ModelRenderer(this, 24, 48);
- Shape4.addBox(0F, 0F, 0F, 4, 2, 4);
- Shape4.setRotationPoint(-2F, -13F, -1F);
- Shape4.setTextureSize(64, 64);
- Shape4.mirror = true;
- setRotation(Shape4, -0.4014257F, 0F, 0F);
+ hatSegment3 = new ModelRenderer(this, 24, 32);
+ hatSegment3.addBox(0F, 0F, 0F, 4, 2, 4);
+ hatSegment3.setRotationPoint(-2F, -13.6F, -1F);
+ hatSegment3.setTextureSize(64, 64);
+ hatSegment3.mirror = true;
+ setRotation(hatSegment3, -0.4014257F, 0F, 0F);
- Shape5 = new ModelRenderer(this, 24, 54);
- Shape5.addBox(0F, 0F, 0F, 3, 2, 3);
- Shape5.setRotationPoint(-1.5F, -14F, 0F);
- Shape5.setTextureSize(64, 64);
- Shape5.mirror = true;
- setRotation(Shape5, -0.5759587F, 0F, 0F);
+ hatSegment4 = new ModelRenderer(this, 24, 38);
+ hatSegment4.addBox(0F, 0F, 0F, 3, 2, 3);
+ hatSegment4.setRotationPoint(-1.5F, -14.6F, 0F);
+ hatSegment4.setTextureSize(64, 64);
+ hatSegment4.mirror = true;
+ setRotation(hatSegment4, -0.5759587F, 0F, 0F);
- Shape6 = new ModelRenderer(this, 20, 59);
- Shape6.addBox(0F, 0F, 0F, 2, 2, 2);
- Shape6.setRotationPoint(-1F, -14F, 0F);
- Shape6.setTextureSize(64, 64);
- Shape6.mirror = true;
- setRotation(Shape6, 0.3316126F, 0F, 0F);
+ hatSegment5 = new ModelRenderer(this, 20, 43);
+ hatSegment5.addBox(0F, 0F, 0F, 2, 2, 2);
+ hatSegment5.setRotationPoint(-1F, -14.6F, 0F);
+ hatSegment5.setTextureSize(64, 64);
+ hatSegment5.mirror = true;
+ setRotation(hatSegment5, 0.3316126F, 0F, 0F);
- Shape7 = new ModelRenderer(this, 28, 59);
- Shape7.addBox(0F, 0F, 0F, 1, 1, 3);
- Shape7.setRotationPoint(-0.5F, -14.5F, 2F);
- Shape7.setTextureSize(64, 64);
- Shape7.mirror = true;
- setRotation(Shape7, -0.5585054F, 0F, 0F);
+ hatSegment6 = new ModelRenderer(this, 28, 43);
+ hatSegment6.addBox(0F, 0F, 0F, 1, 1, 3);
+ hatSegment6.setRotationPoint(-0.5F, -15.1F, 2F);
+ hatSegment6.setTextureSize(64, 64);
+ hatSegment6.mirror = true;
+ setRotation(hatSegment6, -0.5585054F, 0F, 0F);
- // The robe is now the body
- bipedBody = new ModelRenderer(this, 40, 42);
- bipedBody.addBox(-4F, 0F, -2F, 8, 18, 4, scale);
+ bipedBody = new ModelRenderer(this, 16, 16);
+ bipedBody.addBox(-4F, 0F, -2F, 8, 11, 4, delta);
bipedBody.setRotationPoint(0F, 0F, 0F);
bipedBody.setTextureSize(64, 64);
bipedBody.mirror = true;
setRotation(bipedBody, 0F, 0F, 0F);
+ robe = new ModelRenderer(this, 40, 32);
+ robe.addBox(-4F, 0F, -2F, 8, 7, 4, delta);
+ robe.setRotationPoint(0F, 12, 0F); // 12.5 accounts for the expansion of each box
+ robe.setTextureSize(64, 64);
+ robe.mirror = true;
+ setRotation(robe, 0F, 0F, 0F);
+
// Makes the hat rotate with the head.
- bipedHead.addChild(Shape1);
- bipedHead.addChild(Shape2);
- bipedHead.addChild(Shape3);
- bipedHead.addChild(Shape4);
- bipedHead.addChild(Shape5);
- bipedHead.addChild(Shape6);
- bipedHead.addChild(Shape7);
- // Makes the robe move with the body
- // bipedBody.addChild(robe);
+ bipedHead.addChild(hatBrim);
+ bipedHead.addChild(hatSegment1);
+ bipedHead.addChild(hatSegment2);
+ bipedHead.addChild(hatSegment3);
+ bipedHead.addChild(hatSegment4);
+ bipedHead.addChild(hatSegment5);
+ bipedHead.addChild(hatSegment6);
}
public void render(Entity entity, float f, float f1, float f2, float f3, float f4, float f5){
+ if(entity.isInvisible() && !entity.getEntityData().getBoolean(BlockStatue.PETRIFIED_NBT_KEY)
+ && !entity.getEntityData().getBoolean(BlockStatue.FROZEN_NBT_KEY)) return;
super.render(entity, f, f1, f2, f3, f4, f5);
setRotationAngles(f, f1, f2, f3, f4, f5, entity);
+ this.robe.render(f5);
}
private void setRotation(ModelRenderer model, float x, float y, float z){
@@ -104,6 +115,20 @@ public class ModelWizardArmour extends ModelWtfMojang {
public void setRotationAngles(float f, float f1, float f2, float f3, float f4, float f5, Entity entity){
super.setRotationAngles(f, f1, f2, f3, f4, f5, entity);
+ this.robe.showModel = this.bipedBody.showModel;
+ if(this.isSneak){
+ //this.robe.rotationPointY = 10.5f;
+ this.robe.rotationPointZ = 4;
+ }else{
+ //this.robe.rotationPointY = 12.5f;
+ this.robe.rotationPointZ = 0;
+ }
+
+ // The bottom part of the robe takes the y rotation from the rest of the robe but the x/z rotation
+ // from the average of the two legs
+ this.robe.rotateAngleX = (this.bipedLeftLeg.rotateAngleX + this.bipedRightLeg.rotateAngleX) / 2f;
+ this.robe.rotateAngleY = this.bipedBody.rotateAngleY;
+ this.robe.rotateAngleZ = (this.bipedLeftLeg.rotateAngleZ + this.bipedRightLeg.rotateAngleZ) / 2f;
}
}
diff --git a/src/main/java/electroblob/wizardry/client/model/WizardryItemModels.java b/src/main/java/electroblob/wizardry/client/model/WizardryItemModels.java
deleted file mode 100644
index 0f5cec39..00000000
--- a/src/main/java/electroblob/wizardry/client/model/WizardryItemModels.java
+++ /dev/null
@@ -1,198 +0,0 @@
-package electroblob.wizardry.client.model;
-
-import electroblob.wizardry.registry.WizardryBlocks;
-import electroblob.wizardry.registry.WizardryItems;
-import net.minecraft.client.renderer.block.model.ModelResourceLocation;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.NonNullList;
-import net.minecraftforge.client.event.ModelRegistryEvent;
-import net.minecraftforge.client.model.ModelLoader;
-import net.minecraftforge.fml.common.Mod;
-import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-import net.minecraftforge.oredict.OreDictionary;
-
-/**
- * Class responsible for registering all of wizardry's item (and itemblock) models.
- *
- * @author Electroblob
- * @since Wizardry 2.1
- */
-@SideOnly(Side.CLIENT)
-@Mod.EventBusSubscriber(Side.CLIENT)
-public final class WizardryItemModels {
-
- @SubscribeEvent
- public static void register(ModelRegistryEvent event){
-
- // ItemBlocks
-
- registerItemModel(Item.getItemFromBlock(WizardryBlocks.arcane_workbench));
- registerItemModel(Item.getItemFromBlock(WizardryBlocks.crystal_ore));
- registerItemModel(Item.getItemFromBlock(WizardryBlocks.crystal_flower));
- registerItemModel(Item.getItemFromBlock(WizardryBlocks.transportation_stone));
- registerItemModel(Item.getItemFromBlock(WizardryBlocks.crystal_block));
-
- // Items
-
- registerItemModel(WizardryItems.magic_crystal);
-
- registerItemModel(WizardryItems.magic_wand);
- registerItemModel(WizardryItems.apprentice_wand);
- registerItemModel(WizardryItems.advanced_wand);
- registerItemModel(WizardryItems.master_wand);
-
- registerItemModel(WizardryItems.spell_book);
- // Wildcard registered for wizard trades.
- registerItemModel(WizardryItems.spell_book, OreDictionary.WILDCARD_VALUE, "normal");
- registerItemModel(WizardryItems.arcane_tome);
- registerItemModel(WizardryItems.wizard_handbook);
-
- registerItemModel(WizardryItems.basic_fire_wand);
- registerItemModel(WizardryItems.basic_ice_wand);
- registerItemModel(WizardryItems.basic_lightning_wand);
- registerItemModel(WizardryItems.basic_necromancy_wand);
- registerItemModel(WizardryItems.basic_earth_wand);
- registerItemModel(WizardryItems.basic_sorcery_wand);
- registerItemModel(WizardryItems.basic_healing_wand);
-
- registerItemModel(WizardryItems.apprentice_fire_wand);
- registerItemModel(WizardryItems.apprentice_ice_wand);
- registerItemModel(WizardryItems.apprentice_lightning_wand);
- registerItemModel(WizardryItems.apprentice_necromancy_wand);
- registerItemModel(WizardryItems.apprentice_earth_wand);
- registerItemModel(WizardryItems.apprentice_sorcery_wand);
- registerItemModel(WizardryItems.apprentice_healing_wand);
-
- registerItemModel(WizardryItems.advanced_fire_wand);
- registerItemModel(WizardryItems.advanced_ice_wand);
- registerItemModel(WizardryItems.advanced_lightning_wand);
- registerItemModel(WizardryItems.advanced_necromancy_wand);
- registerItemModel(WizardryItems.advanced_earth_wand);
- registerItemModel(WizardryItems.advanced_sorcery_wand);
- registerItemModel(WizardryItems.advanced_healing_wand);
-
- registerItemModel(WizardryItems.master_fire_wand);
- registerItemModel(WizardryItems.master_ice_wand);
- registerItemModel(WizardryItems.master_lightning_wand);
- registerItemModel(WizardryItems.master_necromancy_wand);
- registerItemModel(WizardryItems.master_earth_wand);
- registerItemModel(WizardryItems.master_sorcery_wand);
- registerItemModel(WizardryItems.master_healing_wand);
-
- registerItemModel(WizardryItems.spectral_sword);
- registerItemModel(WizardryItems.spectral_pickaxe);
- registerItemModel(WizardryItems.spectral_bow);
-
- registerItemModel(WizardryItems.mana_flask);
-
- registerItemModel(WizardryItems.storage_upgrade);
- registerItemModel(WizardryItems.siphon_upgrade);
- registerItemModel(WizardryItems.condenser_upgrade);
- registerItemModel(WizardryItems.range_upgrade);
- registerItemModel(WizardryItems.duration_upgrade);
- registerItemModel(WizardryItems.cooldown_upgrade);
- registerItemModel(WizardryItems.blast_upgrade);
- registerItemModel(WizardryItems.attunement_upgrade);
-
- registerItemModel(WizardryItems.flaming_axe);
- registerItemModel(WizardryItems.frost_axe);
-
- registerItemModel(WizardryItems.firebomb);
- registerItemModel(WizardryItems.poison_bomb);
-
- registerItemModel(WizardryItems.blank_scroll);
- registerItemModel(WizardryItems.scroll);
-
- registerItemModel(WizardryItems.armour_upgrade);
-
- registerItemModel(WizardryItems.magic_silk);
-
- registerItemModel(WizardryItems.wizard_hat);
- registerItemModel(WizardryItems.wizard_robe);
- registerItemModel(WizardryItems.wizard_leggings);
- registerItemModel(WizardryItems.wizard_boots);
-
- registerItemModel(WizardryItems.wizard_hat_fire);
- registerItemModel(WizardryItems.wizard_robe_fire);
- registerItemModel(WizardryItems.wizard_leggings_fire);
- registerItemModel(WizardryItems.wizard_boots_fire);
-
- registerItemModel(WizardryItems.wizard_hat_ice);
- registerItemModel(WizardryItems.wizard_robe_ice);
- registerItemModel(WizardryItems.wizard_leggings_ice);
- registerItemModel(WizardryItems.wizard_boots_ice);
-
- registerItemModel(WizardryItems.wizard_hat_lightning);
- registerItemModel(WizardryItems.wizard_robe_lightning);
- registerItemModel(WizardryItems.wizard_leggings_lightning);
- registerItemModel(WizardryItems.wizard_boots_lightning);
-
- registerItemModel(WizardryItems.wizard_hat_necromancy);
- registerItemModel(WizardryItems.wizard_robe_necromancy);
- registerItemModel(WizardryItems.wizard_leggings_necromancy);
- registerItemModel(WizardryItems.wizard_boots_necromancy);
-
- registerItemModel(WizardryItems.wizard_hat_earth);
- registerItemModel(WizardryItems.wizard_robe_earth);
- registerItemModel(WizardryItems.wizard_leggings_earth);
- registerItemModel(WizardryItems.wizard_boots_earth);
-
- registerItemModel(WizardryItems.wizard_hat_sorcery);
- registerItemModel(WizardryItems.wizard_robe_sorcery);
- registerItemModel(WizardryItems.wizard_leggings_sorcery);
- registerItemModel(WizardryItems.wizard_boots_sorcery);
-
- registerItemModel(WizardryItems.wizard_hat_healing);
- registerItemModel(WizardryItems.wizard_robe_healing);
- registerItemModel(WizardryItems.wizard_leggings_healing);
- registerItemModel(WizardryItems.wizard_boots_healing);
-
- registerItemModel(WizardryItems.spectral_helmet);
- registerItemModel(WizardryItems.spectral_chestplate);
- registerItemModel(WizardryItems.spectral_leggings);
- registerItemModel(WizardryItems.spectral_boots);
-
- registerItemModel(WizardryItems.smoke_bomb);
-
- registerItemModel(WizardryItems.identification_scroll);
- }
-
- // Moved from the proxies
-
- /**
- * Registers an item model, using the item's registry name as the model name (this convention makes it easier to
- * keep track of everything). Variant defaults to "normal". Registers the model for metadata 0 automatically, plus
- * all the other metadata values that the item can take, as defined in
- * {@link Item#getSubItems(Item, net.minecraft.creativetab.CreativeTabs, java.util.List)}. The passed in item
- * must allow null to be passed in for the creative tab parameter in the aforementioned method, or a
- * {@link NullPointerException} will result.
- */
- private static void registerItemModel(Item item){
-
- if(item.getHasSubtypes()){
- NonNullList items = NonNullList.create();
- item.getSubItems(item.getCreativeTab(), items); // Client-only method, but we're client-side so this is OK.
- for(ItemStack stack : items){
- ModelLoader.setCustomModelResourceLocation(item, stack.getMetadata(),
- new ModelResourceLocation(item.getRegistryName(), "inventory"));
- }
- }
- // Changing the last parameter from null to "inventory" fixed the item/block model weirdness. No idea why!
- ModelLoader.setCustomModelResourceLocation(item, 0,
- new ModelResourceLocation(item.getRegistryName(), "inventory"));
- }
-
- /**
- * Registers an item model for the given metadata, using the item's registry name as the model name (this convention
- * makes it easier to keep track of everything). This is intended for registering additional metadata values which
- * aren't displayed in the creative menu, for example the wildcard spell book used in wizard trades.
- */
- private static void registerItemModel(Item item, int metadata, String variant){
- ModelLoader.setCustomModelResourceLocation(item, metadata,
- new ModelResourceLocation(item.getRegistryName(), variant));
- }
-
-}
diff --git a/src/main/java/electroblob/wizardry/client/model/WizardryModels.java b/src/main/java/electroblob/wizardry/client/model/WizardryModels.java
new file mode 100644
index 00000000..a6f2529e
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/model/WizardryModels.java
@@ -0,0 +1,343 @@
+package electroblob.wizardry.client.model;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.block.BlockCrystal;
+import electroblob.wizardry.block.BlockPedestal;
+import electroblob.wizardry.block.BlockRunestone;
+import electroblob.wizardry.item.IMultiTexturedItem;
+import electroblob.wizardry.item.ItemBlockMultiTexturedElemental;
+import electroblob.wizardry.item.ItemCrystal;
+import electroblob.wizardry.registry.WizardryBlocks;
+import electroblob.wizardry.registry.WizardryItems;
+import net.minecraft.client.renderer.block.model.IBakedModel;
+import net.minecraft.client.renderer.block.model.ModelResourceLocation;
+import net.minecraft.client.renderer.block.statemap.StateMap;
+import net.minecraft.creativetab.CreativeTabs;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.NonNullList;
+import net.minecraftforge.client.event.ModelBakeEvent;
+import net.minecraftforge.client.event.ModelRegistryEvent;
+import net.minecraftforge.client.model.ModelLoader;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import net.minecraftforge.oredict.OreDictionary;
+
+/**
+ * Class responsible for registering all of wizardry's item and block models.
+ *
+ * @author Electroblob
+ * @since Wizardry 2.1
+ */
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public final class WizardryModels {
+
+ private WizardryModels(){} // No instances!
+
+ @SubscribeEvent
+ public static void register(ModelRegistryEvent event){
+
+ // ItemBlocks
+
+ registerItemModel(Item.getItemFromBlock(WizardryBlocks.arcane_workbench));
+ registerItemModel(Item.getItemFromBlock(WizardryBlocks.crystal_ore));
+ registerItemModel(Item.getItemFromBlock(WizardryBlocks.crystal_flower));
+ registerItemModel(Item.getItemFromBlock(WizardryBlocks.transportation_stone));
+
+ ModelLoader.setCustomStateMapper(WizardryBlocks.crystal_block, new StateMap.Builder()
+ .withName(BlockCrystal.ELEMENT).withSuffix("_crystal_block").build());
+ // Yay unchecked casting! But we know it's always ok here, and it makes everything much neater.
+ ItemBlockMultiTexturedElemental crystalBlockItem = (ItemBlockMultiTexturedElemental)Item.getItemFromBlock(WizardryBlocks.crystal_block);
+ registerMultiTexturedModel(crystalBlockItem);
+
+ ModelLoader.setCustomStateMapper(WizardryBlocks.runestone, new StateMap.Builder()
+ .withName(BlockRunestone.ELEMENT).withSuffix("_runestone").build());
+ ItemBlockMultiTexturedElemental runestoneItem = (ItemBlockMultiTexturedElemental)Item.getItemFromBlock(WizardryBlocks.runestone);
+ registerMultiTexturedModel(runestoneItem);
+
+ ModelLoader.setCustomStateMapper(WizardryBlocks.runestone_pedestal, new StateMap.Builder()
+ .withName(BlockPedestal.ELEMENT).ignore(BlockPedestal.NATURAL).withSuffix("_runestone_pedestal").build()); // Don't care about NATURAL property
+ ItemBlockMultiTexturedElemental pedestalItem = (ItemBlockMultiTexturedElemental)Item.getItemFromBlock(WizardryBlocks.runestone_pedestal);
+ registerMultiTexturedModel(pedestalItem);
+
+ // Items
+
+ registerMultiTexturedModel((ItemCrystal)WizardryItems.magic_crystal);
+
+ registerItemModel(WizardryItems.magic_wand);
+ registerItemModel(WizardryItems.apprentice_wand);
+ registerItemModel(WizardryItems.advanced_wand);
+ registerItemModel(WizardryItems.master_wand);
+
+ registerItemModel(WizardryItems.spell_book);
+ // Wildcard registered for wizard trades.
+ registerItemModel(WizardryItems.spell_book, OreDictionary.WILDCARD_VALUE, "normal");
+ registerItemModel(WizardryItems.arcane_tome);
+ registerItemModel(WizardryItems.wizard_handbook);
+
+ registerItemModel(WizardryItems.novice_fire_wand);
+ registerItemModel(WizardryItems.novice_ice_wand);
+ registerItemModel(WizardryItems.novice_lightning_wand);
+ registerItemModel(WizardryItems.novice_necromancy_wand);
+ registerItemModel(WizardryItems.novice_earth_wand);
+ registerItemModel(WizardryItems.novice_sorcery_wand);
+ registerItemModel(WizardryItems.novice_healing_wand);
+
+ registerItemModel(WizardryItems.apprentice_fire_wand);
+ registerItemModel(WizardryItems.apprentice_ice_wand);
+ registerItemModel(WizardryItems.apprentice_lightning_wand);
+ registerItemModel(WizardryItems.apprentice_necromancy_wand);
+ registerItemModel(WizardryItems.apprentice_earth_wand);
+ registerItemModel(WizardryItems.apprentice_sorcery_wand);
+ registerItemModel(WizardryItems.apprentice_healing_wand);
+
+ registerItemModel(WizardryItems.advanced_fire_wand);
+ registerItemModel(WizardryItems.advanced_ice_wand);
+ registerItemModel(WizardryItems.advanced_lightning_wand);
+ registerItemModel(WizardryItems.advanced_necromancy_wand);
+ registerItemModel(WizardryItems.advanced_earth_wand);
+ registerItemModel(WizardryItems.advanced_sorcery_wand);
+ registerItemModel(WizardryItems.advanced_healing_wand);
+
+ registerItemModel(WizardryItems.master_fire_wand);
+ registerItemModel(WizardryItems.master_ice_wand);
+ registerItemModel(WizardryItems.master_lightning_wand);
+ registerItemModel(WizardryItems.master_necromancy_wand);
+ registerItemModel(WizardryItems.master_earth_wand);
+ registerItemModel(WizardryItems.master_sorcery_wand);
+ registerItemModel(WizardryItems.master_healing_wand);
+
+ registerItemModel(WizardryItems.spectral_sword);
+ registerItemModel(WizardryItems.spectral_pickaxe);
+ registerItemModel(WizardryItems.spectral_bow);
+
+ registerItemModel(WizardryItems.small_mana_flask);
+ registerItemModel(WizardryItems.medium_mana_flask);
+ registerItemModel(WizardryItems.large_mana_flask);
+
+ registerItemModel(WizardryItems.crystal_shard);
+ registerItemModel(WizardryItems.grand_crystal);
+
+ registerItemModel(WizardryItems.astral_diamond);
+
+ registerItemModel(WizardryItems.purifying_elixir);
+
+ registerItemModel(WizardryItems.storage_upgrade);
+ registerItemModel(WizardryItems.siphon_upgrade);
+ registerItemModel(WizardryItems.condenser_upgrade);
+ registerItemModel(WizardryItems.range_upgrade);
+ registerItemModel(WizardryItems.duration_upgrade);
+ registerItemModel(WizardryItems.cooldown_upgrade);
+ registerItemModel(WizardryItems.blast_upgrade);
+ registerItemModel(WizardryItems.attunement_upgrade);
+ registerItemModel(WizardryItems.melee_upgrade);
+
+ registerItemModel(WizardryItems.flaming_axe);
+ registerItemModel(WizardryItems.frost_axe);
+
+ registerItemModel(WizardryItems.firebomb);
+ registerItemModel(WizardryItems.poison_bomb);
+ registerItemModel(WizardryItems.smoke_bomb);
+ registerItemModel(WizardryItems.spark_bomb);
+
+ registerItemModel(WizardryItems.blank_scroll);
+ registerItemModel(WizardryItems.scroll);
+ registerItemModel(WizardryItems.identification_scroll);
+
+ registerItemModel(WizardryItems.armour_upgrade);
+
+ registerItemModel(WizardryItems.magic_silk);
+
+ registerItemModel(WizardryItems.wizard_hat);
+ registerItemModel(WizardryItems.wizard_robe);
+ registerItemModel(WizardryItems.wizard_leggings);
+ registerItemModel(WizardryItems.wizard_boots);
+
+ registerItemModel(WizardryItems.wizard_hat_fire);
+ registerItemModel(WizardryItems.wizard_robe_fire);
+ registerItemModel(WizardryItems.wizard_leggings_fire);
+ registerItemModel(WizardryItems.wizard_boots_fire);
+
+ registerItemModel(WizardryItems.wizard_hat_ice);
+ registerItemModel(WizardryItems.wizard_robe_ice);
+ registerItemModel(WizardryItems.wizard_leggings_ice);
+ registerItemModel(WizardryItems.wizard_boots_ice);
+
+ registerItemModel(WizardryItems.wizard_hat_lightning);
+ registerItemModel(WizardryItems.wizard_robe_lightning);
+ registerItemModel(WizardryItems.wizard_leggings_lightning);
+ registerItemModel(WizardryItems.wizard_boots_lightning);
+
+ registerItemModel(WizardryItems.wizard_hat_necromancy);
+ registerItemModel(WizardryItems.wizard_robe_necromancy);
+ registerItemModel(WizardryItems.wizard_leggings_necromancy);
+ registerItemModel(WizardryItems.wizard_boots_necromancy);
+
+ registerItemModel(WizardryItems.wizard_hat_earth);
+ registerItemModel(WizardryItems.wizard_robe_earth);
+ registerItemModel(WizardryItems.wizard_leggings_earth);
+ registerItemModel(WizardryItems.wizard_boots_earth);
+
+ registerItemModel(WizardryItems.wizard_hat_sorcery);
+ registerItemModel(WizardryItems.wizard_robe_sorcery);
+ registerItemModel(WizardryItems.wizard_leggings_sorcery);
+ registerItemModel(WizardryItems.wizard_boots_sorcery);
+
+ registerItemModel(WizardryItems.wizard_hat_healing);
+ registerItemModel(WizardryItems.wizard_robe_healing);
+ registerItemModel(WizardryItems.wizard_leggings_healing);
+ registerItemModel(WizardryItems.wizard_boots_healing);
+
+ registerItemModel(WizardryItems.spectral_helmet);
+ registerItemModel(WizardryItems.spectral_chestplate);
+ registerItemModel(WizardryItems.spectral_leggings);
+ registerItemModel(WizardryItems.spectral_boots);
+
+ registerItemModel(WizardryItems.lightning_hammer);
+
+ registerItemModel(WizardryItems.ring_condensing);
+ registerItemModel(WizardryItems.ring_siphoning);
+ registerItemModel(WizardryItems.ring_battlemage);
+ registerItemModel(WizardryItems.ring_combustion);
+ registerItemModel(WizardryItems.ring_fire_melee);
+ registerItemModel(WizardryItems.ring_fire_biome);
+ registerItemModel(WizardryItems.ring_disintegration);
+ registerItemModel(WizardryItems.ring_ice_melee);
+ registerItemModel(WizardryItems.ring_ice_biome);
+ registerItemModel(WizardryItems.ring_arcane_frost);
+ registerItemModel(WizardryItems.ring_shattering);
+ registerItemModel(WizardryItems.ring_lightning_melee);
+ registerItemModel(WizardryItems.ring_storm);
+ registerItemModel(WizardryItems.ring_seeking);
+ registerItemModel(WizardryItems.ring_hammer);
+ registerItemModel(WizardryItems.ring_soulbinding);
+ registerItemModel(WizardryItems.ring_leeching);
+ registerItemModel(WizardryItems.ring_necromancy_melee);
+ registerItemModel(WizardryItems.ring_mind_control);
+ registerItemModel(WizardryItems.ring_poison);
+ registerItemModel(WizardryItems.ring_earth_melee);
+ registerItemModel(WizardryItems.ring_earth_biome);
+ registerItemModel(WizardryItems.ring_full_moon);
+ registerItemModel(WizardryItems.ring_extraction);
+ registerItemModel(WizardryItems.ring_mana_return);
+ registerItemModel(WizardryItems.ring_blockwrangler);
+ registerItemModel(WizardryItems.ring_conjurer);
+ registerItemModel(WizardryItems.ring_defender);
+ registerItemModel(WizardryItems.ring_paladin);
+ registerItemModel(WizardryItems.ring_interdiction);
+
+ registerItemModel(WizardryItems.amulet_arcane_defence);
+ registerItemModel(WizardryItems.amulet_warding);
+ registerItemModel(WizardryItems.amulet_wisdom);
+ registerItemModel(WizardryItems.amulet_fire_protection);
+ registerItemModel(WizardryItems.amulet_fire_cloaking);
+ registerItemModel(WizardryItems.amulet_ice_immunity);
+ registerItemModel(WizardryItems.amulet_ice_protection);
+ registerItemModel(WizardryItems.amulet_potential);
+ registerItemModel(WizardryItems.amulet_channeling);
+ registerItemModel(WizardryItems.amulet_lich);
+ registerItemModel(WizardryItems.amulet_wither_immunity);
+ registerItemModel(WizardryItems.amulet_glide);
+ registerItemModel(WizardryItems.amulet_banishing);
+ registerItemModel(WizardryItems.amulet_anchoring);
+ registerItemModel(WizardryItems.amulet_recovery);
+ registerItemModel(WizardryItems.amulet_transience);
+ registerItemModel(WizardryItems.amulet_resurrection);
+ registerItemModel(WizardryItems.amulet_auto_shield);
+
+ registerItemModel(WizardryItems.charm_haggler);
+ registerItemModel(WizardryItems.charm_experience_tome);
+ registerItemModel(WizardryItems.charm_auto_smelt);
+ registerItemModel(WizardryItems.charm_lava_walking);
+ registerItemModel(WizardryItems.charm_storm);
+ registerItemModel(WizardryItems.charm_minion_health);
+ registerItemModel(WizardryItems.charm_minion_variants);
+ registerItemModel(WizardryItems.charm_flight);
+ registerItemModel(WizardryItems.charm_growth);
+ registerItemModel(WizardryItems.charm_abseiling);
+ registerItemModel(WizardryItems.charm_silk_touch);
+ registerItemModel(WizardryItems.charm_stop_time);
+ registerItemModel(WizardryItems.charm_light);
+ registerItemModel(WizardryItems.charm_transportation);
+ registerItemModel(WizardryItems.charm_feeding);
+
+ }
+
+ @SubscribeEvent
+ public static void bake(ModelBakeEvent event){
+ // MMMmmmm I love the smell of freshly-baked models...
+ // This stuff is the boilerplate for making runestone overlay render with full brightness
+ // See https://www.minecraftforge.net/forum/topic/66005-how-do-i-make-a-tileentityspecialrenderer-solved-with-ibakedmodel/
+ // As usual the Forge documentation is just a description of each class and not an explanation of how to use them
+ // I had to work out where this goes from the refined storage repo linked in the above thread, which is mixed in
+ // with a more extensive registration system (which is super neat, but it's overkill for our purposes)
+ // https://github.com/raoulvdberge/refinedstorage/blob/13d6e7f2b92f41b5009187aa2cbde50dbc72082f/src/main/java/com/raoulvdberge/refinedstorage/proxy/ProxyClient.java#L59
+
+ for(ModelResourceLocation location : event.getModelRegistry().getKeys()){
+
+ if(location.getNamespace().equals(Wizardry.MODID)){
+
+ if(location.getPath().contains("runestone") || location.getPath().contains("runestone_pedestal")){
+ IBakedModel original = event.getModelRegistry().getObject(location);
+ event.getModelRegistry().putObject(location, new BakedModelGlowingOverlay(original, "overlay"));
+ }
+ }
+ }
+ }
+
+ // Moved from the proxies
+
+ /**
+ * Registers an item model, using the item's registry name as the model name (this convention makes it easier to
+ * keep track of everything). Variant defaults to "normal". Registers the model for metadata 0 automatically, plus
+ * all the other metadata values that the item can take, as defined in
+ * {@link Item#getSubItems(CreativeTabs, NonNullList)}. The creative tab supplied
+ * to the aforementioned method will be whichever one the item is in.
+ */
+ private static void registerItemModel(Item item){
+
+ if(item.getHasSubtypes()){
+ NonNullList items = NonNullList.create();
+ item.getSubItems(item.getCreativeTab(), items); // Client-only method, but we're client-side so this is OK.
+ for(ItemStack stack : items){
+ ModelLoader.setCustomModelResourceLocation(item, stack.getMetadata(),
+ new ModelResourceLocation(item.getRegistryName(), "inventory"));
+ }
+ }
+ // Changing the last parameter from null to "inventory" fixed the item/block model weirdness. No idea why!
+ ModelLoader.setCustomModelResourceLocation(item, 0,
+ new ModelResourceLocation(item.getRegistryName(), "inventory"));
+ }
+
+ /**
+ * Registers an item model, using the itemstack-sensitive {@link IMultiTexturedItem#getModelName(ItemStack)} as the
+ * model name. This allows items to change their texture based on metadata/NBT. Variant defaults to "normal". Registers the
+ * model for metadata 0 automatically, plus all the other metadata values that the item can take, as defined in
+ * {@link Item#getSubItems(CreativeTabs, NonNullList)}. The creative tab supplied
+ * to the aforementioned method will be whichever one the item is in.
+ */
+ private static void registerMultiTexturedModel(T item){
+
+ if(item.getHasSubtypes()){
+ NonNullList items = NonNullList.create();
+ item.getSubItems(item.getCreativeTab(), items);
+ for(ItemStack stack : items){
+ ModelLoader.setCustomModelResourceLocation(item, stack.getMetadata(),
+ new ModelResourceLocation(item.getModelName(stack), "inventory"));
+ }
+ }
+ }
+
+ /**
+ * Registers an item model for the given metadata, using the item's registry name as the model name (this convention
+ * makes it easier to keep track of everything). This is intended for registering additional metadata values which
+ * aren't displayed in the creative menu, for example the wildcard spell book used in wizard trades.
+ */
+ private static void registerItemModel(Item item, int metadata, String variant){
+ ModelLoader.setCustomModelResourceLocation(item, metadata,
+ new ModelResourceLocation(item.getRegistryName(), variant));
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleBeam.java b/src/main/java/electroblob/wizardry/client/particle/ParticleBeam.java
new file mode 100644
index 00000000..3e6d3a3f
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleBeam.java
@@ -0,0 +1,99 @@
+package electroblob.wizardry.client.particle;
+
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.world.World;
+import org.lwjgl.opengl.GL11;
+
+public class ParticleBeam extends ParticleTargeted {
+
+ /** Half the width of the outermost layer. */
+ private static final float THICKNESS = 0.1f;
+
+ public ParticleBeam(World world, double x, double y, double z){
+ super(world, x, y, z); // Does not have a texture!
+ this.setRBGColorF(1, 1, 1);
+ this.setMaxAge(0);
+ this.particleScale = 1;
+ }
+
+ @Override
+ public boolean shouldDisableDepth(){
+ return true;
+ }
+
+ @Override
+ public int getFXLayer(){
+ return 3;
+ }
+
+ @Override
+ protected void draw(Tessellator tessellator, double length, float partialTicks){
+
+ float scale = this.particleScale;
+
+ if(this.particleMaxAge > 0){
+ float ageFraction = (particleAge + partialTicks - 1)/particleMaxAge;
+ // Squaring this makes it look smoother than a linear shrinking effect
+ scale = this.particleScale * (1 - ageFraction*ageFraction);
+ }
+
+ GlStateManager.disableLighting();
+ GlStateManager.enableBlend();
+ GlStateManager.disableTexture2D();
+ GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE);
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ for(int layer=0; layer<3; layer++){
+ drawSegment(tessellator, layer, 0, 0, 0, 0, 0, length, THICKNESS * scale);
+ }
+
+ GlStateManager.enableTexture2D();
+ GlStateManager.enableLighting();
+ GlStateManager.disableBlend();
+ }
+
+ /** Draws the given layer of a segment of the arc, from the point (x1, y1, z1) to the point (x2, y2, z2), with the given thickness. */
+ private void drawSegment(Tessellator tessellator, int layer, double x1, double y1, double z1, double x2, double y2, double z2, float thickness){
+
+ BufferBuilder buffer = tessellator.getBuffer();
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_COLOR);
+
+ switch(layer){
+
+ case 0:
+ drawShearedBox(buffer, x1, y1, z1, x2, y2, z2, 0.25f*thickness, 1, 1, 1, 1);
+ break;
+
+ case 1:
+ drawShearedBox(buffer, x1, y1, z1, x2, y2, z2, 0.6f*thickness, (particleRed + 1)/2, (particleGreen + 1)/2,
+ (particleBlue + 1)/2, 0.65f);
+ break;
+
+ case 2:
+ drawShearedBox(buffer, x1, y1, z1, x2, y2, z2, thickness, particleRed, particleGreen, particleBlue, 0.3f);
+ break;
+ }
+
+ tessellator.draw();
+ }
+
+ /** Draws a single box for one segment of the arc, from the point (x1, y1, z1) to the point (x2, y2, z2), with given width and colour. */
+ private void drawShearedBox(BufferBuilder buffer, double x1, double y1, double z1, double x2, double y2, double z2, float width, float r, float g, float b, float a){
+
+ buffer.pos(x1-width, y1-width, z1).color(r, g, b, a).endVertex();
+ buffer.pos(x2-width, y2-width, z2).color(r, g, b, a).endVertex();
+ buffer.pos(x1-width, y1+width, z1).color(r, g, b, a).endVertex();
+ buffer.pos(x2-width, y2+width, z2).color(r, g, b, a).endVertex();
+ buffer.pos(x1+width, y1+width, z1).color(r, g, b, a).endVertex();
+ buffer.pos(x2+width, y2+width, z2).color(r, g, b, a).endVertex();
+ buffer.pos(x1+width, y1-width, z1).color(r, g, b, a).endVertex();
+ buffer.pos(x2+width, y2-width, z2).color(r, g, b, a).endVertex();
+ buffer.pos(x1-width, y1-width, z1).color(r, g, b, a).endVertex();
+ buffer.pos(x2-width, y2-width, z2).color(r, g, b, a).endVertex();
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleBlizzard.java b/src/main/java/electroblob/wizardry/client/particle/ParticleBlizzard.java
deleted file mode 100644
index 58e84e48..00000000
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleBlizzard.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package electroblob.wizardry.client.particle;
-
-import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-@SideOnly(Side.CLIENT)
-public class ParticleBlizzard extends ParticleSnow {
-
- private double angle;
- private double radius;
- private double speed;
-
- public ParticleBlizzard(World world, int maxAge, double originX, double originZ, double radius, double yPos){
- super(world, 0, 0, 0, 0, 0, 0, maxAge);
- this.angle = this.rand.nextDouble() * Math.PI * 2;
- double x = originX - Math.cos(angle) * radius;
- double z = originZ + radius * Math.sin(angle);
- this.radius = radius;
- this.setPosition(x, yPos, z);
- this.prevPosX = x;
- this.prevPosY = yPos;
- this.prevPosZ = z;
- if(rand.nextBoolean()){
- speed = rand.nextDouble() * 2 + 1;
- }else{
- speed = rand.nextDouble() * -2 - 1;
- }
- this.multipleParticleScaleBy(1.5f);
- }
-
- @Override
- public void init(){
- super.init();
- this.fullBrightness = true;
- }
-
- // @Override
- // public void renderParticle(VertexBuffer buffer, Entity entity, float partialTicks, float rotationX, float
- // rotationZ, float rotationYZ, float rotationXY, float rotationXZ){
- // if(this.particleAge < this.particleMaxAge / 3 || (this.particleAge + this.particleMaxAge) / 3 % 2 == 0){
- // super.renderParticle(buffer, entity, partialTicks, rotationX, rotationZ, rotationYZ, rotationXY, rotationXZ);
- // }
- // }
-
- @Override
- public void onUpdate(){
-
- this.prevPosX = this.posX;
- this.prevPosY = this.posY;
- this.prevPosZ = this.posZ;
-
- if(this.particleAge++ >= this.particleMaxAge){
- this.setExpired();
- }
-
- // This is in radians per tick...
- double omega = Math.signum(speed) * ((Math.PI * 2) / 20 - speed / (20 * radius));
-
- // v = r times omega; therefore the normalised velocity vector needs to be r times the angle increment / 2 pi.
- this.angle += omega;
-
- this.motionY -= 0.04D * (double)this.particleGravity;
- this.motionZ = radius * omega * Math.cos(angle);
- this.motionX = radius * omega * Math.sin(angle);
- this.move(motionX, motionY, motionZ);
-
- if(this.particleAge > this.particleMaxAge / 2){
- this.setAlphaF(
- 1.0F - ((float)this.particleAge - (float)(this.particleMaxAge / 2)) / (float)this.particleMaxAge);
- }
-
- }
-}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleBuff.java b/src/main/java/electroblob/wizardry/client/particle/ParticleBuff.java
new file mode 100644
index 00000000..a7a8a752
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleBuff.java
@@ -0,0 +1,136 @@
+package electroblob.wizardry.client.particle;
+
+import electroblob.wizardry.Wizardry;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.*;
+import net.minecraft.client.renderer.GlStateManager.DestFactor;
+import net.minecraft.client.renderer.GlStateManager.SourceFactor;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+import org.lwjgl.opengl.GL11;
+
+//@SideOnly(Side.CLIENT)
+public class ParticleBuff extends ParticleWizardry {
+
+ private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/particle/buff.png");
+ private final boolean mirror;
+
+ public ParticleBuff(World world, double x, double y, double z){
+ super(world, x, y, z);
+ this.setVelocity(0, 0.162, 0); // Approximately what it was before
+ this.mirror = random.nextBoolean();
+ this.setMaxAge(15);
+ this.setGravity(false);
+ this.canCollide = false;
+ }
+
+ @Override
+ public boolean shouldDisableDepth(){
+ return true;
+ }
+
+
+ @Override
+ public void onUpdate(){
+ super.onUpdate();
+ if(this.particleAge > this.particleMaxAge/2) this.particleAlpha = 2f - 2f*(float)this.particleAge/(float)this.particleMaxAge;
+ }
+
+ /* There are 4 layers of particles, specified as 0-3 by the method below. - Layer 0 causes the normal particles.png
+ * to be bound to the render engine for normal particles. - Layer 1 causes the block textures to be bound to the
+ * render engine for digging fx and falling fx. - Layer 2 causes the item textures to be bound to the render engine
+ * for tool breaking fx, snowballpoofs, slime particles, etc. - Layer 3 is not used in vanilla minecraft and was
+ * presumably added by forge for exactly this reason. This means no texture is bound by vanilla minecraft, meaning
+ * you are free to do as you wish without possibly overwriting vanilla particles. Mod particles won't be overwritten
+ * anyway since they bind their own textures. It is of course important to bind the texture every time you render a
+ * custom particle, but I don't see how you could do it any other way, since you don't have access to
+ * EffectRenderer. */
+ @Override
+ public int getFXLayer(){
+ // This can only be 0-3 or it will cause an ArrayIndexOutOfBoundsException in EffectRenderer.
+ return 3;
+ }
+
+ @Override
+ public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationZ,
+ float rotationYZ, float rotationXY, float rotationXZ){
+
+ // Copied from ParticleWizardry, needs to be here since we're not calling super
+ updateEntityLinking(partialTicks);
+
+ GlStateManager.pushMatrix();
+ GlStateManager.pushAttrib();
+
+ GlStateManager.enableBlend();
+ GlStateManager.disableAlpha();
+ GlStateManager.disableCull();
+ GlStateManager.disableLighting();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE);
+ // Makes the particle colour add to the colour of the texture pixels, rather than the default multiplying
+ GlStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_ADD);
+
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
+ GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
+
+ // Does the texture translation wrapping thing (the cool stuff)
+ GlStateManager.matrixMode(GL11.GL_TEXTURE);
+ GlStateManager.loadIdentity();
+
+ GlStateManager.translate((this.particleAge + partialTicks)/(float)this.particleMaxAge * -2, 0, 0);
+
+ GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+
+ RenderHelper.disableStandardItemLighting();
+
+ Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE);
+
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR);
+
+ float x = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks - interpPosX);
+ float y = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks - interpPosY);
+ float z = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks - interpPosZ);
+
+ // Increases from 0 to 1 in steps of 0.125 evenly throughout the particle's lifetime
+ float f = 0.875f - 0.125f * MathHelper.floor((float)this.particleAge/(float)this.particleMaxAge * 8 - 0.000001f);
+ float g = f + 0.125f;
+ float hrepeat = 1;
+ float scale = 0.6f;
+ float yScale = 0.7f * scale;
+ float dx = mirror ? -scale : scale;
+ float dz = scale;
+
+ buffer.pos(x-dx, y-yScale, z-dz).tex(0, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-dx, y+yScale, z-dz).tex(0, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+dx, y-yScale, z-dz).tex(0.25*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+dx, y+yScale, z-dz).tex(0.25*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+dx, y-yScale, z+dz).tex(0.5*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+dx, y+yScale, z+dz).tex(0.5*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-dx, y-yScale, z+dz).tex(0.75*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-dx, y+yScale, z+dz).tex(0.75*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-dx, y-yScale, z-dz).tex(hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-dx, y+yScale, z-dz).tex(hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+
+ Tessellator.getInstance().draw();
+
+ // Undoes the texture transformations
+ GlStateManager.matrixMode(GL11.GL_TEXTURE);
+ GlStateManager.loadIdentity();
+ GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+
+ GlStateManager.disableBlend();
+ GlStateManager.enableAlpha();
+ GlStateManager.enableCull();
+ GlStateManager.enableLighting();
+ // Reverses the colour addition change from before
+ GlStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
+
+ GlStateManager.popAttrib();
+ GlStateManager.popMatrix();
+
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleCustomTexture.java b/src/main/java/electroblob/wizardry/client/particle/ParticleCustomTexture.java
deleted file mode 100644
index 38259b1a..00000000
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleCustomTexture.java
+++ /dev/null
@@ -1,217 +0,0 @@
-package electroblob.wizardry.client.particle;
-
-import org.lwjgl.opengl.GL11;
-
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.particle.Particle;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.OpenGlHelper;
-import net.minecraft.client.renderer.RenderHelper;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.entity.Entity;
-import net.minecraft.util.ResourceLocation;
-import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-/**
- * Abstract superclass for all particles that use custom textures. This is intended to centralise as much code as
- * possible; all subclasses need to do is to define the texture to use, how the frames are arranged (and which to
- * choose), and any properties like gravity and collisions.
- *
- * @author Electroblob
- * @since Wizardry 1.2
- */
-@SideOnly(Side.CLIENT)
-public abstract class ParticleCustomTexture extends Particle {
-
- /** True if the particle always renders at full brightness. Defaults to false. */
- protected boolean fullBrightness = false;
-
- public ParticleCustomTexture(World world, double x, double y, double z, double vx, double vy, double vz){
- super(world, x, y, z, vx, vy, vz);
- this.motionX = vx;
- this.motionY = vy;
- this.motionZ = vz;
- this.init();
- }
-
- public ParticleCustomTexture(World world, double x, double y, double z, double vx, double vy, double vz,
- int maxAge){
- this(world, x, y, z, vx, vy, vz);
- this.particleMaxAge = maxAge;
- }
-
- /**
- * Called from both constructors to set constants, avoiding duplicate code. Common fields to set here include:
- * particleScale, particleGravity, canCollide, fullBrightness and setting the texture index.
- */
- public abstract void init();
-
- /**
- * Returns a ResourceLocation for the particle's texture sheet. Do not create a new ResourceLocation in this method,
- * only return a constant.
- */
- public abstract ResourceLocation getTexture();
-
- /** Returns how many 'frames' there are in the x direction on the texture. */
- protected abstract int getXFrames();
-
- /** Returns how many 'frames' there are in the y direction on the texture. */
- protected abstract int getYFrames();
-
- /* There are 4 layers of particles, specified as 0-3 by the method below. - Layer 0 causes the normal particles.png
- * to be bound to the render engine for normal particles. - Layer 1 causes the block textures to be bound to the
- * render engine for digging fx and falling fx. - Layer 2 causes the item textures to be bound to the render engine
- * for tool breaking fx, snowballpoofs, slime particles, etc. - Layer 3 is not used in vanilla minecraft and was
- * presumably added by forge for exactly this reason. This means no texture is bound by vanilla minecraft, meaning
- * you are free to do as you wish without possibly overwriting vanilla particles. Mod particles won't be overwritten
- * anyway since they bind their own textures. It is of course important to bind the texture every time you render a
- * custom particle, but I don't see how you could do it any other way, since you don't have access to
- * EffectRenderer. */
- @Override
- public int getFXLayer(){
- // This can only be 0-3 or it will cause an ArrayIndexOutOfBoundsException in EffectRenderer.
- return 3;
- }
-
- @Override
- public void setParticleTextureIndex(int index){
- this.particleTextureIndexX = index % getXFrames();
- this.particleTextureIndexY = index / getYFrames();
- }
-
- // Overridden to fix the bug with vanilla that makes particles frictionless. (y != y... seriously, Mojang?)
- // TESTME: Probably no longer necessary.
- // @Override
- // public void move(double x, double y, double z){
- //
- // double d0 = y;
- //
- // if (this.canCollide)
- // {
- // List list = this.world.getCollisionBoxes((Entity)null, this.getBoundingBox().addCoord(x, y, z));
- //
- // for (AxisAlignedBB axisalignedbb : list)
- // {
- // y = axisalignedbb.calculateYOffset(this.getBoundingBox(), y);
- // }
- //
- // this.setBoundingBox(this.getBoundingBox().offset(0.0D, y, 0.0D));
- //
- // for (AxisAlignedBB axisalignedbb1 : list)
- // {
- // x = axisalignedbb1.calculateXOffset(this.getBoundingBox(), x);
- // }
- //
- // this.setBoundingBox(this.getBoundingBox().offset(x, 0.0D, 0.0D));
- //
- // for (AxisAlignedBB axisalignedbb2 : list)
- // {
- // z = axisalignedbb2.calculateZOffset(this.getBoundingBox(), z);
- // }
- //
- // this.setBoundingBox(this.getBoundingBox().offset(0.0D, 0.0D, z));
- // }
- // else
- // {
- // this.setBoundingBox(this.getBoundingBox().offset(x, y, z));
- // }
- //
- // this.resetPositionToBB();
- // this.onGround = d0 != y && d0 < 0.0D;
- //
- // /* Can never be true! - But this doesn't seem to make any difference anyway.
- // if (x != x)
- // {
- // this.motionX = 0.0D;
- // }
- //
- // if (z != z)
- // {
- // this.motionZ = 0.0D;
- // }
- // */
- // }
-
- // Overridden to bind the new texture. I think this can be done with TextureAtlasSprite, but this works as it is
- // so I'm not changing it for the time being.
- @Override
- public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationZ,
- float rotationYZ, float rotationXY, float rotationXZ){
-
- GlStateManager.pushMatrix();
-
- this.applyGLStateChanges();
-
- // This stuff does the shading. It vanilla does this later on for each point, but this also seems to work.
- int brightness = this.getBrightnessForRender(partialTicks);
- int lightmapX = brightness % 65536;
- int lightmapY = brightness / 65536;
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, (float)lightmapX / 1.0F,
- (float)lightmapY / 1.0F);
-
- RenderHelper.disableStandardItemLighting();
-
- Minecraft.getMinecraft().getTextureManager().bindTexture(getTexture());
-
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR);
-
- float u1 = (float)this.particleTextureIndexX / (float)getXFrames();
- float u2 = u1 + 1.0f / getXFrames();
- float v1 = (float)this.particleTextureIndexY / (float)getYFrames();
- float v2 = v1 + 1.0f / getYFrames();
- float scale = 0.1F * this.particleScale;
-
- // I'm pretty sure these were always static.
- Particle.interpPosX = viewer.lastTickPosX + (viewer.posX - viewer.lastTickPosX) * (double)partialTicks;
- Particle.interpPosY = viewer.lastTickPosY + (viewer.posY - viewer.lastTickPosY) * (double)partialTicks;
- Particle.interpPosZ = viewer.lastTickPosZ + (viewer.posZ - viewer.lastTickPosZ) * (double)partialTicks;
-
- float x = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks - interpPosX);
- float y = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks - interpPosY);
- float z = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks - interpPosZ);
-
- buffer.pos((double)(x - rotationX * scale - rotationXY * scale), (double)(y - rotationZ * scale),
- (double)(z - rotationYZ * scale - rotationXZ * scale)).tex(u2, v2)
- .color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
- buffer.pos((double)(x - rotationX * scale + rotationXY * scale), (double)(y + rotationZ * scale),
- (double)(z - rotationYZ * scale + rotationXZ * scale)).tex(u2, v1)
- .color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
- buffer.pos((double)(x + rotationX * scale + rotationXY * scale), (double)(y + rotationZ * scale),
- (double)(z + rotationYZ * scale + rotationXZ * scale)).tex(u1, v1)
- .color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
- buffer.pos((double)(x + rotationX * scale - rotationXY * scale), (double)(y - rotationZ * scale),
- (double)(z + rotationYZ * scale - rotationXZ * scale)).tex(u1, v2)
- .color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
- ;
-
- Tessellator.getInstance().draw();
-
- this.undoGLStateChanges();
-
- GlStateManager.popMatrix();
-
- }
-
- /**
- * Override to add any GL state changes, like blending. Does nothing by default. State changes should be done
- * using GLStateManager, not using GL11 directly (as is the case with all rendering code now).
- */
- public void applyGLStateChanges(){
- }
-
- /**
- * Override to undo any GL state changes, like blending. Does nothing by default. State changes should be done
- * using GLStateManager, not using GL11 directly (as is the case with all rendering code now).
- */
- public void undoGLStateChanges(){
- }
-
- @Override
- public int getBrightnessForRender(float partialTick){
- return fullBrightness ? 15728880 : super.getBrightnessForRender(partialTick);
- }
-}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleDarkMagic.java b/src/main/java/electroblob/wizardry/client/particle/ParticleDarkMagic.java
index 9c962a00..7558bf89 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleDarkMagic.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleDarkMagic.java
@@ -1,27 +1,20 @@
package electroblob.wizardry.client.particle;
-import net.minecraft.client.particle.Particle;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.entity.Entity;
import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-public class ParticleDarkMagic extends Particle {
+//@SideOnly(Side.CLIENT)
+public class ParticleDarkMagic extends ParticleWizardry {
/** Base spell texture index */
private int baseSpellTextureIndex = 128;
- public ParticleDarkMagic(World par1World, double par2, double par4, double par6, double par8, double par10,
- double par12, float r, float g, float b){
- super(par1World, par2, par4, par6, par8, par10, par12);
+ public ParticleDarkMagic(World world, double x, double y, double z){
+ super(world, x, y, z);
+
this.motionY *= 0.20000000298023224D;
-
- this.particleRed = r;
- this.particleGreen = g;
- this.particleBlue = b;
-
+ this.setRBGColorF(1, 1, 1);
this.particleScale *= 0.75F;
this.particleMaxAge = (int)(8.0D / (Math.random() * 0.8D + 0.2D));
this.canCollide = true;
@@ -43,9 +36,7 @@ public class ParticleDarkMagic extends Particle {
super.renderParticle(buffer, entity, partialTicks, rotationX, rotationZ, rotationYZ, rotationXY, rotationXZ);
}
- /**
- * Called to update the entity's position/logic.
- */
+ @Override
public void onUpdate(){
this.prevPosX = this.posX;
this.prevPosY = this.posY;
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleDust.java b/src/main/java/electroblob/wizardry/client/particle/ParticleDust.java
index c37dbb51..cd925b81 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleDust.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleDust.java
@@ -1,48 +1,31 @@
package electroblob.wizardry.client.particle;
-import net.minecraft.client.particle.Particle;
import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-public class ParticleDust extends Particle {
+//@SideOnly(Side.CLIENT)
+public class ParticleDust extends ParticleWizardry {
- private final boolean shaded;
-
- public ParticleDust(World par1World, double x, double y, double z, double par8, double par10, double par12, float r,
- float g, float b, boolean shaded){
- super(par1World, x, y, z, par8, par10, par12);
- this.particleRed = r;
- this.particleGreen = g;
- this.particleBlue = b;
+ public ParticleDust(World world, double x, double y, double z){
+ super(world, x, y, z);
+
this.setParticleTextureIndex(0);
this.setSize(0.01F, 0.01F);
+
+ // Defaults
this.particleScale *= this.rand.nextFloat() + 0.2F;
- this.motionX = par8;
- this.motionY = par10;
- this.motionZ = par12;
this.particleMaxAge = (int)(16.0D / (Math.random() * 0.8D + 0.2D));
- this.shaded = shaded;
+ this.setRBGColorF(1, 1, 1);
}
- /**
- * Called to update the entity's position/logic.
- */
+ @Override
public void onUpdate(){
this.prevPosX = this.posX;
this.prevPosY = this.posY;
this.prevPosZ = this.posZ;
- // this.moveEntity(this.motionX, this.motionY, this.motionZ);
+ this.move(this.motionX, this.motionY, this.motionZ);
if(this.particleMaxAge-- <= 0){
this.setExpired();
}
}
-
- @Override
- public int getBrightnessForRender(float par1){
- return shaded ? super.getBrightnessForRender(par1) : 15728880;
- }
- /* @Override public float getBrightness(float par1) { return shaded ? super.getBrightness(par1) : 1.0F; } */
}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleFlash.java b/src/main/java/electroblob/wizardry/client/particle/ParticleFlash.java
new file mode 100644
index 00000000..f85bc90c
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleFlash.java
@@ -0,0 +1,49 @@
+package electroblob.wizardry.client.particle;
+
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+
+/**
+ * Copied from ParticleFirework.Overlay; for some reason that class has no public constructors, plus I want to change the
+ * scale and a few other things
+ * @author Electroblob
+ * @since Wizardry 4.2.0
+ */
+public class ParticleFlash extends ParticleWizardry {
+
+ public ParticleFlash(World world, double x, double y, double z){
+ super(world, x, y, z);
+ this.setRBGColorF(1, 1, 1);
+ this.particleScale = 0.6f; // 7.1f is the value used in fireworks
+ this.particleMaxAge = 6;
+ }
+
+ @Override
+ public boolean shouldDisableDepth(){
+ return true; // Well this fixes everything... let's hope it doesn't cause any side-effects!
+ }
+
+ @Override
+ public void drawParticle(BufferBuilder buffer, Entity entityIn, float partialTicks, float rotationX, float rotationZ, float rotationYZ, float rotationXY, float rotationXZ){
+ float f4 = particleScale * MathHelper.sin(((float)this.particleAge + partialTicks - 1.0F)/particleMaxAge * (float)Math.PI);
+ this.setAlphaF(0.6F - ((float)this.particleAge + partialTicks - 1.0F)/particleMaxAge * 0.5F);
+ float f5 = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks - interpPosX);
+ float f6 = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks - interpPosY);
+ float f7 = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks - interpPosZ);
+ int i = this.getBrightnessForRender(partialTicks);
+ int j = i >> 16 & 65535;
+ int k = i & 65535;
+ buffer.pos((double)(f5 - rotationX * f4 - rotationXY * f4), (double)(f6 - rotationZ * f4), (double)(f7 - rotationYZ * f4 - rotationXZ * f4)).tex(0.5D, 0.375D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex();
+ buffer.pos((double)(f5 - rotationX * f4 + rotationXY * f4), (double)(f6 + rotationZ * f4), (double)(f7 - rotationYZ * f4 + rotationXZ * f4)).tex(0.5D, 0.125D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex();
+ buffer.pos((double)(f5 + rotationX * f4 + rotationXY * f4), (double)(f6 + rotationZ * f4), (double)(f7 + rotationYZ * f4 + rotationXZ * f4)).tex(0.25D, 0.125D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex();
+ buffer.pos((double)(f5 + rotationX * f4 - rotationXY * f4), (double)(f6 - rotationZ * f4), (double)(f7 + rotationYZ * f4 - rotationXZ * f4)).tex(0.25D, 0.375D).color(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha).lightmap(j, k).endVertex();
+ }
+
+ @Override
+ public int getBrightnessForRender(float partialTicks){
+ return 15728880;
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleGiantBubble.java b/src/main/java/electroblob/wizardry/client/particle/ParticleGiantBubble.java
deleted file mode 100644
index b512aaba..00000000
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleGiantBubble.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package electroblob.wizardry.client.particle;
-
-import electroblob.wizardry.Wizardry;
-import net.minecraft.client.particle.Particle;
-import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-
-@SideOnly(Side.CLIENT)
-public class ParticleGiantBubble extends Particle {
- /**
- * The name used to identify this particle. Uses the mod id to avoid any possible conflicts (Not that there would be
- * any, but I may as well.)
- */
- public static final String NAME = Wizardry.MODID + "magicbubble";
-
- public ParticleGiantBubble(World par1World, double par2, double par4, double par6, double par8, double par10,
- double par12){
- super(par1World, par2, par4, par6, par8, par10, par12);
- this.particleRed = 1.0F;
- this.particleGreen = 1.0F;
- this.particleBlue = 1.0F;
- this.setParticleTextureIndex(32);
- this.setSize(0.02F, 0.02F);
- this.particleScale *= this.rand.nextFloat() * 0.6F + 0.2F;
- this.motionX = par8 * 0.20000000298023224D + (double)((float)(Math.random() * 2.0D - 1.0D) * 0.02F);
- this.motionY = par10 * 0.20000000298023224D + (double)((float)(Math.random() * 2.0D - 1.0D) * 0.02F);
- this.motionZ = par12 * 0.20000000298023224D + (double)((float)(Math.random() * 2.0D - 1.0D) * 0.02F);
- this.particleMaxAge = (int)(8.0D / (Math.random() * 0.8D + 0.2D));
- }
-
- /**
- * Called to update the entity's position/logic.
- */
- public void onUpdate(){
- this.prevPosX = this.posX;
- this.prevPosY = this.posY;
- this.prevPosZ = this.posZ;
- this.motionY += 0.002D;
- this.move(this.motionX, this.motionY, this.motionZ);
- this.motionX *= 0.8500000238418579D;
- this.motionY *= 0.8500000238418579D;
- this.motionZ *= 0.8500000238418579D;
-
- if(this.particleMaxAge-- <= 0){
- this.setExpired();
- }
- }
-}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleIce.java b/src/main/java/electroblob/wizardry/client/particle/ParticleIce.java
index dd548900..22a86d18 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleIce.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleIce.java
@@ -1,46 +1,35 @@
package electroblob.wizardry.client.particle;
-import electroblob.wizardry.Wizardry;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-public class ParticleIce extends ParticleCustomTexture {
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class ParticleIce extends ParticleWizardry {
- private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID,
- "textures/particle/ice_particles.png");
-
- public ParticleIce(World world, double x, double y, double z, double vx, double vy, double vz){
- super(world, x, y, z, vx, vy, vz);
- }
-
- public ParticleIce(World world, double x, double y, double z, double vx, double vy, double vz, int maxAge){
- super(world, x, y, z, vx, vy, vz, maxAge);
- }
-
- @Override
- public void init(){
- this.setParticleTextureIndex(rand.nextInt(8));
- this.particleScale *= 0.75f;
- this.particleGravity = 1;
+ private static final ResourceLocation[] TEXTURES = generateTextures("ice", 8);
+
+ public ParticleIce(World world, double x, double y, double z){
+
+ super(world, x, y, z, TEXTURES[world.rand.nextInt(TEXTURES.length)]);
+
this.canCollide = true;
- this.fullBrightness = true;
+
+ // Defaults
+ this.setRBGColorF(1, 1, 1);
+ this.particleScale *= 0.75f;
+ this.setGravity(true);
+ this.shaded = false;
}
-
- @Override
- public ResourceLocation getTexture(){
- return TEXTURE;
- }
-
- @Override
- protected int getXFrames(){
- return 4;
- }
-
- @Override
- protected int getYFrames(){
- return 4;
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+ for(ResourceLocation texture : TEXTURES){
+ event.getMap().registerSprite(texture);
+ }
}
}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleLeaf.java b/src/main/java/electroblob/wizardry/client/particle/ParticleLeaf.java
index e2610a3d..07f7c9fb 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleLeaf.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleLeaf.java
@@ -1,45 +1,46 @@
package electroblob.wizardry.client.particle;
-import electroblob.wizardry.Wizardry;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-public class ParticleLeaf extends ParticleCustomTexture {
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class ParticleLeaf extends ParticleWizardry {
- private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID,
- "textures/particle/leaf_particles.png");
+ private static final ResourceLocation[] TEXTURES = generateTextures("leaf", 16);
- public ParticleLeaf(World world, double x, double y, double z, double vx, double vy, double vz){
- super(world, x, y, z, vx, vy, vz);
- }
-
- public ParticleLeaf(World world, double x, double y, double z, double vx, double vy, double vz, int maxAge){
- super(world, x, y, z, vx, vy, vz, maxAge);
- }
-
- @Override
- public void init(){
- this.setParticleTextureIndex(rand.nextInt(16));
+ public ParticleLeaf(World world, double x, double y, double z){
+
+ super(world, x, y, z, TEXTURES[world.rand.nextInt(TEXTURES.length)]);
+
+ this.setVelocity(0, -0.03, 0);
+ this.setMaxAge(10 + rand.nextInt(5));
this.particleScale *= 1.4f;
this.particleGravity = 0;
this.canCollide = true;
+ // Produces a variety of browns and greens
+ this.setRBGColorF(0.1f + 0.3f * random.nextFloat(), 0.5f + 0.3f * random.nextFloat(), 0.1f);
}
-
+
@Override
- public ResourceLocation getTexture(){
- return TEXTURE;
+ public void onUpdate(){
+
+ super.onUpdate();
+
+ // Fading
+ if(this.particleAge > this.particleMaxAge / 2){
+ this.setAlphaF(1 - ((float)this.particleAge - (float)(this.particleMaxAge / 2)) / (float)this.particleMaxAge);
+ }
}
-
- @Override
- protected int getXFrames(){
- return 4;
- }
-
- @Override
- protected int getYFrames(){
- return 4;
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+ for(ResourceLocation texture : TEXTURES){
+ event.getMap().registerSprite(texture);
+ }
}
}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleLightning.java b/src/main/java/electroblob/wizardry/client/particle/ParticleLightning.java
new file mode 100644
index 00000000..fc73ff92
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleLightning.java
@@ -0,0 +1,165 @@
+package electroblob.wizardry.client.particle;
+
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.world.World;
+import org.lwjgl.opengl.GL11;
+
+public class ParticleLightning extends ParticleTargeted {
+
+ /** Half the width of the outermost layer. */
+ private static final float THICKNESS = 0.04f;
+ /** Maximum length of a segment. */
+ private static final double MAX_SEGMENT_LENGTH = 0.6;
+ /** Minimum length of a segment. */
+ private static final double MIN_SEGMENT_LENGTH = 0.2;
+ /** Maximum deviation (in x or y, as drawn before transformations) from the centreline. */
+ private static final double VERTEX_JITTER = 0.15;
+ /** Maximum number of segments a fork can have before ending. */
+ private static final int MAX_FORK_SEGMENTS = 3;
+ /** Probability (as a fraction) that a vertex will have a fork. */
+ private static final float FORK_CHANCE = 0.3f;
+ /** Number of ticks to wait before the arc changes shape again. */
+ private static final int UPDATE_PERIOD = 1;
+
+ public ParticleLightning(World world, double x, double y, double z){
+ super(world, x, y, z); // Does not have a texture!
+ seed = this.rand.nextLong();
+ this.setRBGColorF(0.2f, 0.6f, 1); // Default blue colour
+ this.setMaxAge(3);
+ this.particleScale = 1;
+ }
+
+ @Override
+ public boolean shouldDisableDepth(){
+ return true;
+ }
+
+ @Override
+ public int getFXLayer(){
+ return 3;
+ }
+
+ @Override
+ protected void draw(Tessellator tessellator, double length, float partialTicks){
+
+ GlStateManager.disableLighting();
+ GlStateManager.enableBlend();
+ GlStateManager.disableTexture2D();
+ GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE);
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ // The direction of the arc drawn by the tessellator is always along the z axis and is rotated to the
+ // correct orientation, that way there isn't a ton of trigonometry and the code is way neater.
+
+ boolean freeEnd = this.target == null;
+
+ int numberOfSegments = (int)Math.round(length/MAX_SEGMENT_LENGTH); // Number of segments
+
+ for(int layer=0; layer<3; layer++){
+
+ double px=0, py=0, pz=0;
+ // Creates a random from the arc's seed field + the number of ticks it has existed/the update period.
+ // By using a seed, we can ensure the vertex positions and forks are identical a) for each layer, even
+ // though they are rendered sequentially, and b) across many frames (and ticks, if updateTime > 1).
+ random.setSeed(this.seed + this.particleAge/UPDATE_PERIOD);
+
+ // numberOfSegments-1 because the last segment is handled separately.
+ for(int i=0; i= this.particleMaxAge){
- this.setExpired();
- }
-
- // This is in radians per tick...
- double omega = Math.signum(speed) * ((Math.PI * 2) / 20 - speed / (20 * radius));
-
- // v = r times omega; therefore the normalised velocity vector needs to be r times the angle increment / 2 pi.
- this.angle += omega;
-
- this.motionY -= 0.04D * (double)this.particleGravity;
- this.motionZ = radius * omega * Math.cos(angle);
- this.motionX = radius * omega * Math.sin(angle);
- this.move(motionX, motionY, motionZ);
-
- if(this.particleAge > this.particleMaxAge / 2){
- this.setAlphaF(
- 1.0F - ((float)this.particleAge - (float)(this.particleMaxAge / 2)) / (float)this.particleMaxAge);
- }
-
- }
-}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleScorch.java b/src/main/java/electroblob/wizardry/client/particle/ParticleScorch.java
new file mode 100644
index 00000000..60d240fa
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleScorch.java
@@ -0,0 +1,74 @@
+package electroblob.wizardry.client.particle;
+
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class ParticleScorch extends ParticleWizardry {
+
+ private static final ResourceLocation[] TEXTURES = generateTextures("scorch", 8);
+
+ public ParticleScorch(World world, double x, double y, double z){
+
+ super(world, x, y, z, TEXTURES[world.rand.nextInt(TEXTURES.length)]);
+
+ this.particleGravity = 0;
+ this.setMaxAge(100 + rand.nextInt(40));
+ this.particleScale *= 2;
+ // Defaults to black (which looks like a 'normal' scorch mark)
+ this.setRBGColorF(0, 0, 0);
+ this.shaded = false;
+ }
+
+ @Override
+ public boolean shouldDisableDepth(){
+ return true;
+ }
+
+ @Override
+ public void setRBGColorF(float r, float g, float b){
+ super.setRBGColorF(r, g, b);
+ this.setFadeColour(0, 0, 0); // Scorch particles fade to black by default
+ }
+
+ @Override
+ public void onUpdate(){
+
+ super.onUpdate();
+
+ // Colour fading (scorch particles do this slightly differently)
+ float ageFraction = Math.min((float)this.particleAge / ((float)this.particleMaxAge * 0.5f), 1);
+ // No longer uses setRBGColorF because that method now also sets the initial values
+ this.particleRed = this.initialRed + (this.fadeRed - this.initialRed) * ageFraction;
+ this.particleGreen = this.initialGreen + (this.fadeGreen - this.initialGreen) * ageFraction;
+ this.particleBlue = this.initialBlue + (this.fadeBlue - this.initialBlue) * ageFraction;
+
+ // Fading
+ if(this.particleAge > this.particleMaxAge/2){
+ this.setAlphaF(1 - ((float)this.particleAge - this.particleMaxAge/2f) / (this.particleMaxAge/2f));
+ }
+
+ EnumFacing facing = EnumFacing.fromAngle(yaw);
+ if(pitch == 90) facing = EnumFacing.UP;
+ if(pitch == -90) facing = EnumFacing.DOWN;
+
+ // Disappears if there is no block behind it (this is the same check used to spawn it)
+ if(!world.getBlockState(new BlockPos(posX, posY, posZ).offset(facing.getOpposite())).getMaterial().isSolid()){
+ this.setExpired();
+ }
+ }
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+ for(ResourceLocation texture : TEXTURES){
+ event.getMap().registerSprite(texture);
+ }
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleSnow.java b/src/main/java/electroblob/wizardry/client/particle/ParticleSnow.java
index e0d5250d..b34b3bab 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleSnow.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleSnow.java
@@ -1,45 +1,35 @@
package electroblob.wizardry.client.particle;
-import electroblob.wizardry.Wizardry;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-public class ParticleSnow extends ParticleCustomTexture {
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class ParticleSnow extends ParticleWizardry {
- private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID,
- "textures/particle/snow_particles.png");
+ private static final ResourceLocation[] TEXTURES = generateTextures("snow", 4);
- public ParticleSnow(World world, double x, double y, double z, double vx, double vy, double vz){
- super(world, x, y, z, vx, vy, vz);
- }
-
- public ParticleSnow(World world, double x, double y, double z, double vx, double vy, double vz, int maxAge){
- super(world, x, y, z, vx, vy, vz, maxAge);
- }
-
- @Override
- public void init(){
- this.setParticleTextureIndex(rand.nextInt(8));
+ public ParticleSnow(World world, double x, double y, double z){
+
+ super(world, x, y, z, TEXTURES[world.rand.nextInt(TEXTURES.length)]);
+
+ this.setVelocity(0, -0.02, 0);
this.particleScale *= 0.6f;
this.particleGravity = 0;
this.canCollide = true;
+ this.setMaxAge(40 + rand.nextInt(10));
+ // Produces a variety of light blues and whites
+ this.setRBGColorF(0.9f + 0.1f * random.nextFloat(), 0.95f + 0.05f * random.nextFloat(), 1);
}
-
- @Override
- public ResourceLocation getTexture(){
- return TEXTURE;
- }
-
- @Override
- protected int getXFrames(){
- return 4;
- }
-
- @Override
- protected int getYFrames(){
- return 4;
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+ for(ResourceLocation texture : TEXTURES){
+ event.getMap().registerSprite(texture);
+ }
}
}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleSpark.java b/src/main/java/electroblob/wizardry/client/particle/ParticleSpark.java
index 201d2373..7617d450 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleSpark.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleSpark.java
@@ -1,70 +1,58 @@
package electroblob.wizardry.client.particle;
-import org.lwjgl.opengl.GL11;
-
-import electroblob.wizardry.Wizardry;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-public class ParticleSpark extends ParticleCustomTexture {
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class ParticleSpark extends ParticleWizardry {
- private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID,
- "textures/particle/lightning_particles.png");
+ // 8 different animation strips, 4 in each strip
+ private static final ResourceLocation[][] TEXTURES = generateTextures("lightning", 8, 4);
- public ParticleSpark(World world, double x, double y, double z, double vx, double vy, double vz){
- // Max age is always 3.
- super(world, x, y, z, vx, vy, vz, 3);
- }
-
- @Override
- public void init(){
- // Multiplied by 4 because the index works slightly differently for spark particles.
- this.setParticleTextureIndex(rand.nextInt(8) * 4);
+ public ParticleSpark(World world, double x, double y, double z){
+
+ super(world, x, y, z, TEXTURES[world.rand.nextInt(TEXTURES.length)]);
+
this.particleScale *= 1.4f;
- this.fullBrightness = true;
+ this.setRBGColorF(1, 1, 1);
+ this.shaded = false;
this.canCollide = false;
+ this.setMaxAge(3); // Lifetime defaults to 3 (and is very unlikely to be changed)
}
@Override
- public void onUpdate(){
- super.onUpdate();
- // Well this is handy! Looks like vanilla uses the texture index like this too.
- this.nextTextureIndexX();
+ public boolean shouldDisableDepth(){
+ return true;
}
- @Override
- public ResourceLocation getTexture(){
- return TEXTURE;
- }
+ // May no longer be necessary, ParticleManager seems to enable blending now
- @Override
- protected int getXFrames(){
- return 4;
- }
-
- @Override
- protected int getYFrames(){
- return 8;
- }
-
- @Override
- public void applyGLStateChanges(){
- GlStateManager.enableBlend();
- GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
- // TESTME: Are these two actually necessary?
- GlStateManager.disableLighting();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240, 240);
- }
-
- @Override
- public void undoGLStateChanges(){
- GlStateManager.disableBlend();
- GlStateManager.enableLighting();
+// @Override
+// public void applyGLStateChanges(){
+// GlStateManager.enableBlend();
+// GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+// GlStateManager.disableLighting();
+// OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240, 240);
+// }
+//
+// @Override
+// public void undoGLStateChanges(){
+// GlStateManager.disableBlend();
+// GlStateManager.enableLighting();
+// }
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+ for(ResourceLocation[] array : TEXTURES){
+ for(ResourceLocation texture : array){
+ event.getMap().registerSprite(texture);
+ }
+ }
}
}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleSparkle.java b/src/main/java/electroblob/wizardry/client/particle/ParticleSparkle.java
index fdbd553c..812678a2 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleSparkle.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleSparkle.java
@@ -1,111 +1,45 @@
package electroblob.wizardry.client.particle;
-import electroblob.wizardry.Wizardry;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
-public class ParticleSparkle extends ParticleCustomTexture {
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class ParticleSparkle extends ParticleWizardry {
- /* I have now figured out what particle factories are for: they separate out the individual uses of the varargs
- * parameter in spawnParticle so they are kept with the particle class. For my purposes, it would be easier to do
- * that in the particle spawning method itself. */
+ private static final ResourceLocation[] TEXTURES = generateTextures("sparkle", 11);
- private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID,
- "textures/particle/sparkle_particles.png");
-
- // NOTE: Uncomment once 2.1.0 is released
- // private final float initialRed;
- // private final float initialGreen;
- // private final float initialBlue;
-
- // TODO: Assign these via the constructors, as part of the refactoring for particle parameters.
- // NOTE: Uncomment once 2.1.0 is released
- // private final float fadeRed = 1;
- // private final float fadeGreen = 1;
- // private final float fadeBlue = 0;
-
- public ParticleSparkle(World world, double x, double y, double z, double vx, double vy, double vz, float r, float g,
- float b){
- super(world, x, y, z, vx, vy, vz);
- this.setRBGColorF(r, g, b);
- // NOTE: Uncomment once 2.1.0 is released
- // initialRed = r;
- // initialGreen = g;
- // initialBlue = b;
+ public ParticleSparkle(World world, double x, double y, double z){
+
+ super(world, x, y, z, TEXTURES); // This time the textures are all one long animation
+
+ this.setRBGColorF(1, 1, 1);
this.particleMaxAge = 48 + this.rand.nextInt(12);
- }
-
- public ParticleSparkle(World world, double x, double y, double z, double vx, double vy, double vz, float r, float g,
- float b, int maxAge){
- super(world, x, y, z, vx, vy, vz, maxAge);
- this.setRBGColorF(r, g, b);
- // NOTE: Uncomment once 2.1.0 is released
- // initialRed = r;
- // initialGreen = g;
- // initialBlue = b;
- }
-
- public ParticleSparkle(World world, double x, double y, double z, double vx, double vy, double vz, float r, float g,
- float b, boolean doGravity){
- this(world, x, y, z, vx, vy, vz, r, g, b);
- this.particleGravity = doGravity ? 1 : 0;
- }
-
- public ParticleSparkle(World world, double x, double y, double z, double vx, double vy, double vz, float r, float g,
- float b, int maxAge, boolean doGravity){
- this(world, x, y, z, vx, vy, vz, r, g, b, maxAge);
- this.particleGravity = doGravity ? 1 : 0;
- }
-
- @Override
- public void init(){
- this.setParticleTextureIndex(rand.nextInt(16));
this.particleScale *= 0.75f;
this.particleGravity = 0;
this.canCollide = false;
- this.fullBrightness = true;
- }
-
- @Override
- public ResourceLocation getTexture(){
- return TEXTURE;
- }
-
- @Override
- protected int getXFrames(){
- return 4;
- }
-
- @Override
- protected int getYFrames(){
- return 4;
+ this.shaded = false;
}
@Override
public void onUpdate(){
super.onUpdate();
+
// Fading
if(this.particleAge > this.particleMaxAge / 2){
- this.setAlphaF(
- 1.0F - ((float)this.particleAge - (float)(this.particleMaxAge / 2)) / (float)this.particleMaxAge);
+ this.setAlphaF(1 - ((float)this.particleAge - (float)(this.particleMaxAge / 2)) / (float)this.particleMaxAge);
+ }
+ }
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+ for(ResourceLocation texture : TEXTURES){
+ event.getMap().registerSprite(texture);
}
- // Colour fading TODO Uncomment once 2.1.0 is released
- // float ageFraction = (float)this.particleAge / (float)this.particleMaxAge;
- // this.setRBGColorF(this.initialRed + (this.fadeRed - this.initialRed)*ageFraction,
- // this.initialGreen + (this.fadeGreen - this.initialGreen)*ageFraction,
- // this.initialBlue + (this.fadeBlue - this.initialBlue)*ageFraction);
-
- this.setParticleTextureIndex((this.particleAge * 11)/this.particleMaxAge);
}
-
- /* As a side note, I see a lot of magic mods with fancy-looking particle effects that really seem to 'glow'. It's
- * actually not that hard - you simply create a reasonably high-res texture with translucency and then set the
- * OpenGL blend function to something like SRC_ALPHA, SRC_ALPHA or ONE, ONE. The thing is... they're not very
- * Minecraft-y. I still maintain that part of wizardry's appeal is that it stays true to the game's pixelated charm,
- * rather than trying to make it something it's not. Still, the newer textures are much better than the defaults I
- * used to use. */
}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleSphere.java b/src/main/java/electroblob/wizardry/client/particle/ParticleSphere.java
new file mode 100644
index 00000000..89eaf5a7
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleSphere.java
@@ -0,0 +1,131 @@
+package electroblob.wizardry.client.particle;
+
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+import org.lwjgl.opengl.GL11;
+
+public class ParticleSphere extends ParticleWizardry {
+
+ public ParticleSphere(World world, double x, double y, double z){
+ super(world, x, y, z);
+ this.setRBGColorF(1, 1, 1);
+ this.particleMaxAge = 5;
+ this.particleAlpha = 0.8f;
+ }
+
+ @Override
+ public boolean shouldDisableDepth(){
+ return true;
+ }
+
+ @Override
+ public int getFXLayer(){
+ return 3;
+ }
+
+ @Override
+ public void onUpdate(){
+
+ super.onUpdate();
+
+ }
+
+ @Override
+ public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationZ, float rotationYZ,
+ float rotationXY, float rotationXZ){
+
+ // Copied from ParticleWizardry, needs to be here since we're not calling super
+ updateEntityLinking(partialTicks);
+
+ float x = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks);
+ float y = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks);
+ float z = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks);
+
+ GlStateManager.pushMatrix();
+ GlStateManager.translate(x - interpPosX, y - interpPosY, z - interpPosZ);
+
+ GlStateManager.disableLighting();
+ GlStateManager.enableBlend();
+ GlStateManager.enableCull();
+ GlStateManager.disableTexture2D();
+ GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE);
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ float latStep = (float)Math.PI/20;
+ float longStep = (float)Math.PI/20;
+
+ float sphereRadius = this.particleScale * (this.particleAge + partialTicks - 1) / this.particleMaxAge;
+ float alpha = this.particleAlpha * (1 - (this.particleAge + partialTicks - 1) / this.particleMaxAge);
+
+ drawSphere(Tessellator.getInstance(), buffer, sphereRadius, latStep, longStep, true, particleRed, particleGreen, particleBlue, alpha);
+ drawSphere(Tessellator.getInstance(), buffer, sphereRadius, latStep, longStep, false, particleRed, particleGreen, particleBlue, alpha);
+
+ GlStateManager.enableTexture2D();
+ GlStateManager.enableLighting();
+ GlStateManager.disableCull();
+ GlStateManager.disableBlend();
+
+ GlStateManager.popMatrix();
+
+ }
+
+ @Override
+ public int getBrightnessForRender(float partialTicks){
+ return 15728880;
+ }
+
+ /**
+ * Draws a sphere (using lat/long triangles) with the given parameters.
+ * @param radius The radius of the sphere.
+ * @param latStep The latitude step; smaller is smoother but increases performance cost.
+ * @param longStep The longitude step; smaller is smoother but increases performance cost.
+ * @param inside Whether to draw the outside or the inside of the sphere.
+ * @param r The red component of the sphere colour.
+ * @param g The green component of the sphere colour.
+ * @param b The blue component of the sphere colour.
+ * @param a The alpha component of the sphere colour.
+ */
+ private static void drawSphere(Tessellator tessellator, BufferBuilder buffer, float radius, float latStep, float longStep, boolean inside, float r, float g, float b, float a){
+
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_COLOR);
+
+ boolean goingUp = inside;
+
+ buffer.pos(0, goingUp ? -radius : radius, 0).color(r, g, b, a).endVertex(); // Start at the north pole
+
+ for(float longitude = -(float)Math.PI; longitude <= (float)Math.PI; longitude += longStep){
+
+ // Leave the poles out since they only have a single point per stack instead of two
+ for(float theta = (float)Math.PI/2 - latStep; theta >= -(float)Math.PI/2 + latStep; theta -= latStep){
+
+ float latitude = goingUp ? -theta : theta;
+
+ float hRadius = radius * MathHelper.cos(latitude);
+ float vy = radius * MathHelper.sin(latitude);
+ float vx = hRadius * MathHelper.sin(longitude);
+ float vz = hRadius * MathHelper.cos(longitude);
+
+ buffer.pos(vx, vy, vz).color(r, g, b, a).endVertex();
+
+ vx = hRadius * MathHelper.sin(longitude + longStep);
+ vz = hRadius * MathHelper.cos(longitude + longStep);
+
+ buffer.pos(vx, vy, vz).color(r, g, b, a).endVertex();
+ }
+
+ // The next pole
+ buffer.pos(0, goingUp ? radius : -radius, 0).color(r, g, b, a).endVertex();
+
+ goingUp = !goingUp;
+ }
+
+ tessellator.draw();
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleSummon.java b/src/main/java/electroblob/wizardry/client/particle/ParticleSummon.java
new file mode 100644
index 00000000..55daba3e
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleSummon.java
@@ -0,0 +1,132 @@
+package electroblob.wizardry.client.particle;
+
+import electroblob.wizardry.Wizardry;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.*;
+import net.minecraft.client.renderer.GlStateManager.DestFactor;
+import net.minecraft.client.renderer.GlStateManager.SourceFactor;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+import org.lwjgl.opengl.GL11;
+
+//@SideOnly(Side.CLIENT)
+public class ParticleSummon extends ParticleWizardry {
+
+ private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/particle/summon.png");
+ private final boolean mirror;
+
+ public ParticleSummon(World world, double x, double y, double z){
+ super(world, x, y, z);
+ this.mirror = random.nextBoolean();
+ this.setMaxAge(10);
+ this.setGravity(false);
+ this.canCollide = false;
+ }
+
+// @Override
+// public void onUpdate(){
+// super.onUpdate();
+// if(this.particleAge > this.particleMaxAge/2) this.particleAlpha = 2f - 2f*(float)this.particleAge/(float)this.particleMaxAge;
+// }
+
+ /* There are 4 layers of particles, specified as 0-3 by the method below. - Layer 0 causes the normal particles.png
+ * to be bound to the render engine for normal particles. - Layer 1 causes the block textures to be bound to the
+ * render engine for digging fx and falling fx. - Layer 2 causes the item textures to be bound to the render engine
+ * for tool breaking fx, snowballpoofs, slime particles, etc. - Layer 3 is not used in vanilla minecraft and was
+ * presumably added by forge for exactly this reason. This means no texture is bound by vanilla minecraft, meaning
+ * you are free to do as you wish without possibly overwriting vanilla particles. Mod particles won't be overwritten
+ * anyway since they bind their own textures. It is of course important to bind the texture every time you render a
+ * custom particle, but I don't see how you could do it any other way, since you don't have access to
+ * EffectRenderer. */
+ @Override
+ public int getFXLayer(){
+ // This can only be 0-3 or it will cause an ArrayIndexOutOfBoundsException in EffectRenderer.
+ return 3;
+ }
+
+ @Override
+ public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationZ,
+ float rotationYZ, float rotationXY, float rotationXZ){
+
+ // Copied from ParticleWizardry, needs to be here since we're not calling super
+ updateEntityLinking(partialTicks);
+
+ GlStateManager.pushMatrix();
+ GlStateManager.pushAttrib();
+
+ float scale = 0.6f;
+ GlStateManager.scale(scale, scale, scale);
+ if(mirror) GlStateManager.scale(-1, 1, 1);
+
+ GlStateManager.enableBlend();
+ GlStateManager.disableAlpha();
+ GlStateManager.disableCull();
+ GlStateManager.disableLighting();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE);
+ // Makes the particle colour add to the colour of the texture pixels, rather than the default multiplying
+ GlStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_ADD);
+
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
+ GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
+
+ // Does the texture translation wrapping thing (the cool stuff)
+// GlStateManager.matrixMode(GL11.GL_TEXTURE);
+// GlStateManager.loadIdentity();
+//
+// GlStateManager.translate((this.particleAge + partialTicks)/(float)this.particleMaxAge * -2, 0, 0);
+//
+// GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+
+ RenderHelper.disableStandardItemLighting();
+
+ Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE);
+
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR);
+
+ float x = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks - interpPosX);
+ float y = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks - interpPosY);
+ float z = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks - interpPosZ);
+
+ // Increases from 0 to 1 in steps of 0.125 evenly throughout the particle's lifetime
+ float f = 0.125f * MathHelper.floor((float)this.particleAge/(float)this.particleMaxAge * 8 - 0.000001f);
+ float g = f + 0.125f;
+ float hrepeat = 1;
+ float yScale = 3f;
+
+ this.setRBGColorF(1, 1, 1);
+
+ buffer.pos(x-1, y, z-1).tex(0, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-1, y+yScale, z-1).tex(0, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+1, y, z-1).tex(0.25*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+1, y+yScale, z-1).tex(0.25*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+1, y, z+1).tex(0.5*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x+1, y+yScale, z+1).tex(0.5*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-1, y, z+1).tex(0.75*hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-1, y+yScale, z+1).tex(0.75*hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-1, y, z-1).tex(hrepeat, g).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+ buffer.pos(x-1, y+yScale, z-1).tex(hrepeat, f).color(particleRed, particleGreen, particleBlue, particleAlpha).endVertex();
+
+ Tessellator.getInstance().draw();
+
+ // Undoes the texture transformations
+// GlStateManager.matrixMode(GL11.GL_TEXTURE);
+// GlStateManager.loadIdentity();
+// GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+
+ GlStateManager.disableBlend();
+ GlStateManager.enableAlpha();
+ GlStateManager.enableCull();
+ GlStateManager.enableLighting();
+ // Reverses the colour addition change from before
+ GlStateManager.glTexEnvi(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
+
+ GlStateManager.popAttrib();
+ GlStateManager.popMatrix();
+
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleTargeted.java b/src/main/java/electroblob/wizardry/client/particle/ParticleTargeted.java
new file mode 100644
index 00000000..cd89d0af
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleTargeted.java
@@ -0,0 +1,150 @@
+package electroblob.wizardry.client.particle;
+
+import electroblob.wizardry.Wizardry;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+import org.lwjgl.opengl.GL11;
+
+import javax.annotation.Nullable;
+
+/** Superclass for particles with a second target entity or target position. */
+public abstract class ParticleTargeted extends ParticleWizardry {
+
+ protected double targetX;
+ protected double targetY;
+ protected double targetZ;
+ protected double targetVelX;
+ protected double targetVelY;
+ protected double targetVelZ;
+
+ protected double length;
+
+ /** The target this particle is linked to. The particle will stretch to touch this entity. */
+ @Nullable
+ protected Entity target = null;
+
+ public ParticleTargeted(World world, double x, double y, double z, ResourceLocation... textures){
+ super(world, x, y, z, textures);
+ }
+
+ @Override
+ public void setTargetPosition(double x, double y, double z){
+ this.targetX = x;
+ this.targetY = y;
+ this.targetZ = z;
+ }
+
+ @Override
+ public void setTargetVelocity(double vx, double vy, double vz){
+ this.targetVelX = vx;
+ this.targetVelY = vy;
+ this.targetVelZ = vz;
+ }
+
+ @Override
+ public void setTargetEntity(Entity target){
+ this.target = target;
+ }
+
+ @Override
+ public void setLength(double length){
+ this.length = length;
+ }
+
+ @Override
+ public void onUpdate(){
+
+ super.onUpdate();
+
+ if(!Double.isNaN(targetVelX) && !Double.isNaN(targetVelY) && !Double.isNaN(targetVelZ)){
+ this.targetX += this.targetVelX;
+ this.targetY += this.targetVelY;
+ this.targetZ += this.targetVelZ;
+ }
+ }
+
+ @Override
+ public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationZ, float rotationYZ,
+ float rotationXY, float rotationXZ){
+
+ // Copied from ParticleWizardry, needs to be here since we're not calling super
+ updateEntityLinking(partialTicks);
+
+ float x = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)partialTicks);
+ float y = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)partialTicks);
+ float z = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)partialTicks);
+
+ if(this.target != null){
+
+ this.targetX = this.target.prevPosX + (this.target.posX - this.target.prevPosX) * partialTicks;
+ double correction = this.target.getEntityBoundingBox().minY - this.target.posY;
+ this.targetY = this.target.prevPosY + (this.target.posY - this.target.prevPosY) * partialTicks
+ + target.height/2 + correction;
+ this.targetZ = this.target.prevPosZ + (this.target.posZ - this.target.prevPosZ) * partialTicks;
+
+ }else if(this.entity != null && this.length > 0){
+
+ Vec3d look = entity.getLook(partialTicks).scale(length);
+ this.targetX = x + look.x;
+ this.targetY = y + look.y;
+ this.targetZ = z + look.z;
+ }
+
+ if(Double.isNaN(targetX) || Double.isNaN(targetY) || Double.isNaN(targetZ)){
+ Wizardry.logger.warn("Attempted to render a targeted particle, but neither its target entity nor target"
+ + "position was set, and it either had no length assigned or was not linked to an entity!");
+ return;
+ }
+
+ GlStateManager.pushMatrix();
+ GlStateManager.translate(x - interpPosX, y - interpPosY, z - interpPosZ);
+
+ double dx = this.targetX - x;
+ double dy = this.targetY - y;
+ double dz = this.targetZ - z;
+
+ // No need for previous tick target positions and all that stuff since this is the only place they're used
+ // and interpolating like this works just as well
+ if(!Double.isNaN(targetVelX) && !Double.isNaN(targetVelY) && !Double.isNaN(targetVelZ)){
+ dx += partialTicks * this.targetVelX;
+ dy += partialTicks * this.targetVelY;
+ dz += partialTicks * this.targetVelZ;
+ }
+
+ // The distance from origin to endpoint
+ double length = Math.sqrt(dx*dx+dy*dy+dz*dz);
+
+ // Math.atan2 computes within -180 to +180, rather than -90 to +90.
+ float yaw = (float)(180d/Math.PI * Math.atan2(dx, dz));
+ float pitch = (float)(180f/(float)Math.PI * Math.atan(-dy/Math.sqrt(dz*dz+dx*dx)));
+
+ GL11.glRotatef(yaw, 0, 1, 0);
+ GL11.glRotatef(pitch, 1, 0, 0);
+
+ Tessellator tessellator = Tessellator.getInstance();
+
+ this.draw(tessellator, length, partialTicks);
+
+ GlStateManager.popMatrix();
+ }
+
+ /** Called from {@link ParticleTargeted#renderParticle(BufferBuilder, Entity, float, float, float, float, float, float)},
+ * once the appropriate calculations and transformations have been applied, to actually render the particle. Subclasses
+ * override this instead of overriding {@code renderParticle} directly, and inside render the particle along
+ * the z-axis, starting at (0, 0, 0) - it will be translated and rotated automatically.
+ *
+ * N.B. Other than transformations, no GL state changes are applied; these should be done within this method.
+ *
+ * @param tessellator A reference to the tessellator, for convenience.
+ * @param length The distance from the origin to the endpoint for the particle being rendered; the particle should
+ * therefore be rendered between (0, 0, 0) and (0, 0, length) within this method.
+ * @param partialTicks The partial tick time.
+ */
+ protected abstract void draw(Tessellator tessellator, double length, float partialTicks);
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleTornado.java b/src/main/java/electroblob/wizardry/client/particle/ParticleTornado.java
index 8887e503..8dab58fa 100644
--- a/src/main/java/electroblob/wizardry/client/particle/ParticleTornado.java
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleTornado.java
@@ -3,14 +3,13 @@ package electroblob.wizardry.client.particle;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.particle.ParticleDigging;
import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class ParticleTornado extends ParticleDigging {
- private double angle;
+ private float angle;
private double radius;
private double speed;
/** Velocity of the tornado itself; in other words the velocity of the point the particle circles around. */
@@ -20,9 +19,9 @@ public class ParticleTornado extends ParticleDigging {
public ParticleTornado(World world, int maxAge, double originX, double originZ, double radius, double yPos,
double velX, double velZ, IBlockState block){
super(world, 0, 0, 0, 0, 0, 0, block);
- this.angle = this.rand.nextDouble() * Math.PI * 2;
- double x = originX - Math.cos(angle) * radius;
- double z = originZ + radius * Math.sin(angle);
+ float angle = this.rand.nextFloat() * (float)Math.PI * 2;
+ double x = originX - MathHelper.cos(angle) * radius;
+ double z = originZ + radius * MathHelper.sin(angle);
this.radius = radius;
this.setPosition(x, yPos, z);
this.prevPosX = x;
@@ -66,8 +65,8 @@ public class ParticleTornado extends ParticleDigging {
// v = r times omega; therefore the normalised velocity vector needs to be r times the angle increment / 2 pi.
this.angle += omega;
- this.motionZ = radius * omega * Math.cos(angle);
- this.motionX = radius * omega * Math.sin(angle);
+ this.motionZ = radius * omega * MathHelper.cos(angle);
+ this.motionX = radius * omega * MathHelper.sin(angle);
this.move(motionX + velX, 0, motionZ + velZ);
if(this.particleAge > this.particleMaxAge / 2){
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleVine.java b/src/main/java/electroblob/wizardry/client/particle/ParticleVine.java
new file mode 100644
index 00000000..79c9d872
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleVine.java
@@ -0,0 +1,154 @@
+package electroblob.wizardry.client.particle;
+
+import electroblob.wizardry.Wizardry;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import org.lwjgl.opengl.GL11;
+
+//@SideOnly(Side.CLIENT)
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class ParticleVine extends ParticleTargeted {
+
+ /** Half the width of the vine. */
+ private static final float THICKNESS = 0.02f;
+ private static final float LEAF_SPACING = 0.5f;
+ private static final float SEGMENT_LENGTH = 1;
+
+ private static final ResourceLocation STEM_TEXTURE = new ResourceLocation(Wizardry.MODID, "particle/vine");
+ private static final ResourceLocation[] LEAF_TEXTURES = generateTextures("vine_leaf", 5);
+
+ public ParticleVine(World world, double x, double y, double z){
+ super(world, x, y, z, STEM_TEXTURE);
+ //this.setRBGColorF(1, 1, 1);
+ this.setMaxAge(0);
+ this.particleScale = 1;
+ this.setRBGColorF(0.2f, 0.65f, 0f);
+ }
+
+ @Override
+ public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationZ, float rotationYZ, float rotationXY, float rotationXZ){
+ // When using FX layer 1 the BufferBuilder is already drawing... but in the wrong mode :/
+ Tessellator.getInstance().draw();
+ super.renderParticle(buffer, viewer, partialTicks, rotationX, rotationZ, rotationYZ, rotationXY, rotationXZ);
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.PARTICLE_POSITION_TEX_COLOR_LMAP);
+ }
+
+ @Override
+ protected void draw(Tessellator tessellator, double length, float partialTicks){
+
+ random.setSeed(seed); // Reset the random so we get the same sequence of numbers each frame
+
+ float scale = this.particleScale;
+
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ GlStateManager.disableLighting();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ // Hmmmm we can't get the texture to tile using OpenGL texture space because it's on a sprite sheet...
+ // Solution: Draw loads of boxes. Simple!
+ // (Since there aren't going to be that many of these particles around we could have not used sprite sheets
+ // and done the OpenGL texture space thing, but this is kinda easier)
+ // Everything is drawn back-to-front so it looks like the vine is growing from the origin, not the endpoint
+ int i = 0;
+ while(i + SEGMENT_LENGTH < length){
+ drawShearedBox(tessellator, 0, 0, length-i, 0, 0, length-i-SEGMENT_LENGTH, THICKNESS * scale,
+ particleRed, particleGreen, particleBlue, particleAlpha);
+ i += SEGMENT_LENGTH;
+ }
+
+ drawShearedBox(tessellator, 0, 0, length-i, 0, 0, 0, THICKNESS * scale,
+ particleRed, particleGreen, particleBlue, particleAlpha);
+
+ for(double l=length; l>0; l-=LEAF_SPACING){
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.rotate(random.nextInt(4) * 90, 0, 0, 1);
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR);
+
+ TextureAtlasSprite leaf = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(
+ LEAF_TEXTURES[random.nextInt(LEAF_TEXTURES.length)].toString());
+
+ float w = 16 * THICKNESS * scale;
+ float u1 = leaf.getMinU();
+ float u2 = leaf.getMaxU();
+ float v1 = leaf.getMinV();
+ float v2 = leaf.getMaxV();
+
+ float colourVariation = 0.3f;
+
+ float r = MathHelper.clamp(particleRed + (random.nextFloat() - 0.5f) * colourVariation, 0, 1);
+ float g = MathHelper.clamp(particleGreen + (random.nextFloat() - 0.5f) * colourVariation, 0, 1);
+ float b = MathHelper.clamp(particleBlue + (random.nextFloat() - 0.5f) * colourVariation, 0, 1);
+
+ buffer.pos(0, 0, l).tex(u1, v1).color(r, g, b, particleAlpha).endVertex();
+ buffer.pos(w, 0, l).tex(u2, v1).color(r, g, b, particleAlpha).endVertex();
+ buffer.pos(w, w, l).tex(u2, v2).color(r, g, b, particleAlpha).endVertex();
+ buffer.pos(0, w, l).tex(u1, v2).color(r, g, b, particleAlpha).endVertex();
+
+ tessellator.draw();
+
+ GlStateManager.popMatrix();
+ }
+
+ // Makes the rain go weird
+ //GlStateManager.enableLighting();
+ }
+
+ /** Draws a single box for one segment of the arc, from the point (x1, y1, z1) to the point (x2, y2, z2), with given width and colour. */
+ private void drawShearedBox(Tessellator tessellator, double x1, double y1, double z1, double x2, double y2, double z2, float width, float r, float g, float b, float a){
+
+ float u1 = particleTexture.getMinU();
+ float u2 = u1 + (particleTexture.getMaxU() - u1) * (float)(z1-z2)/SEGMENT_LENGTH;
+ float v1 = particleTexture.getMinV();
+ float dv = particleTexture.getMaxV() - v1;
+ // width * 8 gives the total 'circumference' of the box
+ float v2 = v1 + dv * 0.0625f;
+ float v3 = v1 + dv * 0.125f;
+ float v4 = v1 + dv * 0.1875f;
+ float v5 = v1 + dv * 0.25f;
+
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR);
+
+ buffer.pos(x1-width, y1-width, z1).tex(u1, v1).color(r, g, b, a).endVertex();
+ buffer.pos(x2-width, y2-width, z2).tex(u2, v1).color(r, g, b, a).endVertex();
+ buffer.pos(x1-width, y1+width, z1).tex(u1, v2).color(r, g, b, a).endVertex();
+ buffer.pos(x2-width, y2+width, z2).tex(u2, v2).color(r, g, b, a).endVertex();
+ buffer.pos(x1+width, y1+width, z1).tex(u1, v3).color(r, g, b, a).endVertex();
+ buffer.pos(x2+width, y2+width, z2).tex(u2, v3).color(r, g, b, a).endVertex();
+ buffer.pos(x1+width, y1-width, z1).tex(u1, v4).color(r, g, b, a).endVertex();
+ buffer.pos(x2+width, y2-width, z2).tex(u2, v4).color(r, g, b, a).endVertex();
+ buffer.pos(x1-width, y1-width, z1).tex(u1, v5).color(r, g, b, a).endVertex();
+ buffer.pos(x2-width, y2-width, z2).tex(u2, v5).color(r, g, b, a).endVertex();
+
+ tessellator.draw();
+ }
+
+ @SubscribeEvent
+ public static void onTextureStitchEvent(TextureStitchEvent.Pre event){
+
+ event.getMap().registerSprite(STEM_TEXTURE);
+
+ for(ResourceLocation texture : LEAF_TEXTURES){
+ event.getMap().registerSprite(texture);
+ }
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/particle/ParticleWizardry.java b/src/main/java/electroblob/wizardry/client/particle/ParticleWizardry.java
new file mode 100644
index 00000000..1172bb34
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/particle/ParticleWizardry.java
@@ -0,0 +1,580 @@
+package electroblob.wizardry.client.particle;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.client.ClientProxy;
+import electroblob.wizardry.entity.ICustomHitbox;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.particle.Particle;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.entity.Entity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.AxisAlignedBB;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.TextureStitchEvent;
+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;
+import java.util.stream.Collectors;
+
+/**
+ * Abstract superclass for all of wizardry's particles. This replaces {@code ParticleCustomTexture} (the functionality of
+ * which is no longer necessary since wizardry now uses {@code TextureAtlasSprite}s to do the rendering), and fits into
+ * {@code ParticleBuilder} by exposing all the necessary variables through getters, allowing them to be set on the fly
+ * rather than needing to be passed into the constructor.
+ *
+ * The new system is as follows:
+ *
+ * - All particle classes have a single constructor which takes a world and a position only.
+ * - Each particle class defines any relevant default values in its constructor, including velocity.
+ * - The particle builder then overwrites any other values that were set during building.
+ *
+ * This beauty of this system is that there are never any redundant parameters when spawning particles, since you can set
+ * as many or as few parameters as necessary - and in addition, common defaults don't need setting at all. For example,
+ * snow particles nearly always fall at the same speed, which can now be defined in the particle class and no longer
+ * needs to be defined when spawning the particle - but importantly, it can still be overridden if desired.
+ *
+ * @author Electroblob
+ * @since Wizardry 4.2.0
+ * @see electroblob.wizardry.util.ParticleBuilder ParticleBuilder
+ */
+//@SideOnly(Side.CLIENT)
+public abstract class ParticleWizardry extends Particle {
+
+ /** Implementation of animated particles using the TextureAtlasSprite system. Why vanilla doesn't support this I
+ * don't know, considering it too has animated particles. */
+ protected final TextureAtlasSprite[] sprites;
+
+ /** A long value used by the renderer as a random number seed, ensuring anything that is randomised remains the
+ * same across multiple frames. For example, lightning particles use this to keep their shape across ticks.
+ * This value can also be set during particle creation, allowing users to keep randomised properties the same
+ * even across multiple particles. If unspecified, the seed is chosen at random. */
+ protected long seed;
+ /** This particle's random number generator. All particles should use this in preference to any other random
+ * instance (like random), even if it isn't actually necessary to keep properties across frames. Note that
+ * if you do need to generate the same sequence of random numbers each frame, you must call
+ * {@code random.setSeed(seed)} from the {@link ParticleWizardry#renderParticle(BufferBuilder, Entity, float, float, float, float, float, float)}
+ * method - this is not done automatically. */
+ protected Random random = new Random(); // If we're not using a seed, this defaults to any old seed
+
+ /** True if the particle is shaded, false if the particle always renders at full brightness. Defaults to false. */
+ protected boolean shaded = false;
+
+ protected float initialRed;
+ protected float initialGreen;
+ protected float initialBlue;
+
+ protected float fadeRed = 0;
+ protected float fadeGreen = 0;
+ protected float fadeBlue = 0;
+
+ protected float angle;
+ protected double radius = 0;
+ protected double speed = 0;
+
+ /** The entity this particle is linked to. The particle will move with this entity. */
+ @Nullable
+ protected Entity entity = null;
+ /** Coordinates of this particle relative to the linked entity. If the linked entity is null, these are used as
+ * the absolute coordinates of the centre of rotation for particles with spin. If the particle has neither a
+ * linked entity nor spin, these are not used. */
+ protected double relativeX, relativeY, relativeZ;
+ /** Velocity of this particle relative to the linked entity. If the linked entity is null, these are not used. */
+ protected double relativeMotionX, relativeMotionY, relativeMotionZ;
+ // Note that roll (equivalent to rotating the texture) is effectively handled by particleAngle - although that is
+ // actually the rotation speed and not the angle itself.
+ /** The yaw angle this particle is facing, or {@code NaN} if this particle always faces the viewer (default behaviour). */
+ protected float yaw = Float.NaN;
+ /** The pitch angle this particle is facing, or {@code NaN} if this particle always faces the viewer (default behaviour). */
+ protected float pitch = Float.NaN;
+
+ /** The fraction of the impact velocity that should be the maximum spread speed added on impact. */
+ private static final double SPREAD_FACTOR = 0.2;
+ /** Lateral velocity is reduced by this factor on impact, before adding random spread velocity. */
+ private static final double IMPACT_FRICTION = 0.2;
+
+ /** Previous-tick velocity, used in collision detection. */
+ private double prevVelX, prevVelY, prevVelZ;
+
+ /**
+ * Creates a new particle in the given world at the given position. All other parameters are set via the various
+ * setter methods ({@link electroblob.wizardry.util.ParticleBuilder ParticleBuilder} deals with all of that anyway).
+ * @param world The world in which to create the particle.
+ * @param x The x-coordinate at which to create the particle.
+ * @param y The y-coordinate at which to create the particle.
+ * @param z The z-coordinate at which to create the particle.
+ * @param textures One or more {@code ResourceLocation}s representing the texture(s) used by this particle. These
+ * must be registered as {@link TextureAtlasSprite}s using {@link TextureStitchEvent} or the textures will be
+ * missing. If more than one {@code ResourceLocation} is specified, the particle will be animated with each texture
+ * shown in order for an equal proportion of the particle's lifetime. If this argument is omitted (or a zero-length
+ * array is given), the particle will use the vanilla system instead (based on the X/Y texture indices).
+ */
+ public ParticleWizardry(World world, double x, double y, double z, ResourceLocation... textures){
+
+ super(world, x, y, z);
+
+ // Sets the relative coordinates in case they are needed
+ this.relativeX = x;
+ this.relativeY = y;
+ this.relativeZ = z;
+
+ // Deals with the textures
+ if(textures.length > 0){
+
+ sprites = Arrays.stream(textures).map(t -> Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(
+ t.toString())).collect(Collectors.toList()).toArray(new TextureAtlasSprite[0]);
+
+ this.setParticleTexture(sprites[0]);
+
+ }else{
+ sprites = new TextureAtlasSprite[0];
+ }
+ }
+
+ // ============================================== Parameter Setters ==============================================
+
+ // Setters for parameters that affect all particles - these are implemented in this class (although they may be
+ // reimplemented in subclasses)
+
+ /** Sets the seed for this particle's randomly generated values and resets {@link ParticleWizardry#random} to use
+ * that seed. Implementations will differ between particle types; for example, ParticleLightning has an update
+ * period which changes the seed every few ticks, whereas ParticleVine simply retains the same seed for its entire
+ * lifetime. */
+ public void setSeed(long seed){
+ this.seed = seed;
+ this.random = new Random(seed);
+ }
+
+ /** Sets whether the particle should render at full brightness or not. True if the particle is shaded, false if
+ * the particle always renders at full brightness. Defaults to false.*/
+ public void setShaded(boolean shaded){
+ this.shaded = shaded;
+ }
+
+ /** Sets this particle's gravity. True to enable gravity, false to disable. Defaults to false.*/
+ public void setGravity(boolean gravity){
+ this.particleGravity = gravity ? 1 : 0;
+ }
+
+ /** Sets this particle's collisions. True to enable block collisions, false to disable. Defaults to false.*/
+ public void setCollisions(boolean canCollide){
+ this.canCollide = canCollide;
+ }
+
+ /**
+ * Sets the velocity of the particle.
+ * @param vx The x velocity
+ * @param vy The y velocity
+ * @param vz The z velocity
+ */
+ public void setVelocity(double vx, double vy, double vz){
+ this.motionX = vx;
+ this.motionY = vy;
+ this.motionZ = vz;
+ }
+
+ /**
+ * Sets the spin parameters of the particle.
+ * @param radius The spin radius
+ * @param speed The spin speed in rotations per tick
+ */
+ public void setSpin(double radius, double speed){
+ this.radius = radius;
+ this.speed = speed * 2 * Math.PI; // Converts rotations per tick into radians per tick for the trig functions
+ this.angle = this.rand.nextFloat() * (float)Math.PI * 2; // Random start angle
+ // Need to set the start position or the circle won't be centred on the correct position
+ this.posX = relativeX - radius * MathHelper.cos(angle);
+ this.posZ = relativeZ + radius * MathHelper.sin(angle);
+ // Set these to the correct values
+ this.relativeMotionX = motionX;
+ this.relativeMotionY = motionY;
+ this.relativeMotionZ = motionZ;
+ }
+
+ /**
+ * Links this particle to the given entity. This will cause its position and velocity to be relative to the entity.
+ * @param entity The entity to link to.
+ */
+ public void setEntity(Entity entity){
+ this.entity = entity;
+ // Set these to the correct values
+ if(entity != null){
+ this.setPosition(this.entity.posX + relativeX, this.entity.getEntityBoundingBox().minY
+ + relativeY, this.entity.posZ + relativeZ);
+ this.prevPosX = this.posX;
+ this.prevPosY = this.posY;
+ this.prevPosZ = this.posZ;
+ // Set these to the correct values
+ this.relativeMotionX = motionX;
+ this.relativeMotionY = motionY;
+ this.relativeMotionZ = motionZ;
+ }
+ }
+
+ // Overridden to set the initial colour values
+ /**
+ * Sets the base colour of the particle. Note that this also sets the fade colour so that particles without a
+ * fade colour do not change colour at all; as such fade colour must be set after calling this method.
+ * @param r The red colour component
+ * @param g The green colour component
+ * @param b The blue colour component
+ */
+ @Override
+ public void setRBGColorF(float r, float g, float b){
+ super.setRBGColorF(r, g, b);
+ initialRed = r;
+ initialGreen = g;
+ initialBlue = b;
+ // If fade colour is not specified, it defaults to the main colour - this method is always called first
+ setFadeColour(r, g, b);
+ }
+
+ /**
+ * Sets the fade colour of the particle.
+ * @param r The red colour component
+ * @param g The green colour component
+ * @param b The blue colour component
+ */
+ public void setFadeColour(float r, float g, float b){
+ this.fadeRed = r;
+ this.fadeGreen = g;
+ this.fadeBlue = b;
+ }
+
+ /**
+ * Sets the direction this particle faces. This will cause the particle to render facing the given direction.
+ * @param yaw The yaw angle of this particle in degrees, where 0 is south.
+ * @param pitch The pitch angle of this particle in degrees, where 0 is horizontal.
+ */
+ public void setFacing(float yaw, float pitch){
+ this.yaw = yaw;
+ this.pitch = pitch;
+ }
+
+ // Setters for parameters that only affect some particles - these are unimplemented in this class because they
+ // doesn't make sense for most particles
+
+ /**
+ * Sets the target position for this particle. This will cause it to stretch to touch the given position,
+ * if supported.
+ * @param x The x-coordinate of the target position.
+ * @param y The y-coordinate of the target position.
+ * @param z The z-coordinate of the target position.
+ */
+ public void setTargetPosition(double x, double y, double z){
+ // Does nothing for normal particles since normal particles always render at a single point
+ }
+
+ /**
+ * Sets the target point velocity for this particle. This will cause the position it stretches to touch to move
+ * at the given velocity. Has no effect unless {@link ParticleWizardry#setTargetVelocity(double, double, double)}
+ * is also used.
+ * @param vx The x velocity of the target point.
+ * @param vy The y velocity of the target point.
+ * @param vz The z velocity of the target point.
+ */
+ public void setTargetVelocity(double vx, double vy, double vz){
+ // Does nothing for normal particles since normal particles always render at a single point
+ }
+
+ /**
+ * Links this particle to the given target. This will cause it to stretch to touch the target, if supported.
+ * @param target The target to link to.
+ */
+ public void setTargetEntity(Entity target){
+ // Does nothing for normal particles since normal particles always render at a single point
+ }
+
+ /**
+ * Sets the length of this particle. This will cause it to stretch to touch a point this distance along its
+ * linked entity's line of sight.
+ * @param length The length to set.
+ */
+ public void setLength(double length){
+ // Does nothing for normal particles since normal particles always render at a single point
+ }
+
+ // ============================================== Method Overrides ==============================================
+
+ @Override
+ public int getFXLayer(){
+ return sprites.length == 0 ? super.getFXLayer() : 1; // This has to be 1 for the TextureAtlasSprites to work
+ }
+
+ @Override
+ public int getBrightnessForRender(float partialTick){
+ return shaded ? super.getBrightnessForRender(partialTick) : 15728880;
+ }
+
+ /**
+ * Renders the particle. The mapping names given to the parameters in this method are very misleading; see below for
+ * details of what they actually do. (They're also in a strange order...)
+ * @param buffer The {@code BufferBuilder} object.
+ * @param viewer The entity whose viewpoint the particle is being rendered from; this should always be the
+ * client-side player.
+ * @param partialTicks The partial tick time.
+ * @param lookZ Equal to the cosine of {@code viewer.rotationYaw}. Will be -1 when facing north (negative Z), 0 when
+ * east/west, and +1 when facing south (positive Z). Independent of pitch.
+ * @param lookY Equal to the cosine of {@code viewer.rotationPitch}. Will be 1 when facing directly up or down, and 0
+ * when facing directly horizontally.
+ * @param lookX Equal to the sine of {@code viewer.rotationYaw}. Will be -1 when facing east (positive X), 0 when
+ * facing north/south, and +1 when facing west (negative X). Independent of pitch.
+ * @param lookXY Equal to {@code lookX} times the sine of {@code viewer.rotationPitch}. Will be 0 when facing directly horizontal.
+ * When facing directly up, will be equal to {@code -lookX}. When facing directly down, will be equal to {@code lookX}.
+ * @param lookYZ Equal to {@code -lookZ} times the sine of {@code viewer.rotationPitch}. Will be 0 when facing directly horizontal.
+ * When facing directly up, will be equal to {@code -lookZ}. When facing directly down, will be equal to {@code lookZ}.
+ */
+ // Fun fact: unlike entities, particles don't seem to bother checking the camera frustum...
+ @Override
+ public void renderParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float lookZ, float lookY,
+ float lookX, float lookXY, float lookYZ){
+
+ updateEntityLinking(partialTicks);
+
+ if(Float.isNaN(this.yaw) || Float.isNaN(this.pitch)){
+ // Normal behaviour (rotates to face the viewer)
+ drawParticle(buffer, viewer, partialTicks, lookZ, lookY, lookX, lookXY, lookYZ);
+ }else{
+
+ // Specific rotation
+
+ // Copied from ActiveRenderInfo; converts yaw and pitch into the weird parameters used by renderParticle.
+ // The 1st/3rd person distinction has been removed since this has nothing to do with the view angle.
+
+ float degToRadFactor = 0.017453292f; // Conversion from degrees to radians
+
+ float rotationX = MathHelper.cos(yaw * degToRadFactor);
+ float rotationZ = MathHelper.sin(yaw * degToRadFactor);
+ float rotationY = MathHelper.cos(pitch * degToRadFactor);
+ float rotationYZ = -rotationZ * MathHelper.sin(pitch * degToRadFactor);
+ float rotationXY = rotationX * MathHelper.sin(pitch * degToRadFactor);
+
+ drawParticle(buffer, viewer, partialTicks, rotationX, rotationY, rotationZ, rotationYZ, rotationXY);
+ }
+ }
+
+ /**
+ * Delegate function for {@link ParticleWizardry#renderParticle(BufferBuilder, Entity, float, float, float, float, float, float)};
+ * does the actual rendering. Subclasses should override this method instead of renderParticle. By default, this
+ * method simply calls super.renderParticle.
+ */
+ protected void drawParticle(BufferBuilder buffer, Entity viewer, float partialTicks, float rotationX, float rotationY, float rotationZ, float rotationYZ, float rotationXY){
+ super.renderParticle(buffer, viewer, partialTicks, rotationX, rotationY, rotationZ, rotationYZ, rotationXY);
+ }
+
+ protected void updateEntityLinking(float partialTicks){
+ if(this.entity != null){
+ // This is kind of cheating but we know it's always a constant velocity so it works fine
+ prevPosX = posX + entity.prevPosX - entity.posX - relativeMotionX * (1-partialTicks);
+ prevPosY = posY + entity.prevPosY - entity.posY - relativeMotionY * (1-partialTicks);
+ prevPosZ = posZ + entity.prevPosZ - entity.posZ - relativeMotionZ * (1-partialTicks);
+ }
+ }
+
+ @Override
+ public void onUpdate(){
+
+ super.onUpdate();
+
+ if(this.canCollide && this.onGround){
+ // I reject your friction and substitute my own!
+ this.motionX /= 0.699999988079071D;
+ this.motionZ /= 0.699999988079071D;
+ }
+
+ if(entity != null || radius > 0){
+
+ double x = relativeX;
+ double y = relativeY;
+ double z = relativeZ;
+
+ // Entity linking
+ if(this.entity != null){
+ if(this.entity.isDead){
+ this.setExpired();
+ }else{
+ x += this.entity.posX;
+ y += this.entity.posY;
+ z += this.entity.posZ;
+ }
+ }
+
+ // Spin
+ if(radius > 0){
+ angle += speed;
+ // If the particle has spin, x/z relative position is used as centre and coords are changed each tick
+ x += radius * -MathHelper.cos(angle);
+ z += radius * MathHelper.sin(angle);
+ }
+
+ this.setPosition(x, y, z);
+
+ this.relativeX += relativeMotionX;
+ this.relativeY += relativeMotionY;
+ this.relativeZ += relativeMotionZ;
+ }
+
+ // Colour fading
+ float ageFraction = (float)this.particleAge / (float)this.particleMaxAge;
+ // No longer uses setRBGColorF because that method now also sets the initial values
+ this.particleRed = this.initialRed + (this.fadeRed - this.initialRed) * ageFraction;
+ this.particleGreen = this.initialGreen + (this.fadeGreen - this.initialGreen) * ageFraction;
+ this.particleBlue = this.initialBlue + (this.fadeBlue - this.initialBlue) * ageFraction;
+
+ // Animation
+ if(sprites.length > 1){
+ // Math.min included for safety so the index cannot possibly exceed the length - 1 an cause an AIOOBE
+ // (which would probably otherwise happen if particleAge == particleMaxAge)
+ this.setParticleTexture(sprites[Math.min((int)(ageFraction * sprites.length), sprites.length - 1)]);
+ }
+
+ // Collision spreading
+ if(canCollide){
+
+ if(this.motionX == 0 && this.prevVelX != 0){ // If the particle just collided in x
+ // Reduce lateral velocity so the added spread speed actually has an effect
+ this.motionY *= IMPACT_FRICTION;
+ this.motionZ *= IMPACT_FRICTION;
+ // Add random velocity in y and z proportional to the impact velocity
+ this.motionY += (rand.nextDouble()*2 - 1) * this.prevVelX * SPREAD_FACTOR;
+ this.motionZ += (rand.nextDouble()*2 - 1) * this.prevVelX * SPREAD_FACTOR;
+ }
+
+ if(this.motionY == 0 && this.prevVelY != 0){ // If the particle just collided in y
+ // Reduce lateral velocity so the added spread speed actually has an effect
+ this.motionX *= IMPACT_FRICTION;
+ this.motionZ *= IMPACT_FRICTION;
+ // Add random velocity in x and z proportional to the impact velocity
+ this.motionX += (rand.nextDouble()*2 - 1) * this.prevVelY * SPREAD_FACTOR;
+ this.motionZ += (rand.nextDouble()*2 - 1) * this.prevVelY * SPREAD_FACTOR;
+ }
+
+ if(this.motionZ == 0 && this.prevVelZ != 0){ // If the particle just collided in z
+ // Reduce lateral velocity so the added spread speed actually has an effect
+ this.motionX *= IMPACT_FRICTION;
+ this.motionY *= IMPACT_FRICTION;
+ // Add random velocity in x and y proportional to the impact velocity
+ this.motionX += (rand.nextDouble()*2 - 1) * this.prevVelZ * SPREAD_FACTOR;
+ this.motionY += (rand.nextDouble()*2 - 1) * this.prevVelZ * SPREAD_FACTOR;
+ }
+
+ double searchRadius = 20;
+
+ List nearbyEntities = WizardryUtilities.getEntitiesWithinRadius(searchRadius, this.posX,
+ this.posY, this.posZ, world, Entity.class);
+
+ nearbyEntities.removeIf(e -> !(e instanceof ICustomHitbox && ((ICustomHitbox)e).contains(new Vec3d(this.posX, this.posY, this.posZ))));
+
+ if(nearbyEntities.size() > 0) this.setExpired();
+
+ }
+
+ this.prevVelX = motionX;
+ this.prevVelY = motionY;
+ this.prevVelZ = motionZ;
+ }
+
+ // Overridden and copied to fix the collision behaviour
+ @Override
+ public void move(double x, double y, double z){
+
+ double origY = y;
+ double origX = x;
+ double origZ = z;
+
+ if(this.canCollide){
+
+ List list = this.world.getCollisionBoxes(null, this.getBoundingBox().expand(x, y, z));
+
+ for(AxisAlignedBB axisalignedbb : list){
+ y = axisalignedbb.calculateYOffset(this.getBoundingBox(), y);
+ }
+
+ this.setBoundingBox(this.getBoundingBox().offset(0.0D, y, 0.0D));
+
+ for(AxisAlignedBB axisalignedbb1 : list){
+ x = axisalignedbb1.calculateXOffset(this.getBoundingBox(), x);
+ }
+
+ this.setBoundingBox(this.getBoundingBox().offset(x, 0.0D, 0.0D));
+
+ for(AxisAlignedBB axisalignedbb2 : list){
+ z = axisalignedbb2.calculateZOffset(this.getBoundingBox(), z);
+ }
+
+ this.setBoundingBox(this.getBoundingBox().offset(0.0D, 0.0D, z));
+
+ }else{
+ this.setBoundingBox(this.getBoundingBox().offset(x, y, z));
+ }
+
+ this.resetPositionToBB();
+ this.onGround = origY != y && origY < 0.0D;
+
+ if(origX != x) this.motionX = 0.0D;
+ if(origY != y) this.motionY = 0.0D; // Why doesn't Particle do this for y?
+ if(origZ != z) this.motionZ = 0.0D;
+ }
+
+
+ // =============================================== Helper Methods ===============================================
+
+ /** Static helper method that generates an array of n ResourceLocations using the particle file naming convention,
+ * which is the given stem plus an underscore plus the integer index. */
+ public static ResourceLocation[] generateTextures(String stem, int n){
+
+ ResourceLocation[] textures = new ResourceLocation[n];
+
+ for(int i=0; ithis method may only be called from the client side, probably a client proxy.
+ * @param name The {@link ResourceLocation} to use for the particle. This effectively replaces the particle type
+ * enum from previous versions. Keep a reference to this somewhere in common code for use later.
+ * @param factory A {@link IWizardryParticleFactory} that produces your particle. A constructor reference is usually
+ * sufficient.
+ */
+ public static void registerParticle(ResourceLocation name, IWizardryParticleFactory factory){
+ ClientProxy.addParticleFactory(name, factory);
+ }
+
+ /** Simple particle factory interface which takes a world and a position and returns a particle. Used (via method
+ * references) in the client proxy to link particle enum types to actual particle classes. */
+ @SideOnly(Side.CLIENT)
+ @FunctionalInterface
+ public interface IWizardryParticleFactory {
+ ParticleWizardry createParticle(World world, double x, double y, double z);
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/LayerFrost.java b/src/main/java/electroblob/wizardry/client/renderer/LayerFrost.java
new file mode 100644
index 00000000..ae8536dd
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/LayerFrost.java
@@ -0,0 +1,153 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.block.BlockStatue;
+import electroblob.wizardry.registry.WizardryPotions;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.model.ModelBase;
+import net.minecraft.client.model.ModelBiped;
+import net.minecraft.client.model.ModelPlayer;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.GlStateManager.DestFactor;
+import net.minecraft.client.renderer.GlStateManager.SourceFactor;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.entity.Render;
+import net.minecraft.client.renderer.entity.RenderLivingBase;
+import net.minecraft.client.renderer.entity.RenderPlayer;
+import net.minecraft.client.renderer.entity.layers.LayerRenderer;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
+
+/**
+ * Layer used to render the frost texture on a creature with the frostbite effect. Handles dynamic tiling of the texture.
+ *
+ * @author Electroblob
+ * @since Wizardry 1.2
+ */
+public class LayerFrost implements LayerRenderer {
+
+ protected ModelBase model;
+ private final RenderLivingBase> renderer;
+
+ private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/entity/frost_overlay.png");
+
+ public static void initialiseLayers(){
+
+ for(Render extends Entity> renderer : Minecraft.getMinecraft().getRenderManager().entityRenderMap.values()){
+ // Because the zombie classes are now split properly, their renderers play nicely like everything else.
+ if(renderer instanceof RenderLivingBase){
+ // Adds a frost layer to all the living entity renderers in the game. Whether it is actually rendered
+ // is decided in doRenderLayer below on a per-entity basis.
+ ((RenderLivingBase>)renderer).addLayer(new LayerFrost((RenderLivingBase>)renderer));
+ }
+ }
+
+ for(RenderPlayer renderer : Minecraft.getMinecraft().getRenderManager().getSkinMap().values()){
+ renderer.addLayer(new LayerFrost(renderer));
+ }
+ }
+
+ public LayerFrost(RenderLivingBase> renderer){
+ this.renderer = renderer;
+ this.model = renderer.getMainModel();
+ }
+
+ @Override
+ public void doRenderLayer(EntityLivingBase entity, float limbSwing, float limbSwingAmount, float partialTicks,
+ float ageInTicks, float netHeadYaw, float headPitch, float scale){
+
+ if(entity.isPotionActive(WizardryPotions.frost) || entity.getEntityData().getBoolean(BlockStatue.FROZEN_NBT_KEY)){
+
+ GlStateManager.enableLighting();
+ int i = this.getBlockBrightnessForEntity(entity, partialTicks);
+
+ int j = i % 65536;
+ int k = i / 65536;
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, (float)j / 1.0F, (float)k / 1.0F);
+
+ // Frost texture
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA);
+ this.renderer.bindTexture(texture);
+ this.renderEntityModel(entity, limbSwing, limbSwingAmount, partialTicks, ageInTicks, netHeadYaw,
+ headPitch, scale);
+ GlStateManager.disableBlend();
+
+ }
+ }
+
+ private int getBlockBrightnessForEntity(Entity entity, float partialTicks){
+
+ BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(MathHelper.floor(entity.posX), 0,
+ MathHelper.floor(entity.posZ));
+
+ if(entity.world.isBlockLoaded(pos)){
+ pos.setY(MathHelper.floor(entity.posY + (double)entity.getEyeHeight()));
+ return entity.world.getCombinedLight(pos, 0);
+ }else{
+ return 0;
+ }
+ }
+
+ private void renderEntityModel(EntityLivingBase entity, float limbSwing, float limbSwingAmount, float partialTicks,
+ float ageInTicks, float netHeadYaw, float headPitch, float scale){
+
+ GlStateManager.pushMatrix();
+ // Enables tiling (Also used for guardian beam, beacon beam and ender crystal beam)
+ GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
+ GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
+
+ GlStateManager.depthMask(true); // Some entities set depth mask to false (i.e. no sorting of faces by depth)
+ // In particular, LayerSpiderEyes sets it to false when the spider is invisible, for some reason.
+
+ // Changes the scale at which the texture is applied to the model. See LayerCreeper for a similar example,
+ // but with translation instead of scaling.
+ // NOTE: You can do all sorts of fun stuff with this, just by applying transformations in the 2D texture space.
+ GlStateManager.matrixMode(GL11.GL_TEXTURE);
+ GlStateManager.loadIdentity();
+ double scaleX = 1, scaleY = 1;
+ // It's more logical to use the model's texture size, but some classes don't bother setting it properly
+ // (e.g. ModelVillager), so to get the correct dimensions I'm getting them from the first box instead.
+ if(model.boxList != null && model.boxList.get(0) != null){
+ scaleX = (double)model.boxList.get(0).textureWidth / 16d;
+ scaleY = (double)model.boxList.get(0).textureHeight / 16d;
+ }else{ // Fallback to model fields; should never be needed
+ scaleX = (double)model.textureWidth / 16d;
+ scaleY = (double)model.textureHeight / 16d;
+ }
+ GlStateManager.scale(scaleX, scaleY, 1);
+ GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+
+ // Hides the hat layer for bipeds
+ if(this.model instanceof ModelBiped) ((ModelBiped)this.model).bipedHeadwear.isHidden = true;
+
+ if(this.model instanceof ModelPlayer){
+ ((ModelPlayer)this.model).bipedBodyWear.isHidden = true;
+ ((ModelPlayer)this.model).bipedLeftArmwear.isHidden = true;
+ ((ModelPlayer)this.model).bipedRightArmwear.isHidden = true;
+ ((ModelPlayer)this.model).bipedLeftLegwear.isHidden = true;
+ ((ModelPlayer)this.model).bipedRightLegwear.isHidden = true;
+ }
+
+ this.model.setLivingAnimations(entity, limbSwing, limbSwingAmount, partialTicks);
+ this.model.render(entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw, headPitch, scale);
+
+ if(this.model instanceof ModelBiped) ((ModelBiped)this.model).bipedHeadwear.isHidden = false;
+
+ // Undoes the texture scaling
+ GlStateManager.matrixMode(GL11.GL_TEXTURE);
+ GlStateManager.loadIdentity();
+ GlStateManager.matrixMode(GL11.GL_MODELVIEW);
+
+ GlStateManager.popMatrix();
+ }
+
+ @Override
+ public boolean shouldCombineTextures(){
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/renderer/LayerStone.java b/src/main/java/electroblob/wizardry/client/renderer/LayerStone.java
index 82f5950f..ed1d5669 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/LayerStone.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/LayerStone.java
@@ -1,11 +1,7 @@
package electroblob.wizardry.client.renderer;
-import java.util.Map.Entry;
-
-import org.lwjgl.opengl.GL11;
-
+import electroblob.wizardry.block.BlockStatue;
import electroblob.wizardry.client.ClientProxy;
-import electroblob.wizardry.spell.Petrify;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.ModelBase;
import net.minecraft.client.model.ModelBiped;
@@ -21,6 +17,7 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
/**
* Layer used to render the stone texture on a petrified creature. Handles dynamic tiling of the stone texture.
@@ -36,16 +33,14 @@ public class LayerStone implements LayerRenderer {
private static final ResourceLocation texture = new ResourceLocation("textures/blocks/stone.png");
public static void initialiseLayers(){
- for(Entry, Render extends Entity>> entry : Minecraft.getMinecraft()
- .getRenderManager().entityRenderMap.entrySet()){
+
+ for(Render extends Entity> renderer : Minecraft.getMinecraft().getRenderManager().entityRenderMap.values()){
// Because the zombie classes are now split properly, their renderers play nicely like everything else.
- if(entry.getValue() instanceof RenderLivingBase){
+ if(renderer instanceof RenderLivingBase){
// Adds a stone layer to all the living entity renderers in the game. Whether it is actually rendered
// is decided in doRenderLayer below on a per-entity basis.
- ((RenderLivingBase>)entry.getValue()).addLayer(new LayerStone((RenderLivingBase>)entry.getValue()));
+ ((RenderLivingBase>)renderer).addLayer(new LayerStone((RenderLivingBase>)renderer));
}
- // NOTE: May have to do some special stuff for players if they are to be added; see
- // Minecraft.getMinecraft().getRenderManager().getSkinMap()
}
}
@@ -53,12 +48,15 @@ public class LayerStone implements LayerRenderer {
this.renderer = renderer;
this.model = renderer.getMainModel();
}
+
+ // FIXME: Does not work with zombie pigmen, I have no idea why.
+ // I believe the issue is with the TESR actually, since LayerFrost works fine
@Override
public void doRenderLayer(EntityLivingBase entity, float limbSwing, float limbSwingAmount, float partialTicks,
float ageInTicks, float netHeadYaw, float headPitch, float scale){
- if(entity.getEntityData().getBoolean(Petrify.NBT_KEY)){
+ if(entity.getEntityData().getBoolean(BlockStatue.PETRIFIED_NBT_KEY)){
GlStateManager.enableLighting();
int i = this.getBlockBrightnessForEntity(entity, partialTicks);
@@ -106,7 +104,6 @@ public class LayerStone implements LayerRenderer {
GlStateManager.pushMatrix();
// Enables tiling (Also used for guardian beam, beacon beam and ender crystal beam)
- // TODO: Backport this improvement
GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
GlStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
diff --git a/src/main/java/electroblob/wizardry/client/renderer/LayerStrayMinionClothing.java b/src/main/java/electroblob/wizardry/client/renderer/LayerStrayMinionClothing.java
new file mode 100644
index 00000000..00496ecb
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/LayerStrayMinionClothing.java
@@ -0,0 +1,33 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.entity.living.EntityStrayMinion;
+import net.minecraft.client.model.ModelSkeleton;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.entity.RenderLivingBase;
+import net.minecraft.client.renderer.entity.layers.LayerRenderer;
+import net.minecraft.util.ResourceLocation;
+
+/** Had to copy this entire class just because of one unnecessarily specific type parameter. The type parameter has been
+ * changed to {@code EntityStrayMinion} and parameter types updated accordingly. Everything else is identical. */
+public class LayerStrayMinionClothing implements LayerRenderer {
+
+ private static final ResourceLocation STRAY_CLOTHES_TEXTURES = new ResourceLocation("textures/entity/skeleton/stray_overlay.png");
+ private final RenderLivingBase> renderer;
+ private final ModelSkeleton layerModel = new ModelSkeleton(0.25F, true);
+
+ public LayerStrayMinionClothing(RenderLivingBase> renderer){
+ this.renderer = renderer;
+ }
+
+ public void doRenderLayer(EntityStrayMinion entity, float limbSwing, float limbSwingAmount, float partialTicks, float ageInTicks, float netHeadYaw, float headPitch, float scale){
+ this.layerModel.setModelAttributes(this.renderer.getMainModel());
+ this.layerModel.setLivingAnimations(entity, limbSwing, limbSwingAmount, partialTicks);
+ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
+ this.renderer.bindTexture(STRAY_CLOTHES_TEXTURES);
+ this.layerModel.render(entity, limbSwing, limbSwingAmount, ageInTicks, netHeadYaw, headPitch, scale);
+ }
+
+ public boolean shouldCombineTextures(){
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderArc.java b/src/main/java/electroblob/wizardry/client/renderer/RenderArc.java
deleted file mode 100644
index 1e59d927..00000000
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderArc.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package electroblob.wizardry.client.renderer;
-
-import org.lwjgl.opengl.GL11;
-
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.entity.EntityArc;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.OpenGlHelper;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.entity.Render;
-import net.minecraft.client.renderer.entity.RenderManager;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.util.ResourceLocation;
-
-public class RenderArc extends Render {
-
- private static final ResourceLocation[] textures = new ResourceLocation[16];
-
- public RenderArc(RenderManager renderManager){
- super(renderManager);
- for(int i = 0; i < 16; i++){
- textures[i] = new ResourceLocation(Wizardry.MODID, "textures/entity/arc_" + i + ".png");
- }
- }
-
- @Override
- public void doRender(EntityArc arc, double d0, double d1, double d2, float fa, float fb){
- GlStateManager.pushMatrix();
- GlStateManager.translate((float)d0, (float)d1, (float)d2);
- GlStateManager.disableLighting();
- GlStateManager.enableBlend();
- GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); // This line fixes the weird
- // brightness bug.
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
-
- // System.out.println("Entity coords: " + entity.posX + ", " + entity.posY + ", " + entity.posZ);
- // System.out.println("doRender parameters: " + d0 + ", " + d1 + ", " + d2 + ", " + fa + ", " + fb);
-
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
-
- bindTexture(textures[arc.textureIndex]); // This MUST be after the tessellator declaration and the gl stuff
-
- /**
- * Note: A lot of the maths here works on similar triangles and the ratios between them, avoiding too much
- * pythagoras and eliminating the need for any trig. Ratios are used for the positioning of the arc endpoints.
- * Ratios are usually used swapping x and z because the triangles are rotated through 90 degrees.
- */
-
- double dx = -d0;
- double dy = -d1;
- double dz = -d2;
-
- if(arc.x1 != 0){
- dx = arc.x1 - arc.posX;// - d2/lengthOffsetRatio;
- dy = arc.y1 - arc.posY + 0.3;
- dz = arc.z1 - arc.posZ;// + d0/lengthOffsetRatio;
-
- // The distance from caster to target
- double arcLength = Math.sqrt(dz * dz + dx * dx);
-
- // The ratio between the length of the arc and the offset of the start point from the player's centre (which
- // is always 0.3).
- // double lengthOffsetRatio = arcLength/0.3;
-
- // EntityClientPlayerMP player = Minecraft.getMinecraft().player;
-
- // double xViewDist = player.posX - d0;
- // double yViewDist = player.posY + player.eyeHeight - d1;
- // double zViewDist = player.posZ - d2;
-
- // double xzViewDist = Math.sqrt(xViewDist * xViewDist + zViewDist * zViewDist);
-
- // The angle above the horizontal that this particular player is viewing the arc from
- // double viewAngle = Math.atan(yViewDist/xzViewDist);
-
- // Half the width of the arc
- double arcWidth = 0.3d;
-
- // Right hand side of vertical plane
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
- // Target end
- buffer.pos(0, -0.5, 0).tex(1, 1).endVertex();
- buffer.pos(0, 0.5, 0).tex(1, 0).endVertex();
- // Caster end
- buffer.pos(dx, dy, dz).tex(0, 0).endVertex();
- buffer.pos(dx, dy - 1, dz).tex(0, 1).endVertex();
- tessellator.draw();
-
- // Left
- buffer.begin(GL11.GL_QUADS, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_TEX);
- // Target end
- buffer.pos(0, -0.5, 0).tex(1, 1).endVertex();
- // Caster end
- buffer.pos(dx, dy - 1, dz).tex(0, 1).endVertex();
- buffer.pos(dx, dy, dz).tex(0, 0).endVertex();
- // Target end
- buffer.pos(0, 0.5, 0).tex(1, 0).endVertex();
- tessellator.draw();
-
- // Bottom of horizontal plane
- buffer.begin(GL11.GL_QUADS, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_TEX);
- buffer.pos((arcWidth / arcLength) * dz, 0, (-arcWidth / arcLength) * dx).tex(1, 1).endVertex();
- buffer.pos(dx + (arcWidth / arcLength) * dz, dy - 0.5, dz - (arcWidth / arcLength) * dx).tex(0, 1)
- .endVertex();
- buffer.pos(dx - (arcWidth / arcLength) * dz, dy - 0.5, dz + (arcWidth / arcLength) * dx).tex(0, 0)
- .endVertex();
- buffer.pos((-arcWidth / arcLength) * dz, 0, (arcWidth / arcLength) * dx).tex(1, 0).endVertex();
- tessellator.draw();
-
- // Top
- buffer.begin(GL11.GL_QUADS, net.minecraft.client.renderer.vertex.DefaultVertexFormats.POSITION_TEX);
- buffer.pos((arcWidth / arcLength) * dz, 0, (-arcWidth / arcLength) * dx).tex(1, 1).endVertex();
- buffer.pos((-arcWidth / arcLength) * dz, 0, (arcWidth / arcLength) * dx).tex(1, 0).endVertex();
- buffer.pos(dx - (arcWidth / arcLength) * dz, dy - 0.5, dz + (arcWidth / arcLength) * dx).tex(0, 0)
- .endVertex();
- buffer.pos(dx + (arcWidth / arcLength) * dz, dy - 0.5, dz - (arcWidth / arcLength) * dx).tex(0, 1)
- .endVertex();
- tessellator.draw();
- }
-
- GlStateManager.enableLighting();
- GlStateManager.disableBlend();
- GlStateManager.popMatrix();
- }
-
- @Override
- protected ResourceLocation getEntityTexture(EntityArc entity){
- return textures[entity.textureIndex];
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderArcaneLock.java b/src/main/java/electroblob/wizardry/client/renderer/RenderArcaneLock.java
new file mode 100644
index 00000000..cb2058f8
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderArcaneLock.java
@@ -0,0 +1,89 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.spell.ArcaneLock;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.RenderWorldLastEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import org.lwjgl.opengl.GL11;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class RenderArcaneLock {
+
+ private static final ResourceLocation[] textures = new ResourceLocation[8];
+
+ static {
+ for(int i=0; i {
@@ -30,21 +30,21 @@ public class RenderArcaneWorkbench extends TileEntitySpecialRenderer {
@@ -81,21 +77,21 @@ public class RenderBlackHole extends Render {
int sliceAngle = 20 + a;
- double x1 = scale * Math.sin((blackhole.ticksExisted + 40 * j) * (Math.PI / 180));
- // double y1 = 0.7*Math.cos((blackhole.timer - 40*j)*(Math.PI/180))*j/10;
- double z1 = scale * Math.cos((blackhole.ticksExisted + 40 * j) * (Math.PI / 180));
+ double x1 = scale * MathHelper.sin((blackhole.ticksExisted + 40 * j) * ((float)Math.PI / 180f));
+ // double y1 = 0.7*MathHelper.cos((blackhole.timer - 40*j)*(Math.PI/180))*j/10;
+ double z1 = scale * MathHelper.cos((blackhole.ticksExisted + 40 * j) * ((float)Math.PI / 180));
- double x2 = scale * Math.sin((blackhole.ticksExisted + 40 * j - sliceAngle) * (Math.PI / 180));
- // double y2 = 0.7*Math.sin((blackhole.timer - 40*j)*(Math.PI/180))*j/10;
- double z2 = scale * Math.cos((blackhole.ticksExisted + 40 * j - sliceAngle) * (Math.PI / 180));
+ double x2 = scale * MathHelper.sin((blackhole.ticksExisted + 40 * j - sliceAngle) * ((float)Math.PI / 180));
+ // double y2 = 0.7*MathHelper.sin((blackhole.timer - 40*j)*(Math.PI/180))*j/10;
+ double z2 = scale * MathHelper.cos((blackhole.ticksExisted + 40 * j - sliceAngle) * ((float)Math.PI / 180));
- double absoluteX = x1 * Math.cos(31 * b);
- double absoluteY = z1 * Math.sin(31 * a) + x1 * Math.cos(31 * a) * Math.sin(31 * b);
- double absoluteZ = z1 * Math.cos(31 * a);
+ double absoluteX = x1 * MathHelper.cos(31 * b);
+ double absoluteY = z1 * MathHelper.sin(31 * a) + x1 * MathHelper.cos(31 * a) * MathHelper.sin(31 * b);
+ double absoluteZ = z1 * MathHelper.cos(31 * a);
- double absoluteX2 = x2 * Math.cos(31 * b);
- double absoluteY2 = z2 * Math.sin(31 * a) + x2 * Math.cos(31 * a) * Math.sin(31 * b);
- double absoluteZ2 = z2 * Math.cos(31 * a);
+ double absoluteX2 = x2 * MathHelper.cos(31 * b);
+ double absoluteY2 = z2 * MathHelper.sin(31 * a) + x2 * MathHelper.cos(31 * a) * MathHelper.sin(31 * b);
+ double absoluteZ2 = z2 * MathHelper.cos(31 * a);
/* buffer.begin(0, DefaultVertexFormats.POSITION_TEX);
*
* tessellator.setColorOpaque(255, 255, 255); GL11.glPointSize(5);
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderBubble.java b/src/main/java/electroblob/wizardry/client/renderer/RenderBubble.java
index fa0cb695..2694f1e8 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderBubble.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderBubble.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.entity.construct.EntityBubble;
import electroblob.wizardry.util.WizardryUtilities;
@@ -14,6 +12,7 @@ import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
+import org.lwjgl.opengl.GL11;
public class RenderBubble extends Render {
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderContainmentField.java b/src/main/java/electroblob/wizardry/client/renderer/RenderContainmentField.java
new file mode 100644
index 00000000..b52b83b0
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderContainmentField.java
@@ -0,0 +1,268 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.potion.PotionContainment;
+import electroblob.wizardry.registry.WizardryPotions;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.nbt.NBTUtil;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+import net.minecraftforge.client.event.RenderWorldLastEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import org.lwjgl.opengl.GL11;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class RenderContainmentField {
+
+ private static final ResourceLocation[] textures = new ResourceLocation[8];
+
+ private static final float ANIMATION_SPEED = 0.004f;
+ private static final float FADE_DISTANCE_SQUARED = 15;
+
+ static {
+ for(int i=0; i {
@@ -44,7 +43,7 @@ public class RenderDecay extends Render {
GlStateManager.rotate(-90, 1, 0, 0);
- float scale = 2 * Math.min(1, (float)(EntityDecay.LIFETIME - entity.ticksExisted) / 50f);
+ float scale = 2 * Math.min(1, (float)(entity.lifetime - entity.ticksExisted) / 50f);
GlStateManager.scale(scale, scale, scale);
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderDecoy.java b/src/main/java/electroblob/wizardry/client/renderer/RenderDecoy.java
index 4c421adc..10acfa56 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderDecoy.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderDecoy.java
@@ -1,8 +1,5 @@
package electroblob.wizardry.client.renderer;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.entity.living.EntityDecoy;
import net.minecraft.client.model.ModelBiped;
@@ -12,10 +9,11 @@ import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.Entity;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.relauncher.ReflectionHelper;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+//@SideOnly(Side.CLIENT)
public class RenderDecoy extends RenderBiped {
private static final ResourceLocation steveTextures = new ResourceLocation("textures/entity/steve.png");
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderEvilWizard.java b/src/main/java/electroblob/wizardry/client/renderer/RenderEvilWizard.java
index 2828261f..34299e1c 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderEvilWizard.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderEvilWizard.java
@@ -7,10 +7,8 @@ import net.minecraft.client.renderer.entity.RenderBiped;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.layers.LayerBipedArmor;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderEvilWizard extends RenderBiped {
static final ResourceLocation[] textures = new ResourceLocation[6];
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderFireRing.java b/src/main/java/electroblob/wizardry/client/renderer/RenderFireRing.java
index 8597bacf..9e01b89b 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderFireRing.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderFireRing.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.entity.construct.EntityFireRing;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
@@ -15,6 +13,7 @@ import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.init.Blocks;
import net.minecraft.util.ResourceLocation;
+import org.lwjgl.opengl.GL11;
public class RenderFireRing extends Render {
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderForceArrow.java b/src/main/java/electroblob/wizardry/client/renderer/RenderForceArrow.java
index 5878aeeb..7f908e71 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderForceArrow.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderForceArrow.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.entity.projectile.EntityForceArrow;
import net.minecraft.client.renderer.BufferBuilder;
@@ -13,10 +11,9 @@ import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.MathHelper;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import org.lwjgl.opengl.GL11;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderForceArrow extends Render {
private static final ResourceLocation arrowTextures = new ResourceLocation(Wizardry.MODID,
@@ -73,13 +70,23 @@ public class RenderForceArrow extends Render {
GlStateManager.scale(scale, scale, scale);
GlStateManager.translate(-4.0F, 0.0F, 0.0F);
+ // Front
buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
buffer.pos(-5, 3.5, -3.5).tex((double)u5, (double)v5).endVertex();
buffer.pos(-5, 3.5, 3.5).tex((double)u6, (double)v5).endVertex();
buffer.pos(-5, -3.5, 3.5).tex((double)u6, (double)v6).endVertex();
- buffer.pos(-5, -3.5, -3.5).tex((double)u5, (double)v6);
+ buffer.pos(-5, -3.5, -3.5).tex((double)u5, (double)v6).endVertex();
+ tessellator.draw();
+
+ // Back
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+ buffer.pos(-5, -3.5, -3.5).tex((double)u5, (double)v6).endVertex();
+ buffer.pos(-5, -3.5, 3.5).tex((double)u6, (double)v6).endVertex();
+ buffer.pos(-5, 3.5, 3.5).tex((double)u6, (double)v5).endVertex();
+ buffer.pos(-5, 3.5, -3.5).tex((double)u5, (double)v5).endVertex();
tessellator.draw();
+ // Rings
for(int i = 0; i < 5; i++){
GlStateManager.color(1, 1, 1, 1 - i * 0.2f);
double j = i + ((double)arrow.ticksExisted % 3) / 3;
@@ -102,6 +109,7 @@ public class RenderForceArrow extends Render {
GlStateManager.color(1, 1, 1, 1);
+ // Sides
for(int i = 0; i < 4; ++i){
GlStateManager.rotate(90.0F, 1.0F, 0.0F, 0.0F);
GL11.glNormal3f(0.0F, 0.0F, scale);
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderForcefield.java b/src/main/java/electroblob/wizardry/client/renderer/RenderForcefield.java
new file mode 100644
index 00000000..660c1e1a
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderForcefield.java
@@ -0,0 +1,124 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.entity.construct.EntityForcefield;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.entity.Render;
+import net.minecraft.client.renderer.entity.RenderManager;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
+
+public class RenderForcefield extends Render {
+
+ private static final float EXPANSION_TIME = 3;
+
+ public RenderForcefield(RenderManager renderManager){
+ super(renderManager);
+ }
+
+ @Override
+ public void doRender(EntityForcefield entity, double x, double y, double z, float yaw, float partialTicks){
+
+ // For now we're just using a UV sphere
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.disableLighting();
+ GlStateManager.enableBlend();
+ GlStateManager.disableTexture2D();
+ GlStateManager.blendFunc(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA);
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ GlStateManager.translate(x, y, z);
+
+ float latStep = (float)Math.PI/20;
+ float longStep = (float)Math.PI/20;
+
+ float pulse = MathHelper.sin((entity.ticksExisted + partialTicks)/10f);
+
+ float r = 0.35f, g = 0.55f + 0.05f * pulse, b = 1;
+
+ float radius = entity.getRadius();
+ float a = 0.5f;
+
+ if(entity.ticksExisted > entity.lifetime - EXPANSION_TIME){
+ radius *= 1 + 0.2f * (entity.ticksExisted + partialTicks - (entity.lifetime - EXPANSION_TIME))/EXPANSION_TIME;
+ a *= Math.max(0, 1 - (entity.ticksExisted + partialTicks - (entity.lifetime - EXPANSION_TIME))/EXPANSION_TIME);
+ }else if(entity.ticksExisted < EXPANSION_TIME){
+ radius *= 1 - (EXPANSION_TIME - entity.ticksExisted - partialTicks)/EXPANSION_TIME;
+ a *= 1 - (EXPANSION_TIME - entity.ticksExisted - partialTicks)/EXPANSION_TIME;
+ }
+
+ // Draw the inside first
+ drawSphere(radius - 0.1f - 0.025f * pulse, latStep, longStep, true, r, g, b, a);
+ drawSphere(radius - 0.1f - 0.025f * pulse, latStep, longStep, false, 1, 1, 1, a);
+ drawSphere(radius, latStep, longStep, false, r, g, b, 0.7f * a);
+
+ GlStateManager.enableTexture2D();
+ GlStateManager.enableLighting();
+ GlStateManager.disableBlend();
+
+ GlStateManager.popMatrix();
+ }
+
+ @Override
+ protected ResourceLocation getEntityTexture(EntityForcefield entity){
+ return null;
+ }
+
+ /**
+ * Draws a sphere (using lat/long triangles) with the given parameters.
+ * @param radius The radius of the sphere.
+ * @param latStep The latitude step; smaller is smoother but increases performance cost.
+ * @param longStep The longitude step; smaller is smoother but increases performance cost.
+ * @param inside Whether to draw the outside or the inside of the sphere.
+ * @param r The red component of the sphere colour.
+ * @param g The green component of the sphere colour.
+ * @param b The blue component of the sphere colour.
+ * @param a The alpha component of the sphere colour.
+ */
+ private static void drawSphere(float radius, float latStep, float longStep, boolean inside, float r, float g, float b, float a){
+
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_COLOR);
+
+ boolean goingUp = inside;
+
+ buffer.pos(0, goingUp ? -radius : radius, 0).color(r, g, b, a).endVertex(); // Start at the north pole
+
+ for(float longitude = -(float)Math.PI; longitude <= (float)Math.PI; longitude += longStep){
+
+ // Leave the poles out since they only have a single point per stack instead of two
+ for(float theta = (float)Math.PI/2 - latStep; theta >= -(float)Math.PI/2 + latStep; theta -= latStep){
+
+ float latitude = goingUp ? -theta : theta;
+
+ float hRadius = radius * MathHelper.cos(latitude);
+ float vy = radius * MathHelper.sin(latitude);
+ float vx = hRadius * MathHelper.sin(longitude);
+ float vz = hRadius * MathHelper.cos(longitude);
+
+ buffer.pos(vx, vy, vz).color(r, g, b, a).endVertex();
+
+ vx = hRadius * MathHelper.sin(longitude + longStep);
+ vz = hRadius * MathHelper.cos(longitude + longStep);
+
+ buffer.pos(vx, vy, vz).color(r, g, b, a).endVertex();
+ }
+
+ // The next pole
+ buffer.pos(0, goingUp ? radius : -radius, 0).color(r, g, b, a).endVertex();
+
+ goingUp = !goingUp;
+ }
+
+ tessellator.draw();
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderHammer.java b/src/main/java/electroblob/wizardry/client/renderer/RenderHammer.java
index 8f448603..3962e04b 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderHammer.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderHammer.java
@@ -19,11 +19,13 @@ public class RenderHammer extends Render {
}
@Override
- public void doRender(EntityHammer entity, double x, double y, double z, float f, float f1){
+ public void doRender(EntityHammer entity, double x, double y, double z, float yaw, float partialTicks){
GlStateManager.pushMatrix();
GlStateManager.translate(x, y + 1.5, z);
GlStateManager.rotate(180, 0F, 0F, 1F);
+ GlStateManager.rotate(yaw, 0, 1, 0);
+ GlStateManager.rotate(entity.prevRotationPitch + (entity.rotationPitch - entity.prevRotationPitch) * partialTicks, 0, 0, 1);
this.bindTexture(texture);
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderIceGiant.java b/src/main/java/electroblob/wizardry/client/renderer/RenderIceGiant.java
index b78c6888..e20f22bf 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderIceGiant.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderIceGiant.java
@@ -7,10 +7,8 @@ import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.entity.RenderLiving;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderIceGiant extends RenderLiving {
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID,
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderIceSpike.java b/src/main/java/electroblob/wizardry/client/renderer/RenderIceSpike.java
index 961b87f5..3a88d93b 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderIceSpike.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderIceSpike.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.entity.construct.EntityIceSpike;
import net.minecraft.client.renderer.BufferBuilder;
@@ -12,6 +10,7 @@ import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
+import org.lwjgl.opengl.GL11;
public class RenderIceSpike extends Render {
@@ -23,13 +22,14 @@ public class RenderIceSpike extends Render {
}
@Override
- public void doRender(EntityIceSpike entity, double x, double y, double z, float fa, float partialTickTime){
+ public void doRender(EntityIceSpike entity, double x, double y, double z, float yaw, float partialTickTime){
GlStateManager.pushMatrix();
GlStateManager.translate((float)x, (float)y, (float)z);
- // Apparently, disabling lighting... doesn't disable lighting. Or at least, you can still set the brightness
- // with setLightmapTextureCoords.
+ GlStateManager.rotate(entity.rotationYaw - 90.0F, 0.0F, 1.0F, 0.0F);
+ GlStateManager.rotate(entity.rotationPitch - 90, 0.0F, 0.0F, 1.0F);
+
GlStateManager.disableLighting();
int j = entity.getBrightnessForRender();
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderLightningDisc.java b/src/main/java/electroblob/wizardry/client/renderer/RenderLightningDisc.java
index 77af69d3..96fe6110 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderLightningDisc.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderLightningDisc.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.entity.projectile.EntityLightningDisc;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
@@ -11,6 +9,7 @@ import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
+import org.lwjgl.opengl.GL11;
public class RenderLightningDisc extends Render {
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderLightningPulse.java b/src/main/java/electroblob/wizardry/client/renderer/RenderLightningPulse.java
deleted file mode 100644
index 65f2bc1d..00000000
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderLightningPulse.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package electroblob.wizardry.client.renderer;
-
-import org.lwjgl.opengl.GL11;
-
-import electroblob.wizardry.Wizardry;
-import electroblob.wizardry.entity.construct.EntityLightningPulse;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.OpenGlHelper;
-import net.minecraft.client.renderer.Tessellator;
-import net.minecraft.client.renderer.entity.Render;
-import net.minecraft.client.renderer.entity.RenderManager;
-import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
-import net.minecraft.util.ResourceLocation;
-
-public class RenderLightningPulse extends Render {
-
- private final ResourceLocation[] textures = new ResourceLocation[8];
- private float scale = 1.0f;
-
- public RenderLightningPulse(RenderManager renderManager, float scale){
- super(renderManager);
- for(int i = 0; i < textures.length; i++){
- textures[i] = new ResourceLocation(Wizardry.MODID, "textures/entity/lightning_pulse_" + i + ".png");
- }
- this.scale = scale;
- }
-
- @Override
- public void doRender(EntityLightningPulse entity, double par2, double par4, double par6, float par8, float par9){
-
- GlStateManager.pushMatrix();
- GlStateManager.enableBlend();
- GlStateManager.disableLighting();
- OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240, 240);
- GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
-
- float yOffset = 0;
-
- GlStateManager.translate((float)par2, (float)par4 + yOffset, (float)par6);
-
- this.bindTexture(textures[entity.ticksExisted]);
- float f6 = 1.0F;
- float f7 = 0.5F;
- float f8 = 0.5F;
-
- GlStateManager.rotate(-90, 1, 0, 0);
-
- GlStateManager.scale(scale, scale, scale);
-
- Tessellator tessellator = Tessellator.getInstance();
- BufferBuilder buffer = tessellator.getBuffer();
- buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
- buffer.pos((double)(0.0F - f7), (double)(0.0F - f8), 0.01).tex(0, 1).endVertex();
- buffer.pos((double)(f6 - f7), (double)(0.0F - f8), 0.01).tex(1, 1).endVertex();
- buffer.pos((double)(f6 - f7), (double)(1.0F - f8), 0.01).tex(1, 0).endVertex();
- buffer.pos((double)(0.0F - f7), (double)(1.0F - f8), 0.01).tex(0, 0).endVertex();
-
- tessellator.draw();
-
- GlStateManager.disableBlend();
- GlStateManager.enableLighting();
- GlStateManager.disableRescaleNormal();
- GlStateManager.popMatrix();
- }
-
- @Override
- protected ResourceLocation getEntityTexture(EntityLightningPulse entity){
- return null;
- }
-
-}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderMagicArrow.java b/src/main/java/electroblob/wizardry/client/renderer/RenderMagicArrow.java
index 9dfb0527..b0db9b36 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderMagicArrow.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderMagicArrow.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.entity.projectile.EntityMagicArrow;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
@@ -12,10 +10,9 @@ import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.MathHelper;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import org.lwjgl.opengl.GL11;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderMagicArrow extends Render {
private final ResourceLocation texture;
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderMagicLight.java b/src/main/java/electroblob/wizardry/client/renderer/RenderMagicLight.java
index 198e02f7..a0e2a2ce 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderMagicLight.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderMagicLight.java
@@ -1,18 +1,14 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.Wizardry;
import electroblob.wizardry.tileentity.TileEntityMagicLight;
import net.minecraft.client.Minecraft;
-import net.minecraft.client.renderer.BufferBuilder;
-import net.minecraft.client.renderer.GlStateManager;
-import net.minecraft.client.renderer.OpenGlHelper;
-import net.minecraft.client.renderer.RenderHelper;
-import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.*;
import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import org.lwjgl.opengl.GL11;
public class RenderMagicLight extends TileEntitySpecialRenderer {
@@ -40,10 +36,9 @@ public class RenderMagicLight extends TileEntitySpecialRenderer tileentity.maxTimer - 10 && tileentity.timer <= tileentity.maxTimer){
- GlStateManager.scale((float)(tileentity.maxTimer - tileentity.timer) / 10,
- (float)(tileentity.maxTimer - tileentity.timer) / 10,
- (float)(tileentity.maxTimer - tileentity.timer) / 10);
+ if(tileentity.maxTimer > 0 && tileentity.timer > tileentity.maxTimer - 10){
+ float scale = Math.max(0, (float)(tileentity.maxTimer - tileentity.timer) / 10);
+ GlStateManager.scale(scale, scale, scale);
}
// Renders the aura effect
@@ -105,13 +100,13 @@ public class RenderMagicLight extends TileEntitySpecialRenderer {
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID, "textures/entity/phoenix.png");
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderPossessingPlayer.java b/src/main/java/electroblob/wizardry/client/renderer/RenderPossessingPlayer.java
new file mode 100644
index 00000000..0820a178
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderPossessingPlayer.java
@@ -0,0 +1,41 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.spell.Possession;
+import net.minecraft.client.renderer.entity.Render;
+import net.minecraft.entity.EntityLiving;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraftforge.client.event.RenderPlayerEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class RenderPossessingPlayer {
+
+ @SubscribeEvent
+ @SuppressWarnings("unchecked") // Can't check it due to type erasure
+ public static void onRenderPlayerPreEvent(RenderPlayerEvent.Pre event){
+
+ EntityPlayer player = event.getEntityPlayer();
+ EntityLiving possessee = Possession.getPossessee(player);
+
+ if(possessee != null){
+ // I reject your renderer and substitute my own!
+ Render renderer = (Render)event.getRenderer().getRenderManager().entityRenderMap.get(possessee.getClass());
+ float yaw = player.prevRotationYaw + (player.rotationYaw - player.prevRotationYaw) * event.getPartialRenderTick();
+ possessee.swingProgress = player.swingProgress;
+ possessee.prevSwingProgress = player.prevSwingProgress;
+ possessee.renderYawOffset = player.renderYawOffset;
+ possessee.prevRenderYawOffset = player.prevRenderYawOffset;
+ possessee.rotationYawHead = player.rotationYawHead;
+ possessee.prevRotationYawHead = player.prevRotationYawHead;
+ possessee.rotationPitch = player.rotationPitch;
+ possessee.prevRotationPitch = player.prevRotationPitch;
+ possessee.limbSwing = player.limbSwing;
+ possessee.limbSwingAmount = player.limbSwingAmount;
+ possessee.prevLimbSwingAmount = player.prevLimbSwingAmount;
+ renderer.doRender(possessee, event.getX(), event.getY(), event.getZ(), yaw, event.getPartialRenderTick());
+ event.setCanceled(true);
+ }
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderProjectile.java b/src/main/java/electroblob/wizardry/client/renderer/RenderProjectile.java
index 387257b4..f5ece03f 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderProjectile.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderProjectile.java
@@ -1,7 +1,5 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.entity.projectile.EntityMagicProjectile;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
@@ -12,10 +10,9 @@ import net.minecraft.client.renderer.entity.Render;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import org.lwjgl.opengl.GL11;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderProjectile extends Render {
private float scale;
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderShadowWard.java b/src/main/java/electroblob/wizardry/client/renderer/RenderShadowWard.java
new file mode 100644
index 00000000..a0cf181f
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderShadowWard.java
@@ -0,0 +1,148 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.Vec3d;
+import net.minecraftforge.client.event.RenderPlayerEvent;
+import net.minecraftforge.client.event.RenderWorldLastEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import org.lwjgl.opengl.GL11;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class RenderShadowWard {
+
+ private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/entity/shadow_ward.png");
+
+ // First person
+ @SubscribeEvent
+ public static void onRenderWorldLastEvent(RenderWorldLastEvent event){
+ // Only render in first person
+ if(Minecraft.getMinecraft().gameSettings.thirdPersonView == 0){
+
+ EntityPlayer player = Minecraft.getMinecraft().player;
+
+ if(WizardryUtilities.isCasting(player, Spells.shadow_ward)){
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ //GlStateManager.shadeModel(GL11.GL_SMOOTH);
+ GlStateManager.disableLighting();
+ //GlStateManager.disableAlpha();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ GlStateManager.translate(0, 1.2, 0);
+ GlStateManager.rotate(-player.rotationYaw, 0, 1, 0);
+ GlStateManager.rotate(player.rotationPitch, 1, 0, 0);
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE);
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.translate(0, 0, 1.2);
+ GlStateManager.rotate(player.world.getTotalWorldTime() * -2, 0, 0, 1);
+ GlStateManager.scale(1.1, 1.1, 1.1);
+
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
+ buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
+ buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
+ buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
+
+ tessellator.draw();
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
+ buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
+ buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
+ buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
+
+ tessellator.draw();
+
+ GlStateManager.popMatrix();
+
+ //GlStateManager.shadeModel(GL11.GL_FLAT);
+ GlStateManager.enableLighting();
+ GlStateManager.disableBlend();
+
+ GlStateManager.popMatrix();
+
+ }
+ }
+ }
+
+ // Third person
+ @SubscribeEvent
+ public static void onRenderPlayerEvent(RenderPlayerEvent.Post event){
+
+ EntityPlayer player = event.getEntityPlayer();
+
+ if(WizardryUtilities.isCasting(player, Spells.shadow_ward)){
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ GlStateManager.disableLighting();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ Vec3d delta = player.getPositionEyes(event.getPartialRenderTick())
+ .subtract(Minecraft.getMinecraft().player.getPositionEyes(event.getPartialRenderTick()));
+ GlStateManager.translate(delta.x, delta.y, delta.z);
+
+ GlStateManager.rotate(180, 0, 1, 0);
+ GlStateManager.rotate(-player.renderYawOffset, 0, 1, 0);
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE);
+
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ GlStateManager.translate(0, 1.2, 0);
+ GlStateManager.rotate(player.world.getTotalWorldTime() * -2, 0, 0, 1);
+ GlStateManager.scale(1.1, 1.1, 1.1);
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
+ buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
+ buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
+ buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
+
+ tessellator.draw();
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(-0.5, 0.5, -0.5).tex(0, 0).endVertex();
+ buffer.pos(-0.5, -0.5, -0.5).tex(0, 1).endVertex();
+ buffer.pos(0.5, -0.5, -0.5).tex(1, 1).endVertex();
+ buffer.pos(0.5, 0.5, -0.5).tex(1, 0).endVertex();
+
+ tessellator.draw();
+
+ GlStateManager.enableLighting();
+ GlStateManager.disableBlend();
+
+ GlStateManager.popMatrix();
+
+ }
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderShield.java b/src/main/java/electroblob/wizardry/client/renderer/RenderShield.java
new file mode 100644
index 00000000..6211d4f4
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderShield.java
@@ -0,0 +1,168 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.spell.Shield;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.Vec3d;
+import net.minecraftforge.client.event.RenderPlayerEvent;
+import net.minecraftforge.client.event.RenderWorldLastEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import org.lwjgl.opengl.GL11;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class RenderShield {
+
+ private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/entity/shield.png");
+
+ // First person
+ @SubscribeEvent
+ public static void onRenderWorldLastEvent(RenderWorldLastEvent event){
+ // Only render in first person
+ if(Minecraft.getMinecraft().gameSettings.thirdPersonView == 0){
+
+ EntityPlayer player = Minecraft.getMinecraft().player;
+
+ if(WizardData.get(player).getVariable(Shield.SHIELD_KEY) != null && WizardryUtilities.isCasting(player, Spells.shield)){
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.disableCull();
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(GL11.GL_ONE, GL11.GL_SRC_ALPHA);
+ GlStateManager.shadeModel(GL11.GL_SMOOTH);
+ GlStateManager.disableLighting();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ GlStateManager.translate(0, 1.4, 0);
+
+ GlStateManager.rotate(-player.rotationYaw, 0, 1, 0);
+ GlStateManager.rotate(player.rotationPitch, 1, 0, 0);
+
+ GlStateManager.translate(0, 0, 0.8);
+
+ Tessellator tessellator = Tessellator.getInstance();
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE);
+
+ render(tessellator);
+
+ GlStateManager.enableLighting();
+
+ GlStateManager.shadeModel(GL11.GL_FLAT);
+ GlStateManager.enableCull();
+ GlStateManager.disableBlend();
+ // RenderHelper.enableStandardItemLighting();
+
+ GlStateManager.popMatrix();
+ }
+ }
+ }
+
+ // Third person
+ @SubscribeEvent
+ public static void onRenderPlayerEvent(RenderPlayerEvent.Post event){
+
+ EntityPlayer player = event.getEntityPlayer();
+
+ if(WizardData.get(player).getVariable(Shield.SHIELD_KEY) != null && WizardryUtilities.isCasting(player, Spells.shield)){
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.disableCull();
+ GlStateManager.enableBlend();
+ // For some reason, the old blend function (GL11.GL_SRC_ALPHA, GL11.GL_SRC_ALPHA) caused the inner
+ // edges to appear black, so I have changed it to this, which looks very slightly different.
+ GlStateManager.blendFunc(GL11.GL_ONE, GL11.GL_SRC_ALPHA);
+ GlStateManager.shadeModel(GL11.GL_SMOOTH);
+ GlStateManager.disableLighting();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ Vec3d delta = player.getPositionEyes(event.getPartialRenderTick())
+ .subtract(Minecraft.getMinecraft().player.getPositionEyes(event.getPartialRenderTick()));
+ GlStateManager.translate(delta.x, delta.y, delta.z);
+
+ GlStateManager.translate(0, 1.3, 0);
+
+ // GlStateManager.rotate(180, 0, 1, 0);
+ GlStateManager.rotate(-player.renderYawOffset, 0, 1, 0);
+ // GlStateManager.rotate(-player.rotationPitch, 1, 0, 0);
+
+ GlStateManager.translate(0, 0, 0.8);
+
+ Tessellator tessellator = Tessellator.getInstance();
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE);
+
+ render(tessellator);
+
+ GlStateManager.enableLighting();
+
+ GlStateManager.shadeModel(GL11.GL_FLAT);
+ GlStateManager.enableCull();
+ GlStateManager.disableBlend();
+ // RenderHelper.enableStandardItemLighting();
+
+ GlStateManager.popMatrix();
+ }
+ }
+
+ private static void render(Tessellator tessellator){
+
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ double widthOuter = 0.6d;
+ double heightOuter = 0.7d;
+ double widthInner = 0.3d;
+ double heightInner = 0.4d;
+ double depth = 0.2d;
+
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR);
+
+ buffer.pos(-widthOuter, heightInner, -depth).tex(0, 0.2).color(0, 0, 0, 255).endVertex();
+ buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
+ buffer.pos(-widthInner, heightOuter, -depth).tex(0.2, 0).color(0, 0, 0, 255).endVertex();
+ buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
+
+ buffer.pos(widthInner, heightOuter, -depth).tex(0.8, 0).color(0, 0, 0, 255).endVertex();
+ buffer.pos(widthInner, heightInner, 0).tex(0.8, 0.2).color(200, 200, 255, 255).endVertex();
+ buffer.pos(widthOuter, heightInner, -depth).tex(1, 0.2).color(0, 0, 0, 255).endVertex();
+ buffer.pos(widthInner, heightInner, 0).tex(0.8, 0.2).color(200, 200, 255, 255).endVertex();
+
+ buffer.pos(widthOuter, -heightInner, -depth).tex(1, 0.8).color(0, 0, 0, 255).endVertex();
+ buffer.pos(widthInner, -heightInner, 0).tex(0.8, 0.8).color(200, 200, 255, 255).endVertex();
+ buffer.pos(widthInner, -heightOuter, -depth).tex(0.8, 1).color(0, 0, 0, 255).endVertex();
+ buffer.pos(widthInner, -heightInner, 0).tex(0.8, 0.8).color(200, 200, 255, 255).endVertex();
+
+ buffer.pos(-widthInner, -heightOuter, -depth).tex(0.2, 1).color(0, 0, 0, 255).endVertex();
+ buffer.pos(-widthInner, -heightInner, 0).tex(0.2, 0.8).color(200, 200, 255, 255).endVertex();
+ buffer.pos(-widthOuter, -heightInner, -depth).tex(0, 0.8).color(0, 0, 0, 255).endVertex();
+ buffer.pos(-widthInner, -heightInner, 0).tex(0.2, 0.8).color(200, 200, 255, 255).endVertex();
+
+ buffer.pos(-widthOuter, heightInner, -depth).tex(0, 0.2).color(0, 0, 0, 255).endVertex();
+ buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
+
+ tessellator.draw();
+
+ buffer.begin(GL11.GL_TRIANGLE_STRIP, DefaultVertexFormats.POSITION_TEX_COLOR);
+
+ buffer.pos(-widthInner, heightInner, 0).tex(0.2, 0.2).color(200, 200, 255, 255).endVertex();
+ buffer.pos(widthInner, heightInner, 0).tex(0.8, 0.2).color(200, 200, 255, 255).endVertex();
+ buffer.pos(-widthInner, -heightInner, 0).tex(0.2, 0.8).color(200, 200, 255, 255).endVertex();
+ buffer.pos(widthInner, -heightInner, 0).tex(0.8, 0.8).color(200, 200, 255, 255).endVertex();
+
+ tessellator.draw();
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderSigil.java b/src/main/java/electroblob/wizardry/client/renderer/RenderSigil.java
index 739b44c1..5d43d31c 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderSigil.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderSigil.java
@@ -1,10 +1,8 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.entity.construct.EntityHealAura;
import electroblob.wizardry.entity.construct.EntityMagicConstruct;
-import electroblob.wizardry.util.WizardryUtilities;
+import electroblob.wizardry.util.AllyDesignationSystem;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
@@ -15,6 +13,7 @@ import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.ResourceLocation;
+import org.lwjgl.opengl.GL11;
public class RenderSigil extends Render {
@@ -34,8 +33,8 @@ public class RenderSigil extends Render {
// Makes the sigil invisible to enemies of the player that created it
if(this.invisibleToEnemies){
-
- if(entity.getCaster() instanceof EntityPlayer && !WizardryUtilities
+ // Unfortunately we can't access the caster's allies if they're not online, it only works the other way round
+ if(entity.getCaster() instanceof EntityPlayer && !AllyDesignationSystem
.isPlayerAlly((EntityPlayer)entity.getCaster(), Minecraft.getMinecraft().player)){
return;
}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritHorse.java b/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritHorse.java
index c3452e38..79cd0ab7 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritHorse.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritHorse.java
@@ -1,22 +1,23 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.entity.living.EntitySpiritHorse;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.entity.RenderHorse;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.passive.EntityHorse;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import org.lwjgl.opengl.GL11;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderSpiritHorse extends RenderHorse {
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID,
"textures/entity/spirit_horse.png");
+// private static final int GHOST_COPIES = 3;
+// private static final float DECONVERGENCE = 0.35f;
+
public RenderSpiritHorse(RenderManager renderManager){
super(renderManager);
}
@@ -27,10 +28,36 @@ public class RenderSpiritHorse extends RenderHorse {
}
@Override
- protected void preRenderCallback(EntityHorse entitylivingbaseIn, float partialTickTime){
- super.preRenderCallback(entitylivingbaseIn, partialTickTime);
+ protected void preRenderCallback(EntityHorse horse, float partialTickTime){
+ super.preRenderCallback(horse, partialTickTime);
GlStateManager.enableBlend();
GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ if(horse instanceof EntitySpiritHorse){ // Always true
+ GlStateManager.color(1, 1, 1, ((EntitySpiritHorse)horse).getOpacity());
+ }
+ }
+
+ @Override
+ public void doRender(EntityHorse entity, double x, double y, double z, float entityYaw, float partialTicks){
+
+ super.doRender(entity, x, y, z, entityYaw, partialTicks);
+
+// double dx = (entity.posX - entity.prevPosX) * DECONVERGENCE;
+// double dy = (entity.posY - entity.prevPosY) * DECONVERGENCE;
+// double dz = (entity.posZ - entity.prevPosZ) * DECONVERGENCE;
+// float dyaw = (entity.rotationYaw - entity.prevRotationYaw) * DECONVERGENCE;
+//
+// float opacity = 1;
+// if(entity instanceof EntitySpiritHorse){ // Always true
+// opacity = ((EntitySpiritHorse)entity).getOpacity();
+// }
+//
+// for(int i = 0; i < GHOST_COPIES; i++){
+//
+// GlStateManager.color(1, 1, 1, opacity * (0.6f - (float)i/(GHOST_COPIES*2)));
+//
+// super.doRender(entity, x - dx * i, y - dy * i, z - dz * i, entityYaw - dyaw * i, partialTicks);
+// }
}
}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritWolf.java b/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritWolf.java
index a3f4fadc..0564f69b 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritWolf.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderSpiritWolf.java
@@ -1,22 +1,23 @@
package electroblob.wizardry.client.renderer;
-import org.lwjgl.opengl.GL11;
-
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.entity.living.EntitySpiritWolf;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.RenderWolf;
import net.minecraft.entity.passive.EntityWolf;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
+import org.lwjgl.opengl.GL11;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderSpiritWolf extends RenderWolf {
private static final ResourceLocation texture = new ResourceLocation(Wizardry.MODID,
"textures/entity/spirit_wolf.png");
+// private static final int GHOST_COPIES = 3;
+// private static final float DECONVERGENCE = 0.8f;
+
public RenderSpiritWolf(RenderManager renderManager){
super(renderManager);
}
@@ -31,5 +32,31 @@ public class RenderSpiritWolf extends RenderWolf {
super.preRenderCallback(entity, partialTickTime);
GlStateManager.enableBlend();
GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ if(entity instanceof EntitySpiritWolf){ // Always true
+ GlStateManager.color(1, 1, 1, ((EntitySpiritWolf)entity).getOpacity());
+ }
+ }
+
+ @Override
+ public void doRender(EntityWolf entity, double x, double y, double z, float entityYaw, float partialTicks){
+
+ super.doRender(entity, x, y, z, entityYaw, partialTicks);
+
+// double dx = (entity.posX - entity.prevPosX) * DECONVERGENCE;
+// double dy = (entity.posY - entity.prevPosY) * DECONVERGENCE;
+// double dz = (entity.posZ - entity.prevPosZ) * DECONVERGENCE;
+// float dyaw = (entity.rotationYaw - entity.prevRotationYaw) * DECONVERGENCE;
+//
+// float opacity = 1;
+// if(entity instanceof EntitySpiritWolf){ // Always true
+// opacity = ((EntitySpiritWolf)entity).getOpacity();
+// }
+//
+// for(int i = 0; i < GHOST_COPIES; i++){
+//
+// GlStateManager.color(1, 1, 1, opacity * (0.6f - (float)i/(GHOST_COPIES*2)));
+//
+// super.doRender(entity, x - dx * i, y - dy * i, z - dz * i, entityYaw - dyaw * i, partialTicks);
+// }
}
}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderStrayMinion.java b/src/main/java/electroblob/wizardry/client/renderer/RenderStrayMinion.java
new file mode 100644
index 00000000..815d4979
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderStrayMinion.java
@@ -0,0 +1,24 @@
+package electroblob.wizardry.client.renderer;
+
+import net.minecraft.client.renderer.entity.RenderManager;
+import net.minecraft.client.renderer.entity.RenderSkeleton;
+import net.minecraft.entity.monster.AbstractSkeleton;
+import net.minecraft.util.ResourceLocation;
+
+/** This class also had to be copied for the same reason as {@link LayerStrayMinionClothing}. */
+public class RenderStrayMinion extends RenderSkeleton {
+
+ private static final ResourceLocation STRAY_SKELETON_TEXTURES = new ResourceLocation("textures/entity/skeleton/stray.png");
+
+ public RenderStrayMinion(RenderManager manager){
+ super(manager);
+ this.addLayer(new LayerStrayMinionClothing(this)); // This is the only change
+ }
+
+ /**
+ * Returns the location of an entity's texture. Doesn't seem to be called unless you call Render.bindEntityTexture.
+ */
+ protected ResourceLocation getEntityTexture(AbstractSkeleton entity){
+ return STRAY_SKELETON_TEXTURES;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderTransportationUI.java b/src/main/java/electroblob/wizardry/client/renderer/RenderTransportationUI.java
new file mode 100644
index 00000000..599d168e
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderTransportationUI.java
@@ -0,0 +1,177 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.data.WizardData;
+import electroblob.wizardry.item.ISpellCastingItem;
+import electroblob.wizardry.item.ItemArtefact;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.registry.WizardryItems;
+import electroblob.wizardry.spell.Transportation;
+import electroblob.wizardry.util.Location;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.Vec3d;
+import net.minecraftforge.client.event.RenderWorldLastEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import org.lwjgl.opengl.GL11;
+
+import java.util.List;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class RenderTransportationUI {
+
+ private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/gui/transportation_marker.png");
+
+ // I can't get rid of the view bobbing with RenderWorldLastEvent, is there an alternative?
+ @SubscribeEvent
+ public static void onRenderWorldLastEvent(RenderWorldLastEvent event){
+
+ // Only render in first person
+ if(Minecraft.getMinecraft().gameSettings.thirdPersonView != 0) return;
+
+ EntityPlayer player = Minecraft.getMinecraft().player;
+
+ ItemStack stack = player.getHeldItemMainhand();
+ if(!(stack.getItem() instanceof ISpellCastingItem)){
+ stack = player.getHeldItemOffhand();
+ if(!(stack.getItem() instanceof ISpellCastingItem)) return;
+ }
+
+ if(((ISpellCastingItem)stack.getItem()).getCurrentSpell(stack) == Spells.transportation
+ && ItemArtefact.isArtefactActive(player, WizardryItems.charm_transportation)){
+
+ WizardData data = WizardData.get(player);
+ if(data == null) return;
+
+ List locations = data.getVariable(Transportation.LOCATIONS_KEY);
+
+ if(locations == null) return;
+
+ GlStateManager.pushMatrix();
+
+ Vec3d origin = player.getPositionEyes(event.getPartialTicks());
+ GlStateManager.translate(0, origin.y - Minecraft.getMinecraft().getRenderManager().viewerPosY, 0);
+
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ Location target = Transportation.getLocationAimedAt(player, locations, event.getPartialTicks());
+
+ for(Location location : locations){
+
+ if(location.dimension != player.dimension) continue;
+
+ GlStateManager.pushMatrix();
+ GlStateManager.enableBlend();
+ GlStateManager.disableLighting();
+ GlStateManager.disableDepth();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240, 240);
+ GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ GlStateManager.color(1, 1, 1, 1);
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE);
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX_COLOR);
+
+ Vec3d position = WizardryUtilities.getCentre(location.pos).subtract(origin);
+ double distance = position.length();
+ // The icon lines up perfectly if you render it at actual distance, otherwise view bobbing messes things up
+ // However, if that's outside the render distance it won't render at all! To fudge our way around this
+ // problem, we're capping the distance to just below the render distance and adjusting the scale accordingly
+ double distanceCap = Minecraft.getMinecraft().gameSettings.renderDistanceChunks * 16 - 8;
+ double displayDist = distance > distanceCap ? distanceCap : distance;
+ double factor = displayDist/distance;
+
+ GlStateManager.translate(position.x * factor, position.y * factor, position.z * factor);
+
+ GlStateManager.rotate(-Minecraft.getMinecraft().getRenderManager().playerViewY, 0.0F, 1.0F, 0.0F);
+ GlStateManager.rotate(Minecraft.getMinecraft().getRenderManager().playerViewX, 1.0F, 0.0F, 0.0F);
+
+ // Get the angle between the player's look vector and the direction of the stone circle
+ double angle = Transportation.getLookDeviationAngle(player, location.pos, event.getPartialTicks());
+ double iconSize = Transportation.getIconSize(distance);
+
+ // Now apply a fancy formula to make it enlarge with a nice smooth animation:
+ double proximityFactor = Math.max(0, Math.pow(1 - angle/iconSize * angle/iconSize, 3));
+ iconSize *= 1 + 0.3 * proximityFactor;
+ iconSize *= displayDist; // Adjust the icon size for perspective
+
+ float f = location == target ? 1 : 0.5f; // Makes it obvious which one is being aimed at
+
+ buffer.pos(-iconSize, iconSize, 0).tex(0, 0).color(f, 1, f, f).endVertex();
+ buffer.pos(iconSize, iconSize, 0).tex(1, 0).color(f, 1, f, f).endVertex();
+ buffer.pos(iconSize, -iconSize, 0).tex(1, 1).color(f, 1, f, f).endVertex();
+ buffer.pos(-iconSize, -iconSize, 0).tex(0, 1).color(f, 1, f, f).endVertex();
+
+ tessellator.draw();
+
+ GlStateManager.popMatrix();
+
+ if(location == target){
+ String label = location.pos.getX() + ", " + location.pos.getY() + ", " + location.pos.getZ();
+ drawLabel(Minecraft.getMinecraft().fontRenderer, label, (float)(position.x * factor),
+ (float)(position.y * factor + iconSize*1.5f), (float)(position.z * factor), (float)displayDist * 0.2f, 0,
+ Minecraft.getMinecraft().getRenderManager().playerViewY, Minecraft.getMinecraft().getRenderManager().playerViewX);
+ }
+ }
+
+ GlStateManager.disableBlend();
+ GlStateManager.enableTexture2D();
+ GlStateManager.enableLighting();
+ GlStateManager.enableDepth();
+ GlStateManager.disableRescaleNormal();
+ GlStateManager.popMatrix();
+ }
+ }
+
+ // Copied from EntityRenderer#drawNameplate and tweaked a bit
+ private static void drawLabel(FontRenderer fontRendererIn, String str, float x, float y, float z, float scale, int verticalShift, float viewerYaw, float viewerPitch){
+
+ GlStateManager.pushMatrix();
+ GlStateManager.translate(x, y, z);
+ GlStateManager.glNormal3f(0.0F, 1.0F, 0.0F);
+ GlStateManager.rotate(-viewerYaw, 0.0F, 1.0F, 0.0F);
+ GlStateManager.rotate(viewerPitch, 1.0F, 0.0F, 0.0F);
+ GlStateManager.scale(-0.025F, -0.025F, 0.025F);
+ GlStateManager.scale(scale, scale, scale);
+ GlStateManager.disableLighting();
+ GlStateManager.depthMask(false);
+
+ GlStateManager.disableDepth();
+
+ GlStateManager.enableBlend();
+ GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
+ int i = fontRendererIn.getStringWidth(str) / 2;
+ GlStateManager.disableTexture2D();
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder bufferbuilder = tessellator.getBuffer();
+ bufferbuilder.begin(7, DefaultVertexFormats.POSITION_COLOR);
+ bufferbuilder.pos((double)(-i - 1), (double)(-1 + verticalShift), 0.0D).color(0.0F, 0.0F, 0.0F, 0.25F).endVertex();
+ bufferbuilder.pos((double)(-i - 1), (double)(8 + verticalShift), 0.0D).color(0.0F, 0.0F, 0.0F, 0.25F).endVertex();
+ bufferbuilder.pos((double)(i + 1), (double)(8 + verticalShift), 0.0D).color(0.0F, 0.0F, 0.0F, 0.25F).endVertex();
+ bufferbuilder.pos((double)(i + 1), (double)(-1 + verticalShift), 0.0D).color(0.0F, 0.0F, 0.0F, 0.25F).endVertex();
+ tessellator.draw();
+ GlStateManager.enableTexture2D();
+
+ fontRendererIn.drawString(str, -fontRendererIn.getStringWidth(str) / 2, verticalShift, 0x86ff65);
+ GlStateManager.enableDepth();
+
+ GlStateManager.depthMask(true);
+ fontRendererIn.drawString(str, -fontRendererIn.getStringWidth(str) / 2, verticalShift, 0x86ff65);
+ GlStateManager.enableLighting();
+ GlStateManager.disableBlend();
+ GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
+ GlStateManager.popMatrix();
+ }
+}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderWings.java b/src/main/java/electroblob/wizardry/client/renderer/RenderWings.java
new file mode 100644
index 00000000..5701d736
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderWings.java
@@ -0,0 +1,113 @@
+package electroblob.wizardry.client.renderer;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.util.WizardryUtilities;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.BufferBuilder;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.OpenGlHelper;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.util.ResourceLocation;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+import net.minecraftforge.client.event.RenderPlayerEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.relauncher.Side;
+import org.lwjgl.opengl.GL11;
+
+@Mod.EventBusSubscriber(Side.CLIENT)
+public class RenderWings {
+
+ private static final ResourceLocation TEXTURE = new ResourceLocation(Wizardry.MODID, "textures/entity/wing.png");
+
+ // No first person in here because you can never see the wings on your back!
+
+ // Third person
+ @SubscribeEvent
+ public static void onRenderPlayerEvent(RenderPlayerEvent.Post event){
+
+ EntityPlayer player = event.getEntityPlayer();
+
+ if(WizardryUtilities.isCasting(player, Spells.flight)){
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.enableBlend();
+ GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
+ GlStateManager.disableLighting();
+ OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240f, 240f);
+
+ Vec3d delta = player.getPositionEyes(event.getPartialRenderTick())
+ .subtract(Minecraft.getMinecraft().player.getPositionEyes(event.getPartialRenderTick()));
+ GlStateManager.translate(delta.x, delta.y, delta.z);
+
+ // GlStateManager.rotate(-entityplayer.rotationYawHead, 0, 1, 0);
+ GlStateManager.rotate(-player.renderYawOffset, 0, 1, 0);
+ // GlStateManager.rotate(180, 1, 0, 0);
+
+ Minecraft.getMinecraft().renderEngine.bindTexture(TEXTURE);
+ Tessellator tessellator = Tessellator.getInstance();
+ BufferBuilder buffer = tessellator.getBuffer();
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.translate(0.1, 0.4, -0.15);
+ GlStateManager.rotate(20 + 20 * MathHelper.sin((player.ticksExisted + event.getPartialRenderTick()) * 0.3f), 0, 1, 0);
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(0, 2, 0).tex(0, 0).endVertex();
+ buffer.pos(2, 2, 0).tex(1, 0).endVertex();
+ buffer.pos(2, 0, 0).tex(1, 1).endVertex();
+ buffer.pos(0, 0, 0).tex(0, 1).endVertex();
+
+ tessellator.draw();
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(0, 2, 0).tex(0, 0).endVertex();
+ buffer.pos(0, 0, 0).tex(0, 1).endVertex();
+ buffer.pos(2, 0, 0).tex(1, 1).endVertex();
+ buffer.pos(2, 2, 0).tex(1, 0).endVertex();
+
+ tessellator.draw();
+
+ GlStateManager.popMatrix();
+
+ GlStateManager.pushMatrix();
+
+ GlStateManager.translate(-0.1, 0.4, -0.15);
+ GlStateManager.rotate(-200 - 20 * MathHelper.sin((player.ticksExisted + event.getPartialRenderTick()) * 0.3f), 0, 1, 0);
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(0, 2, 0).tex(0, 0).endVertex();
+ buffer.pos(2, 2, 0).tex(1, 0).endVertex();
+ buffer.pos(2, 0, 0).tex(1, 1).endVertex();
+ buffer.pos(0, 0, 0).tex(0, 1).endVertex();
+
+ tessellator.draw();
+
+ buffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);
+
+ buffer.pos(0, 2, 0).tex(0, 0).endVertex();
+ buffer.pos(0, 0, 0).tex(0, 1).endVertex();
+ buffer.pos(2, 0, 0).tex(1, 1).endVertex();
+ buffer.pos(2, 2, 0).tex(1, 0).endVertex();
+
+ tessellator.draw();
+
+ GlStateManager.popMatrix();
+
+ GlStateManager.enableLighting();
+ GlStateManager.disableBlend();
+
+ GlStateManager.popMatrix();
+ }
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderWizard.java b/src/main/java/electroblob/wizardry/client/renderer/RenderWizard.java
index baa322ee..566bc5a7 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderWizard.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderWizard.java
@@ -7,10 +7,8 @@ import net.minecraft.client.renderer.entity.RenderBiped;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.layers.LayerBipedArmor;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderWizard extends RenderBiped {
static final ResourceLocation[] textures = new ResourceLocation[6];
diff --git a/src/main/java/electroblob/wizardry/client/renderer/RenderWraithMinion.java b/src/main/java/electroblob/wizardry/client/renderer/RenderWraithMinion.java
index da691189..d8cfe382 100644
--- a/src/main/java/electroblob/wizardry/client/renderer/RenderWraithMinion.java
+++ b/src/main/java/electroblob/wizardry/client/renderer/RenderWraithMinion.java
@@ -5,10 +5,8 @@ import net.minecraft.client.model.ModelBlaze;
import net.minecraft.client.renderer.entity.RenderLiving;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.util.ResourceLocation;
-import net.minecraftforge.fml.relauncher.Side;
-import net.minecraftforge.fml.relauncher.SideOnly;
-@SideOnly(Side.CLIENT)
+//@SideOnly(Side.CLIENT)
public class RenderWraithMinion extends RenderLiving {
private ResourceLocation texture = new ResourceLocation("textures/entity/blaze.png");
diff --git a/src/main/java/electroblob/wizardry/command/CommandCastSpell.java b/src/main/java/electroblob/wizardry/command/CommandCastSpell.java
index 7dbbde14..c71e9e29 100644
--- a/src/main/java/electroblob/wizardry/command/CommandCastSpell.java
+++ b/src/main/java/electroblob/wizardry/command/CommandCastSpell.java
@@ -1,36 +1,41 @@
package electroblob.wizardry.command;
-import java.util.List;
-
-import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.event.SpellCastEvent;
import electroblob.wizardry.event.SpellCastEvent.Source;
import electroblob.wizardry.packet.PacketCastSpell;
+import electroblob.wizardry.packet.PacketCastSpellAtPos;
import electroblob.wizardry.packet.WizardryPacketHandler;
import electroblob.wizardry.spell.Spell;
import electroblob.wizardry.util.SpellModifiers;
-import net.minecraft.command.CommandBase;
-import net.minecraft.command.CommandException;
-import net.minecraft.command.ICommandSender;
-import net.minecraft.command.NumberInvalidException;
-import net.minecraft.command.PlayerNotFoundException;
-import net.minecraft.command.WrongUsageException;
-import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.command.*;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.JsonToNBT;
import net.minecraft.nbt.NBTException;
import net.minecraft.server.MinecraftServer;
+import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumHand;
import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Vec3d;
import net.minecraft.util.text.ITextComponent;
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.fml.common.network.simpleimpl.IMessage;
+import java.util.List;
+
public class CommandCastSpell extends CommandBase {
+ /** The default number of ticks for which /cast will cast a continuous spell, if duration is not specified. */
+ public static final int DEFAULT_CASTING_DURATION = 100;
+ /** The minimum number of seconds for which /cast may cast a continuous spell. */
+ public static final int MIN_CASTING_DURATION = 0;
+ /** The maximum number of seconds for which /cast may cast a continuous spell. */
+ public static final int MAX_CASTING_DURATION = 1000000;
+
@Override
public String getName(){
return Wizardry.settings.castCommandName;
@@ -74,6 +79,8 @@ public class CommandCastSpell extends CommandBase {
int i = 0;
EntityPlayerMP caster = null;
+ Vec3d origin = null;
+ EnumFacing direction = null;
try{
caster = getCommandSenderAsPlayer(sender);
@@ -85,12 +92,24 @@ public class CommandCastSpell extends CommandBase {
Spell spell = Spell.get(arguments[i++]);
if(spell == null){
- throw new NumberInvalidException("commands." + Wizardry.MODID + ":cast.not_found", new Object[]{arguments[i - 1]});
+ throw new NumberInvalidException("commands." + Wizardry.MODID + ":cast.not_found", arguments[i - 1]);
}
boolean castAsOtherPlayer = false;
- if(i < arguments.length){
+ if(i + 3 < arguments.length){
+
+ Vec3d vec3d = sender.getPositionVector();
+ CoordinateArg x = parseCoordinate(vec3d.x, arguments[i++], true);
+ CoordinateArg y = parseCoordinate(vec3d.y, arguments[i++], 0, 256, false);
+ CoordinateArg z = parseCoordinate(vec3d.z, arguments[i++], true);
+
+ origin = new Vec3d(x.getResult(), y.getResult(), z.getResult());
+
+ direction = EnumFacing.byName(arguments[i++]);
+ if(direction == null) throw new NumberInvalidException("commands." + Wizardry.MODID + ":cast.invalid_direction", arguments[i - 1]);
+
+ }else if(i < arguments.length){
try{
// If the second argument is a player and is not the player that gave the command, the spell is cast
// as the given player rather than the command sender, and there is a different chat readout.
@@ -107,8 +126,31 @@ public class CommandCastSpell extends CommandBase {
// If, after this point, the player is still null, the sender must be a command block or the console and the
// player must not have been specified, meaning an exception should be thrown.
- if(caster == null)
- throw new PlayerNotFoundException("You must specify which player you wish to perform this action on.");
+ if(caster == null && origin == null)
+ throw new PlayerNotFoundException("commands." + Wizardry.MODID + ":cast.origin_not_specified");
+
+ int duration = DEFAULT_CASTING_DURATION;
+ int seconds = duration/20;
+
+ if(spell.isContinuous){
+
+ if(i >= arguments.length) throw new CommandException("commands." + Wizardry.MODID + ":cast.duration_not_specified");
+
+ try{
+ seconds = parseInt(arguments[i++]);
+ }catch(NumberInvalidException e){
+ // If no duration was found, assume it was unspecified
+ i--;
+ }
+
+ if(seconds < MIN_CASTING_DURATION){
+ throw new NumberInvalidException("commands.generic.num.tooSmall", seconds, MIN_CASTING_DURATION);
+ }else if(seconds > MAX_CASTING_DURATION){
+ throw new NumberInvalidException("commands.generic.num.tooBig", seconds, MAX_CASTING_DURATION);
+ }
+
+ duration = seconds * 20;
+ }
SpellModifiers modifiers = new SpellModifiers();
@@ -136,64 +178,124 @@ public class CommandCastSpell extends CommandBase {
// ===== Spell casting =====
- // If anything stops the spell working at this point, nothing else happens.
- if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Pre(caster, spell, modifiers, Source.COMMAND))){
- displayFailMessage(sender, spell);
- return;
- }
+ if(origin != null){ // Positional
- WizardData data = WizardData.get((EntityPlayer)caster);
+ World world = sender.getEntityWorld();
- if(spell.isContinuous){
+ // If anything stops the spell working at this point, nothing else happens.
+ if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Pre(Source.COMMAND, spell, world,
+ origin.x, origin.y, origin.z, direction, modifiers))){
+ if(server.sendCommandFeedback()) displayFailMessage(sender, spell);
+ return;
+ }
- // Events for continuous spell casting via commands are dealt with in WizardData.
+ if(spell.isContinuous){
- if(data != null){
- if(data.isCasting()){
- data.stopCastingContinuousSpell();
- }else{
+ if(spell.cast(world, origin.x, origin.y, origin.z, direction, 0, duration, modifiers)){
- data.startCastingContinuousSpell(spell, modifiers);
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.COMMAND, spell, world, origin.x, origin.y, origin.z, direction, modifiers));
- if(castAsOtherPlayer){
- sender.sendMessage(
- new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_remote_continuous",
- spell.getNameForTranslationFormatted(), caster.getName()));
- }else{
- sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_continuous",
- spell.getNameForTranslationFormatted()));
+ SpellEmitter.add(spell, world, origin.x, origin.y, origin.z, direction, duration, modifiers);
+ IMessage msg = new PacketCastSpellAtPos.Message(origin, direction, spell, modifiers, duration);
+ WizardryPacketHandler.net.sendToDimension(msg, world.provider.getDimension());
+
+ if(server.sendCommandFeedback()){
+ sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_position_continuous",
+ spell.getNameForTranslationFormatted(), origin.x, origin.y, origin.z, seconds));
}
+
+ return;
}
+ }else{
+
+ if(spell.cast(world, origin.x, origin.y, origin.z, direction, 0, -1, modifiers)){
+
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.COMMAND, spell, world, origin.x, origin.y, origin.z, direction, modifiers));
+
+ if(spell.requiresPacket()){
+ // Sends a packet to all players in dimension to tell them to spawn particles.
+ // Only sent if the spell succeeded, because if the spell failed, you wouldn't
+ // need to spawn any particles!
+ IMessage msg = new PacketCastSpellAtPos.Message(origin, direction, spell, modifiers);
+ WizardryPacketHandler.net.sendToDimension(msg, world.provider.getDimension());
+ }
+
+ if(server.sendCommandFeedback()){
+ sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_position",
+ spell.getNameForTranslationFormatted(), origin.x, origin.y, origin.z));
+ }
+
+ return;
+ }
+ }
+
+ }else{ // Player-based
+
+ // If anything stops the spell working at this point, nothing else happens.
+ if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Pre(Source.COMMAND, spell, caster, modifiers))){
+ if(server.sendCommandFeedback()) displayFailMessage(sender, spell);
return;
}
- }else{
+ if(spell.isContinuous){
- if(spell.cast(caster.world, caster, EnumHand.MAIN_HAND, 0, modifiers)){
+ WizardData data = WizardData.get(caster);
- MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(caster, spell, modifiers, Source.COMMAND));
+ // Events/packets for continuous spell casting via commands are dealt with in WizardData.
- if(spell.doesSpellRequirePacket()){
- // Sends a packet to all players in dimension to tell them to spawn particles.
- // Only sent if the spell succeeded, because if the spell failed, you wouldn't
- // need to spawn any particles!
- IMessage msg = new PacketCastSpell.Message(caster.getEntityId(), null, spell.id(), modifiers);
- WizardryPacketHandler.net.sendToDimension(msg, caster.world.provider.getDimension());
+ if(data != null){
+ if(data.isCasting()){
+ data.stopCastingContinuousSpell(); // TODO: Where should this go now?
+ }else{
+
+ data.startCastingContinuousSpell(spell, modifiers, duration);
+
+ if(server.sendCommandFeedback()){
+ if(castAsOtherPlayer){
+ sender.sendMessage(
+ new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_remote_continuous",
+ spell.getNameForTranslationFormatted(), caster.getName(), seconds));
+ }else{
+ sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_continuous",
+ spell.getNameForTranslationFormatted(), seconds));
+ }
+ }
+ }
+
+ return;
}
- if(castAsOtherPlayer){
- sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_remote",
- spell.getNameForTranslationFormatted(), caster.getName()));
- }else{
- sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success",
- spell.getNameForTranslationFormatted()));
+ }else{
+
+ if(spell.cast(caster.world, caster, EnumHand.MAIN_HAND, 0, modifiers)){
+
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(Source.COMMAND, spell, caster, modifiers));
+
+ if(spell.requiresPacket()){
+ // Sends a packet to all players in dimension to tell them to spawn particles.
+ // Only sent if the spell succeeded, because if the spell failed, you wouldn't
+ // need to spawn any particles!
+ IMessage msg = new PacketCastSpell.Message(caster.getEntityId(), null, spell, modifiers);
+ WizardryPacketHandler.net.sendToDimension(msg, caster.world.provider.getDimension());
+ }
+
+ if(server.sendCommandFeedback()){
+ if(castAsOtherPlayer){
+ sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success_remote",
+ spell.getNameForTranslationFormatted(), caster.getName()));
+ }else{
+ sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":cast.success",
+ spell.getNameForTranslationFormatted()));
+ }
+ }
+
+ return;
}
- return;
}
}
- displayFailMessage(sender, spell);
+ if(server.sendCommandFeedback()) displayFailMessage(sender, spell);
}
}
diff --git a/src/main/java/electroblob/wizardry/command/CommandDiscoverSpell.java b/src/main/java/electroblob/wizardry/command/CommandDiscoverSpell.java
index d97e6470..84733924 100644
--- a/src/main/java/electroblob/wizardry/command/CommandDiscoverSpell.java
+++ b/src/main/java/electroblob/wizardry/command/CommandDiscoverSpell.java
@@ -1,24 +1,19 @@
package electroblob.wizardry.command;
-import java.util.List;
-
-import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.event.DiscoverSpellEvent;
import electroblob.wizardry.registry.Spells;
import electroblob.wizardry.spell.Spell;
-import net.minecraft.command.CommandBase;
-import net.minecraft.command.CommandException;
-import net.minecraft.command.ICommandSender;
-import net.minecraft.command.NumberInvalidException;
-import net.minecraft.command.PlayerNotFoundException;
-import net.minecraft.command.WrongUsageException;
+import net.minecraft.command.*;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraftforge.common.MinecraftForge;
+import java.util.List;
+
public class CommandDiscoverSpell extends CommandBase {
@Override
@@ -92,7 +87,7 @@ public class CommandDiscoverSpell extends CommandBase {
if(spell == null){
throw new NumberInvalidException("commands." + Wizardry.MODID + ":discoverspell.not_found",
- new Object[]{arguments[i - 1]});
+ arguments[i - 1]);
}
}
@@ -110,32 +105,32 @@ public class CommandDiscoverSpell extends CommandBase {
if(player == null)
throw new PlayerNotFoundException("You must specify which player you wish to perform this action on.");
- WizardData properties = WizardData.get(player);
+ WizardData data = WizardData.get(player);
- if(properties != null){
+ if(data != null){
if(clear){
- properties.spellsDiscovered.clear();
- sender.sendMessage(
+ data.spellsDiscovered.clear();
+ if(server.sendCommandFeedback()) sender.sendMessage(
new TextComponentTranslation("commands." + Wizardry.MODID + ":discoverspell.clear", player.getName()));
}else if(all){
- properties.spellsDiscovered.addAll(Spell.getSpells(Spell.allSpells));
- sender.sendMessage(
+ data.spellsDiscovered.addAll(Spell.getSpells(Spell.allSpells));
+ if(server.sendCommandFeedback()) sender.sendMessage(
new TextComponentTranslation("commands." + Wizardry.MODID + ":discoverspell.all", player.getName()));
}else{
- if(properties.hasSpellBeenDiscovered(spell)){
- properties.spellsDiscovered.remove(spell);
- sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":discoverspell.removespell",
+ if(data.hasSpellBeenDiscovered(spell)){
+ data.spellsDiscovered.remove(spell);
+ if(server.sendCommandFeedback()) sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":discoverspell.removespell",
spell.getNameForTranslationFormatted(), player.getName()));
}else{
if(!MinecraftForge.EVENT_BUS
.post(new DiscoverSpellEvent(player, spell, DiscoverSpellEvent.Source.COMMAND))){
- properties.discoverSpell(spell);
- sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":discoverspell.addspell",
+ data.discoverSpell(spell);
+ if(server.sendCommandFeedback()) sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":discoverspell.addspell",
spell.getNameForTranslationFormatted(), player.getName()));
}
}
}
- properties.sync();
+ data.sync();
}
}
}
diff --git a/src/main/java/electroblob/wizardry/command/CommandSetAlly.java b/src/main/java/electroblob/wizardry/command/CommandSetAlly.java
index 2ca42dfa..2ffcc375 100644
--- a/src/main/java/electroblob/wizardry/command/CommandSetAlly.java
+++ b/src/main/java/electroblob/wizardry/command/CommandSetAlly.java
@@ -1,16 +1,9 @@
package electroblob.wizardry.command;
-import java.util.List;
-
-import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.command.CommandBase;
-import net.minecraft.command.CommandException;
-import net.minecraft.command.ICommandSender;
-import net.minecraft.command.NumberInvalidException;
-import net.minecraft.command.PlayerNotFoundException;
-import net.minecraft.command.WrongUsageException;
+import net.minecraft.command.*;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.server.MinecraftServer;
@@ -18,6 +11,8 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;
+import java.util.List;
+
public class CommandSetAlly extends CommandBase {
@Override
@@ -85,10 +80,12 @@ public class CommandSetAlly extends CommandBase {
if(allyOf != sender && sender instanceof EntityPlayer
&& !WizardryUtilities.isPlayerOp((EntityPlayer)sender, server)){
// Displays a chat message if a non-op tries to modify another player's allies.
- TextComponentTranslation TextComponentTranslation2 = new TextComponentTranslation(
- "commands." + Wizardry.MODID + ":ally.permission");
- TextComponentTranslation2.getStyle().setColor(TextFormatting.RED);
- allyOf.sendMessage(TextComponentTranslation2);
+ if(server.sendCommandFeedback()){
+ TextComponentTranslation TextComponentTranslation2 = new TextComponentTranslation(
+ "commands." + Wizardry.MODID + ":ally.permission");
+ TextComponentTranslation2.getStyle().setColor(TextFormatting.RED);
+ allyOf.sendMessage(TextComponentTranslation2);
+ }
return;
}
@@ -102,15 +99,17 @@ public class CommandSetAlly extends CommandBase {
if(allyOf == ally) throw new NumberInvalidException("commands." + Wizardry.MODID + ":ally.self");
- if(WizardData.get(allyOf) != null){
- String string = WizardData.get(allyOf).toggleAlly(ally) ? "add" : "remove";
- if(executeAsOtherPlayer){
- sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":ally." + string + "ally",
- ally.getName(), allyOf.getName()));
- // In this case, the player whose allies have been modified is also notified.
- allyOf.sendMessage(new TextComponentTranslation("item.wand." + string + "ally", ally.getName()));
- }else{
- sender.sendMessage(new TextComponentTranslation("item.wand." + string + "ally", ally.getName()));
+ if(server.sendCommandFeedback()){
+ if(WizardData.get(allyOf) != null){
+ String string = WizardData.get(allyOf).toggleAlly(ally) ? "add" : "remove";
+ if(executeAsOtherPlayer){
+ sender.sendMessage(new TextComponentTranslation("commands." + Wizardry.MODID + ":ally." + string + "ally",
+ ally.getName(), allyOf.getName()));
+ // In this case, the player whose allies have been modified is also notified.
+ allyOf.sendMessage(new TextComponentTranslation("item.wand." + string + "ally", ally.getName()));
+ }else{
+ sender.sendMessage(new TextComponentTranslation("item.wand." + string + "ally", ally.getName()));
+ }
}
}
diff --git a/src/main/java/electroblob/wizardry/command/CommandViewAllies.java b/src/main/java/electroblob/wizardry/command/CommandViewAllies.java
index 3bb82a2b..dde5e46d 100644
--- a/src/main/java/electroblob/wizardry/command/CommandViewAllies.java
+++ b/src/main/java/electroblob/wizardry/command/CommandViewAllies.java
@@ -1,12 +1,8 @@
package electroblob.wizardry.command;
-import java.util.List;
-import java.util.Set;
-
-import electroblob.wizardry.WizardData;
import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.data.WizardData;
import electroblob.wizardry.util.WizardryUtilities;
-import net.minecraft.client.resources.I18n;
import net.minecraft.command.CommandBase;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
@@ -18,6 +14,9 @@ import net.minecraft.util.math.BlockPos;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;
+import java.util.List;
+import java.util.Set;
+
public class CommandViewAllies extends CommandBase {
@Override
@@ -75,10 +74,12 @@ public class CommandViewAllies extends CommandBase {
if(player != sender && sender instanceof EntityPlayer
&& !WizardryUtilities.isPlayerOp((EntityPlayer)sender, server)){
// Displays a chat message if a non-op tries to view another player's allies.
- TextComponentTranslation TextComponentTranslation2 = new TextComponentTranslation(
- "commands." + Wizardry.MODID + ":allies.permission");
- TextComponentTranslation2.getStyle().setColor(TextFormatting.RED);
- player.sendMessage(TextComponentTranslation2);
+ if(server.sendCommandFeedback()){
+ TextComponentTranslation TextComponentTranslation2 = new TextComponentTranslation(
+ "commands." + Wizardry.MODID + ":allies.permission");
+ TextComponentTranslation2.getStyle().setColor(TextFormatting.RED);
+ player.sendMessage(TextComponentTranslation2);
+ }
return;
}
@@ -101,6 +102,7 @@ public class CommandViewAllies extends CommandBase {
playerList = new TextComponentTranslation("commands." + Wizardry.MODID + ":allies.none");
}
+ // Ignore sendCommandFeedback here since that's the entire point of this command
if(executeAsOtherPlayer){
sender.sendMessage(
new TextComponentTranslation("commands." + Wizardry.MODID + ":allies.list_other", player.getName(), playerList));
diff --git a/src/main/java/electroblob/wizardry/command/SpellEmitter.java b/src/main/java/electroblob/wizardry/command/SpellEmitter.java
new file mode 100644
index 00000000..7a09e6b0
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/command/SpellEmitter.java
@@ -0,0 +1,177 @@
+package electroblob.wizardry.command;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.data.SpellEmitterData;
+import electroblob.wizardry.event.SpellCastEvent;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.util.SpellModifiers;
+import io.netty.buffer.ByteBuf;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.ITickable;
+import net.minecraft.world.World;
+import net.minecraftforge.common.MinecraftForge;
+
+/**
+ * A {@code SpellEmitter} represents a continuous spell being cast from a position via commands.
+ *
+ * @since Wizardry 4.2
+ * @author Electroblob
+ */
+public class SpellEmitter implements ITickable {
+
+ protected final Spell spell;
+ protected World world;
+ protected final double x, y, z;
+ protected final EnumFacing direction;
+ protected final int duration;
+ protected final SpellModifiers modifiers;
+
+ protected int castingTick = 0;
+ protected boolean needsRemoving = false;
+
+ protected SpellEmitter(Spell spell, World world, double x, double y, double z, EnumFacing direction, int duration, SpellModifiers modifiers){
+ this.spell = spell;
+ this.world = world;
+ this.duration = duration;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.direction = direction;
+ this.modifiers = modifiers;
+ }
+
+ /** Marks this spell emitter to be removed next tick. */
+ protected void markForRemoval(){
+ this.needsRemoving = true;
+ }
+
+ /** Returns whether this spell emitter is marked for removal. */
+ public boolean needsRemoving(){
+ return needsRemoving;
+ }
+
+ /** Returns the {@link SpellCastEvent.Source} that should be used for events fired by this spell emitter. */
+ protected SpellCastEvent.Source getSource(){
+ return SpellCastEvent.Source.COMMAND;
+ }
+
+ /** Sets this spell emitter's world. This should only be used on the client side when the world has not yet been
+ * set, otherwise the world will not be changed and a warning will be printed to the console. */
+ public void setWorld(World world){
+ if(world.isRemote && this.world == null){
+ this.world = world;
+ }else{
+ Wizardry.logger.warn("Tried to change the world for a spell emitter, this shouldn't happen!");
+ }
+ }
+
+ @Override
+ public void update(){
+
+ if(castingTick < duration){
+
+ if(!MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(getSource(), spell, world, x, y, z, direction, modifiers, castingTick))){
+
+ if(spell.cast(world, x, y, z, direction, castingTick, duration, modifiers)){
+ if(castingTick == 0) MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Post(getSource(), spell, world, x, y, z, direction, modifiers));
+ castingTick++;
+ return;
+ }
+ }
+ }
+ // If the time ran out or the spell failed, interrupt spell casting
+ MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Finish(getSource(), spell, world, x, y, z, direction, modifiers, castingTick));
+ spell.finishCasting(world, null, x, y, z, direction, duration, modifiers);
+ markForRemoval();
+ }
+
+ /** Writes this {@code SpellEmitter} to the given ByteBuf. */
+ public void write(ByteBuf buf){
+ buf.writeInt(spell.networkID());
+ buf.writeDouble(x);
+ buf.writeDouble(y);
+ buf.writeDouble(z);
+ buf.writeInt(direction.getIndex());
+ // This is sent through as the duration, meaning castingTick always starts at zero client-side, which is
+ // important for sounds to work correctly. As a consequence, the client's castingTick will be different to the
+ // server value if the player changes dimension or re-logs. However, since this is pretty uncommon anyway I
+ // think it's an ok compromise.
+ buf.writeInt(duration - castingTick);
+ modifiers.write(buf);
+ }
+
+ /** Reads a {@code SpellEmitter} from the given ByteBuf and returns it. */
+ public static SpellEmitter read(ByteBuf buf){
+
+ Spell spell = Spell.byNetworkID(buf.readInt());
+ double x = buf.readDouble();
+ double y = buf.readDouble();
+ double z = buf.readDouble();
+ EnumFacing direction = EnumFacing.byIndex(buf.readInt());
+ int duration = buf.readInt();
+ SpellModifiers modifiers = new SpellModifiers();
+ modifiers.read(buf);
+
+ return new SpellEmitter(spell, null, x, y, z, direction, duration, modifiers);
+ }
+
+ // INBTSerializable is annoying, it doesn't allow you to have final fields
+
+ /** Returns a new {@link NBTTagCompound} representing this {@code SpellEmitter}. */
+ public NBTTagCompound toNBT(){
+
+ NBTTagCompound nbt = new NBTTagCompound();
+
+ nbt.setInteger("spell", spell.metadata());
+ nbt.setDouble("x", x);
+ nbt.setDouble("y", y);
+ nbt.setDouble("z", z);
+ nbt.setInteger("direction", direction.getIndex());
+ nbt.setInteger("duration", duration);
+ nbt.setTag("modifiers", modifiers.toNBT());
+ nbt.setInteger("castingTick", castingTick);
+
+ return nbt;
+ }
+
+ /** Creates a new {@code SpellEmitter} from the given {@link NBTTagCompound} and returns it. */
+ public static SpellEmitter fromNBT(World world, NBTTagCompound nbt){
+
+ Spell spell = Spell.byMetadata(nbt.getInteger("spell"));
+ double x = nbt.getDouble("x");
+ double y = nbt.getDouble("y");
+ double z = nbt.getDouble("z");
+ EnumFacing direction = EnumFacing.byIndex(nbt.getInteger("direction"));
+ int duration = nbt.getInteger("duration");
+ SpellModifiers modifiers = SpellModifiers.fromNBT(nbt.getCompoundTag("modifiers"));
+ int castingTick = nbt.getInteger("castingTick");
+
+ SpellEmitter emitter = new SpellEmitter(spell, world, x, y, z, direction, duration, modifiers);
+ emitter.castingTick = castingTick;
+ return emitter;
+ }
+
+ /**
+ * Creates a new {@code SpellEmitter} and adds it to the list of active emitters in {@link SpellEmitterData}.
+ * This method does not perform any syncing.
+ *
+ * @param spell The spell to be cast
+ * @param world The world in which to cast the spell
+ * @param x The x-coordinate of the spell origin
+ * @param y The y-coordinate of the spell origin
+ * @param z The z-coordinate of the spell origin
+ * @param direction The direction to cast the spell in
+ * @param duration The number of ticks to cast the spell for
+ * @param modifiers The {@link SpellModifiers} for the spell
+ */
+ public static void add(Spell spell, World world, double x, double y, double z, EnumFacing direction, int duration, SpellModifiers modifiers){
+ if(spell.isContinuous){
+ if(duration <= 0) Wizardry.logger.warn("Adding a spell emitter with negative or zero duration!");
+ SpellEmitterData.get(world).add(new SpellEmitter(spell, world, x, y, z, direction, duration, modifiers));
+ }else{
+ Wizardry.logger.warn("Tried to add a non-continuous spell emitter for spell {}", spell.getRegistryName());
+ }
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/constants/Constants.java b/src/main/java/electroblob/wizardry/constants/Constants.java
index 28bba9a9..0316b4e3 100644
--- a/src/main/java/electroblob/wizardry/constants/Constants.java
+++ b/src/main/java/electroblob/wizardry/constants/Constants.java
@@ -5,23 +5,25 @@ import electroblob.wizardry.WizardryEventHandler;
/** Stores various global constants used in Wizardry. */
public final class Constants {
+ /** The amount of mana a crystal shard is worth */
+ // 100 doesn't divide nicely by 9 so we're calling this 10. I guess you lose a little bit by smashing a crystal.
+ public static final int MANA_PER_SHARD = 10;
/** The amount of mana each magic crystal is worth */
public static final int MANA_PER_CRYSTAL = 100;
- /** The amount of mana each mana flask can hold */
- public static final int MANA_PER_FLASK = 700;
+ /** The amount of mana a grand magic crystal is worth */
+ public static final int GRAND_CRYSTAL_MANA = 400;
/** The maximum number of one type of wand upgrade which can be applied to a wand. */
public static final int UPGRADE_STACK_LIMIT = 3;
/** The fraction by which cooldowns are reduced for each level of cooldown upgrade. */
public static final float COOLDOWN_REDUCTION_PER_LEVEL = 0.15f;
/** The fraction by which maximum charge is increased for each level of storage upgrade. */
public static final float STORAGE_INCREASE_PER_LEVEL = 0.15f;
- /** The fraction by which damage is increased for each tier of matching wand. */
- public static final float DAMAGE_INCREASE_PER_TIER = 0.15f;
- /**
- * The fraction by which costs are reduced for each piece of matching armour. Note that changing this value will not
- * affect continuous spells, since they are handled differently.
- */
- public static final float COST_REDUCTION_PER_ARMOUR = 0.2f;
+ /** The fraction by which potency is increased for each tier of matching wand. */
+ public static final float POTENCY_INCREASE_PER_TIER = 0.15f;
+ /** The fraction by which costs are reduced for each piece of matching armour. */
+ public static final float COST_REDUCTION_PER_ARMOUR = 0.15f;
+ /** The extra fraction by which costs are reduced for a full set of elemental armour. */
+ public static final float FULL_ARMOUR_SET_BONUS = 0.2f;
/** The fraction by which spell duration is increased for each level of duration upgrade. */
public static final float DURATION_INCREASE_PER_LEVEL = 0.25f;
/** The fraction by which spell range is increased for each level of range upgrade. */
@@ -32,7 +34,7 @@ public final class Constants {
public static final double FROST_SLOWNESS_PER_LEVEL = 0.5;
/** The fraction by which movement speed is reduced per level of decay effect. */
public static final double DECAY_SLOWNESS_PER_LEVEL = 0.2;
- /** The fraction by which dig speed is reduced per level of frost effect. */
+ /** The fraction by which dig speed is reduced per level of frostbite effect. */
public static final float FROST_FATIGUE_PER_LEVEL = 0.45f;
/** The number of ticks between each mana increase for wands with the condenser upgrade. */
public static final int CONDENSER_TICK_INTERVAL = 50;
@@ -40,11 +42,13 @@ public final class Constants {
* The amount of mana given for a kill for each level of siphon upgrade. A random amount from 0 to this number - 1
* is also added. See {@link WizardryEventHandler#onLivingDeathEvent} for more details.
*/
- public static final int SIPHON_MANA_PER_LEVEL = 3;
+ public static final int SIPHON_MANA_PER_LEVEL = 5;
/**
* The number of ticks between the spawning of patches of decay when an entity has the decay effect. Note that decay
* won't spawn again if something is already standing in it.
*/
public static final int DECAY_SPREAD_INTERVAL = 8;
+ /** The fraction by which potency is increased per level of the empowerment effect. */
+ public static final float EMPOWERMENT_POTENCY_PER_LEVEL = 0.25f;
}
diff --git a/src/main/java/electroblob/wizardry/constants/Element.java b/src/main/java/electroblob/wizardry/constants/Element.java
index dd357774..7b433693 100644
--- a/src/main/java/electroblob/wizardry/constants/Element.java
+++ b/src/main/java/electroblob/wizardry/constants/Element.java
@@ -1,7 +1,7 @@
package electroblob.wizardry.constants;
import electroblob.wizardry.Wizardry;
-import net.minecraft.client.resources.I18n;
+import net.minecraft.util.IStringSerializable;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.Style;
@@ -10,19 +10,18 @@ import net.minecraft.util.text.TextFormatting;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
-public enum Element {
+public enum Element implements IStringSerializable {
- /**
- * The 'default' element, with {@link electroblob.wizardry.spell.MagicMissile MagicMissile} being its only spell.
- */
- MAGIC(new Style().setColor(TextFormatting.GRAY), "simple", Wizardry.MODID),
- FIRE(new Style().setColor(TextFormatting.DARK_RED), "fire", Wizardry.MODID),
- ICE(new Style().setColor(TextFormatting.AQUA), "ice", Wizardry.MODID),
- LIGHTNING(new Style().setColor(TextFormatting.DARK_AQUA), "lightning", Wizardry.MODID),
- NECROMANCY(new Style().setColor(TextFormatting.DARK_PURPLE), "necromancy", Wizardry.MODID),
- EARTH(new Style().setColor(TextFormatting.DARK_GREEN), "earth", Wizardry.MODID),
- SORCERY(new Style().setColor(TextFormatting.GREEN), "sorcery", Wizardry.MODID),
- HEALING(new Style().setColor(TextFormatting.YELLOW), "healing", Wizardry.MODID);
+ /** The 'default' element, with {@link electroblob.wizardry.registry.Spells#magic_missile magic missile} being its
+ * only spell. */
+ MAGIC(new Style().setColor(TextFormatting.GRAY), "magic"),
+ FIRE(new Style().setColor(TextFormatting.DARK_RED), "fire"),
+ ICE(new Style().setColor(TextFormatting.AQUA), "ice"),
+ LIGHTNING(new Style().setColor(TextFormatting.DARK_AQUA), "lightning"),
+ NECROMANCY(new Style().setColor(TextFormatting.DARK_PURPLE), "necromancy"),
+ EARTH(new Style().setColor(TextFormatting.DARK_GREEN), "earth"),
+ SORCERY(new Style().setColor(TextFormatting.GREEN), "sorcery"),
+ HEALING(new Style().setColor(TextFormatting.YELLOW), "healing");
/** Display colour for this element */
private final Style colour;
@@ -31,16 +30,31 @@ public enum Element {
/** The {@link ResourceLocation} for this element's 8x8 icon (displayed in the arcane workbench GUI) */
private final ResourceLocation icon;
- private Element(Style colour, String name, String modid){
+ Element(Style colour, String name){
+ this(colour, name, Wizardry.MODID);
+ }
+
+ Element(Style colour, String name, String modid){
this.colour = colour;
this.unlocalisedName = name;
this.icon = new ResourceLocation(modid, "textures/gui/element_icon_" + unlocalisedName + ".png");
}
+ /** Returns the element with the given name, or throws an {@link java.lang.IllegalArgumentException} if no such
+ * element exists. */
+ public static Element fromName(String name){
+
+ for(Element element : values()){
+ if(element.unlocalisedName.equals(name)) return element;
+ }
+
+ throw new IllegalArgumentException("No such element with unlocalised name: " + name);
+ }
+
/** Returns the translated display name of this element, without formatting. */
@SideOnly(Side.CLIENT)
public String getDisplayName(){
- return I18n.format("element." + getUnlocalisedName());
+ return net.minecraft.client.resources.I18n.format("element." + getName());
}
/** Returns the {@link Style} object representing the colour of this element. */
@@ -55,11 +69,12 @@ public enum Element {
/** Returns the translated display name for wizards of this element, shown in the trading GUI. */
public ITextComponent getWizardName(){
- return new TextComponentTranslation("element." + getUnlocalisedName() + ".wizard");
+ return new TextComponentTranslation("element." + getName() + ".wizard");
}
- /** Returns this element's unlocalised name. */
- public String getUnlocalisedName(){
+ /** Returns this element's unlocalised name. Also used as the serialised string in block properties. */
+ @Override
+ public String getName(){
return unlocalisedName;
}
diff --git a/src/main/java/electroblob/wizardry/constants/SpellType.java b/src/main/java/electroblob/wizardry/constants/SpellType.java
index 5f58dede..afefec7f 100644
--- a/src/main/java/electroblob/wizardry/constants/SpellType.java
+++ b/src/main/java/electroblob/wizardry/constants/SpellType.java
@@ -1,12 +1,18 @@
package electroblob.wizardry.constants;
-import net.minecraft.client.resources.I18n;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
public enum SpellType {
- ATTACK("attack"), DEFENCE("defence"), UTILITY("utility"), MINION("minion");
+ ATTACK("attack"),
+ DEFENCE("defence"),
+ UTILITY("utility"),
+ MINION("minion"),
+ BUFF("buff"),
+ CONSTRUCT("construct"),
+ PROJECTILE("projectile"),
+ ALTERATION("alteration");
private final String unlocalisedName;
@@ -14,8 +20,23 @@ public enum SpellType {
this.unlocalisedName = name;
}
+ /** Returns the spell type with the given name, or throws an {@link java.lang.IllegalArgumentException} if no such
+ * spell type exists. */
+ public static SpellType fromName(String name){
+
+ for(SpellType type : values()){
+ if(type.unlocalisedName.equals(name)) return type;
+ }
+
+ throw new IllegalArgumentException("No such spell type with unlocalised name: " + name);
+ }
+
+ public String getUnlocalisedName(){
+ return unlocalisedName;
+ }
+
@SideOnly(Side.CLIENT)
public String getDisplayName(){
- return I18n.format("spelltype." + unlocalisedName);
+ return net.minecraft.client.resources.I18n.format("spelltype." + unlocalisedName);
}
}
\ No newline at end of file
diff --git a/src/main/java/electroblob/wizardry/constants/Tier.java b/src/main/java/electroblob/wizardry/constants/Tier.java
index 6c7d8958..5cf6bdd0 100644
--- a/src/main/java/electroblob/wizardry/constants/Tier.java
+++ b/src/main/java/electroblob/wizardry/constants/Tier.java
@@ -1,19 +1,20 @@
package electroblob.wizardry.constants;
-import java.util.Random;
-
-import net.minecraft.client.resources.I18n;
+import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.Style;
+import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
+import java.util.Random;
+
public enum Tier {
- BASIC(700, 3, 12, new Style().setColor(TextFormatting.WHITE), "basic"), APPRENTICE(1000, 4, 5,
- new Style().setColor(TextFormatting.AQUA), "apprentice"), ADVANCED(1500, 5, 2,
- new Style().setColor(TextFormatting.DARK_BLUE),
- "advanced"), MASTER(2500, 6, 1, new Style().setColor(TextFormatting.DARK_PURPLE), "master");
+ NOVICE(700, 3, 12, 0, new Style().setColor(TextFormatting.WHITE), "novice"),
+ APPRENTICE(1000, 5, 5, 6000, new Style().setColor(TextFormatting.AQUA), "apprentice"),
+ ADVANCED(1500, 7, 2, 9000, new Style().setColor(TextFormatting.DARK_BLUE), "advanced"),
+ MASTER(2500, 9, 1, 15000, new Style().setColor(TextFormatting.DARK_PURPLE), "master");
/** Maximum mana a wand of this tier can store. */
public final int maxCharge;
@@ -23,29 +24,59 @@ public enum Tier {
public final int upgradeLimit;
/** The weight given to this tier in the standard weighting. */
public final int weight;
+ /** The progression required for a wand to be upgraded to this tier. */
+ public final int progression;
/** The colour of text associated with this tier. */
// Changed to a Style object for consistency.
private final Style colour;
private final String unlocalisedName;
- private Tier(int maxCharge, int upgradeLimit, int weight, Style colour, String name){
+ Tier(int maxCharge, int upgradeLimit, int weight, int progression, Style colour, String name){
this.maxCharge = maxCharge;
this.level = ordinal();
this.upgradeLimit = upgradeLimit;
this.weight = weight;
+ this.progression = progression;
this.colour = colour;
this.unlocalisedName = name;
}
+ /** Returns the tier with the given name, or throws an {@link java.lang.IllegalArgumentException} if no such
+ * tier exists. */
+ public static Tier fromName(String name){
+
+ for(Tier tier : values()){
+ if(tier.unlocalisedName.equals(name)) return tier;
+ }
+
+ throw new IllegalArgumentException("No such tier with unlocalised name: " + name);
+ }
+
@SideOnly(Side.CLIENT)
public String getDisplayName(){
- return I18n.format("tier." + unlocalisedName);
+ return net.minecraft.client.resources.I18n.format("tier." + unlocalisedName);
+ }
+
+ /**
+ * Returns a {@code TextComponentTranslation} which will be translated to the display name of the tier, without
+ * formatting (i.e. not coloured).
+ */
+ public TextComponentTranslation getNameForTranslation(){
+ return new TextComponentTranslation("tier." + unlocalisedName);
}
@SideOnly(Side.CLIENT)
public String getDisplayNameWithFormatting(){
- return this.getFormattingCode() + I18n.format("tier." + unlocalisedName);
+ return this.getFormattingCode() + net.minecraft.client.resources.I18n.format("tier." + unlocalisedName);
+ }
+
+ /**
+ * Returns a {@code TextComponentTranslation} which will be translated to the display name of the tier, with
+ * formatting (i.e. coloured).
+ */
+ public ITextComponent getNameForTranslationFormatted(){
+ return new TextComponentTranslation("tier." + unlocalisedName).setStyle(this.colour);
}
public String getUnlocalisedName(){
@@ -68,8 +99,7 @@ public enum Tier {
int totalWeight = 0;
- for(Tier tier : tiers)
- totalWeight += tier.weight;
+ for(Tier tier : tiers) totalWeight += tier.weight;
int randomiser = random.nextInt(totalWeight);
int cumulativeWeight = 0;
diff --git a/src/main/java/electroblob/wizardry/data/BlockCastingData.java b/src/main/java/electroblob/wizardry/data/BlockCastingData.java
new file mode 100644
index 00000000..a02d2de7
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/data/BlockCastingData.java
@@ -0,0 +1,183 @@
+package electroblob.wizardry.data;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.event.SpellCastEvent;
+import electroblob.wizardry.packet.PacketDispenserCastSpell;
+import electroblob.wizardry.packet.WizardryPacketHandler;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.spell.None;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.util.SpellModifiers;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.util.EnumFacing;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.util.INBTSerializable;
+import net.minecraftforge.fml.common.network.simpleimpl.IMessage;
+
+/**
+ * Base class for {@link DispenserCastingData}. Originally this was written because command blocks had a similar system,
+ * but that was later removed in favour of spell emitters - however, this class has been kept so that others can use it
+ * for different spellcasting blocks if they wish.
+ *
+ * @since Wizardry 4.2
+ * @author Electroblob
+ */
+public abstract class BlockCastingData implements INBTSerializable {
+
+ /** The tile entity this BlockCastingData instance belongs to. */
+ protected final T tileEntity;
+
+ /** The continuous spell this tile entity is currently casting, or the {@link None} spell if it is not casting. */
+ protected Spell spell;
+ /** The coordinates of the current continuous spell's origin. */
+ protected double x, y, z;
+ /** The time for which this tile entity has been casting a continuous spell. Increments by 1 each tick. */
+ protected int castingTick;
+ /** SpellModifiers object for the current continuous spell. */
+ protected SpellModifiers modifiers;
+
+ public BlockCastingData(T tileEntity){
+ this.tileEntity = tileEntity;
+ this.spell = Spells.none;
+ this.modifiers = new SpellModifiers();
+ this.castingTick = 0;
+ }
+
+ /** Returns whether this tile entity is currently casting a continuous spell. */
+ public boolean isCasting(){
+ return this.spell != null && this.spell != Spells.none;
+ }
+
+ /** Returns the continuous spell this tile entity is currently casting, or the {@link None} spell if it isn't
+ * casting anything. */
+ public Spell currentlyCasting(){
+ return spell;
+ }
+
+ /** Starts casting the given continuous spell from this tile entity. */
+ protected void startCasting(Spell spell, double x, double y, double z, SpellModifiers modifiers){
+
+ if(!spell.isContinuous){
+ Wizardry.logger.warn("Tried to start casting a continuous spell from a tile entity, but the given spell was not continuous!");
+ return;
+ }
+
+ this.spell = spell;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.castingTick = 0;
+ this.modifiers = modifiers;
+ }
+
+ /** Stops casting the current spell. */
+ protected void stopCasting(){
+ this.spell = Spells.none;
+ this.castingTick = 0;
+ this.modifiers.reset();
+ }
+
+ /** Stops casting the current spell and sends a packet to clients to update them. If called client-side, this just
+ * delegates to {@link BlockCastingData#stopCasting()}. */
+ protected void stopCastingAndNotify(){
+
+ stopCasting();
+
+ if(!tileEntity.getWorld().isRemote){
+ IMessage msg = new PacketDispenserCastSpell.Message(x, y, z, getDirection(), tileEntity.getPos(), spell, 0, modifiers);
+ WizardryPacketHandler.net.sendToDimension(msg, tileEntity.getWorld().provider.getDimension());
+ }
+ }
+
+ /** Called once per tick to update the block casting data. This is not called automatically , subclasses must
+ * do so using their own tick event handlers. */
+ protected void update(){
+
+ if(this.tileEntity.isInvalid()){
+ return;
+ }
+
+ if(this.isCasting() && this.spell.isContinuous){
+
+ // If the dispenser has stopped receiving power, the spell stops immediately.
+ if(!shouldContinueCasting()){
+ this.stopCasting(); // This seems to work fine on both sides, so no point sending a packet
+ return;
+ }
+
+ EnumFacing direction = getDirection();
+
+ if(MinecraftForge.EVENT_BUS.post(new SpellCastEvent.Tick(getSource(), spell, tileEntity.getWorld(),
+ x, y, z, direction, modifiers, castingTick))){
+ // When the event is canceled client-side, this will stop the spell on the client only, as specified in
+ // the javadoc for SpellCastEvent.Tick.
+ this.stopCastingAndNotify();
+ return;
+ }
+
+ this.spell.cast(tileEntity.getWorld(), x, y, z, direction, castingTick, -1, modifiers);
+
+ castingTick++;
+
+ }else{
+ this.castingTick = 0;
+ }
+ }
+
+ /** Returns the direction to cast the current spell in. */
+ protected abstract EnumFacing getDirection();
+
+ /** Returns the source of spells cast from this block. */
+ protected abstract SpellCastEvent.Source getSource();
+
+ /** Called each tick during continuous spell casting to determine if the spell should continue or stop. */
+ protected abstract boolean shouldContinueCasting();
+
+ @Override
+ public NBTTagCompound serializeNBT(){
+
+ NBTTagCompound nbt = new NBTTagCompound();
+
+ nbt.setInteger("spell", spell.metadata());
+ nbt.setInteger("castingTick", castingTick);
+ nbt.setTag("modifiers", modifiers.toNBT());
+
+ return nbt;
+ }
+
+ @Override
+ public void deserializeNBT(NBTTagCompound nbt){
+
+ if(nbt != null){
+
+ this.spell = Spell.byMetadata(nbt.getInteger("spell"));
+ this.castingTick = nbt.getInteger("castingTick");
+ this.modifiers = SpellModifiers.fromNBT(nbt.getCompoundTag("modifiers"));
+ }
+ }
+
+ // The two methods below broke EVERYTHING, somehow they made the server think it was the client...
+
+// // Only fired server-side
+// @SubscribeEvent
+// public static void onWorldTickEvent(TickEvent.WorldTickEvent event){
+//
+// if(!event.world.isRemote && event.phase == TickEvent.Phase.END){
+// // This will fire once for each dimension, but since we want dispenser-casting to work in all dimensions,
+// // this is correct (the loaded tile entity list will of course be different in each case.
+// this.update();
+// }
+// }
+//
+// // Only called client-side
+// @SubscribeEvent
+// public static void onClientTickEvent(TickEvent.ClientTickEvent event){
+// World world = net.minecraft.client.Minecraft.getMinecraft().world;
+// if(event.phase == TickEvent.Phase.END && !net.minecraft.client.Minecraft.getMinecraft().isGamePaused()
+// && world != null){
+// this.update();
+// }
+// }
+
+}
diff --git a/src/main/java/electroblob/wizardry/data/DispenserCastingData.java b/src/main/java/electroblob/wizardry/data/DispenserCastingData.java
new file mode 100644
index 00000000..28d0fe56
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/data/DispenserCastingData.java
@@ -0,0 +1,219 @@
+package electroblob.wizardry.data;
+
+import electroblob.wizardry.Wizardry;
+import electroblob.wizardry.event.SpellCastEvent.Source;
+import electroblob.wizardry.item.ItemScroll;
+import electroblob.wizardry.registry.Spells;
+import electroblob.wizardry.spell.Spell;
+import electroblob.wizardry.util.SpellModifiers;
+import net.minecraft.block.BlockDispenser;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.NBTTagCompound;
+import net.minecraft.tileentity.TileEntity;
+import net.minecraft.tileentity.TileEntityDispenser;
+import net.minecraft.util.EnumFacing;
+import net.minecraft.util.ResourceLocation;
+import net.minecraftforge.common.capabilities.Capability;
+import net.minecraftforge.common.capabilities.Capability.IStorage;
+import net.minecraftforge.common.capabilities.CapabilityInject;
+import net.minecraftforge.common.capabilities.CapabilityManager;
+import net.minecraftforge.common.capabilities.ICapabilitySerializable;
+import net.minecraftforge.event.AttachCapabilitiesEvent;
+import net.minecraftforge.fml.common.Mod;
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
+import net.minecraftforge.fml.common.gameevent.TickEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Internal capability for attaching data to dispensers. The sole purpose of this class is to keep track of continuous
+ * spell casting for dispensers.
+ *
+ * Forge seems to have separate classes to hold the Capability<...> instance ('key') and methods for getting the
+ * capability, but in my opinion there are already too many classes to deal with, so I'm not adding any more than are
+ * necessary, meaning those constants and values are kept here instead.
+ *
+ * @since Wizardry 4.2
+ * @author Electroblob
+ */
+@Mod.EventBusSubscriber
+public class DispenserCastingData extends BlockCastingData {
+
+ /** Static instance of what I like to refer to as the capability key. Private because, well, it's internal! */
+ // This annotation does some crazy Forge magic behind the scenes and assigns this field a value.
+ @CapabilityInject(DispenserCastingData.class)
+ private static final Capability DISPENSER_CASTING_CAPABILITY = null;
+
+ /** The time for which this dispenser will continue casting a continuous spell. When castingTick exceeds this value,
+ * the dispenser will either stop casting or, if it contains more of the same type of scroll, continue casting and
+ * increase this value by the duration that the spell should be cast for. */
+ private int duration;
+
+ public DispenserCastingData(){
+ this(null); // Nullary constructor for the registration method factory parameter
+ }
+
+ public DispenserCastingData(TileEntityDispenser dispenser){
+ super(dispenser);
+ }
+
+ /** Starts casting the given continuous spell from this dispenser. */
+ public void startCasting(Spell spell, double x, double y, double z, int duration, SpellModifiers modifiers){
+ startCasting(spell, x, y, z, modifiers);
+ this.castingTick = 1; // 1 because we already cast it once in BehaviourSpellDispense
+ this.duration = duration;
+ }
+
+ @Override
+ public void stopCasting(){
+ super.stopCasting();
+ }
+
+ @Override
+ protected Source getSource(){
+ return Source.DISPENSER;
+ }
+
+ @Override
+ protected EnumFacing getDirection(){
+ return tileEntity.getWorld().getBlockState(tileEntity.getPos()).getValue(BlockDispenser.FACING);
+ }
+
+ @Override
+ protected boolean shouldContinueCasting(){
+ return tileEntity.getWorld().isBlockPowered(tileEntity.getPos());
+ }
+
+ @Override
+ public void update(){
+
+ super.update();
+
+ // Check whether enough scrolls are left
+ if(this.isCasting() && this.spell.isContinuous){
+
+ if(castingTick > duration && !tileEntity.getWorld().isRemote){
+
+ if(findNewScroll()){
+ duration += ItemScroll.CASTING_TIME; // Best way to do it for now.
+ }else{
+ this.stopCastingAndNotify();
+ }
+ }
+ }
+ }
+
+ /** Searches through the dispenser's inventory for a new stack of scrolls of the same spell that is currently being
+ * cast and returns true if at least one such stack is found. Also consumes one scroll if a stack is found; if more
+ * than one applicable stack is found then one will be chosen at random. */
+ private boolean findNewScroll(){
+
+ if(spell == Spells.none) return false;
+
+ List slots = new ArrayList();
+
+ for(int i = 0; i < tileEntity.getSizeInventory(); i++){
+ ItemStack stack = tileEntity.getStackInSlot(i);
+ if(stack.getItem() instanceof ItemScroll && stack.getMetadata() == spell.metadata()) slots.add(i);
+ }
+
+ if(slots.isEmpty()) return false; // If no stack was found that matched the current spell
+
+ tileEntity.decrStackSize(slots.get(tileEntity.getWorld().rand.nextInt(slots.size())), 1); // Consumes 1 scroll
+ return true;
+ }
+
+ /** Returns the DispenserCastingData instance for the specified dispenser. */
+ public static DispenserCastingData get(TileEntityDispenser dispenser){
+ return dispenser.getCapability(DISPENSER_CASTING_CAPABILITY, null);
+ }
+
+ /** Called from preInit in the main mod class to register the DispenserCastingData capability. */
+ public static void register(){
+
+ CapabilityManager.INSTANCE.register(DispenserCastingData.class, new IStorage(){
+
+ @Override
+ public NBTBase writeNBT(Capability capability, DispenserCastingData instance, EnumFacing side){
+ return null;
+ }
+
+ @Override
+ public void readNBT(Capability capability, DispenserCastingData instance, EnumFacing side, NBTBase nbt){}
+
+ }, DispenserCastingData::new);
+ }
+
+ // Event handlers
+
+ @SubscribeEvent
+ // The type parameter here has to be SoundLoopSpellDispenser, not TileEntityDispenser, or the event won't get fired.
+ public static void onCapabilityLoad(AttachCapabilitiesEvent event){
+
+ if(event.getObject() instanceof TileEntityDispenser)
+ event.addCapability(new ResourceLocation(Wizardry.MODID, "casting_data"),
+ new DispenserCastingData.Provider((TileEntityDispenser)event.getObject()));
+ }
+
+ // Only fired server-side
+ @SubscribeEvent
+ public static void onWorldTickEvent(TickEvent.WorldTickEvent event){
+
+ if(event.phase == TickEvent.Phase.END){
+
+ // This will fire once for each dimension, but since we want dispenser-casting to work in all dimensions,
+ // this is correct (the loaded tile entity list will of course be different in each case.
+
+ for(TileEntity tileentity : event.world.loadedTileEntityList){
+ if(tileentity instanceof TileEntityDispenser){
+ if(DispenserCastingData.get((TileEntityDispenser)tileentity) != null){
+ DispenserCastingData.get((TileEntityDispenser)tileentity).update();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * This is a nested class for a few reasons: firstly, it makes sense because instances of this and
+ * DispenserCastingData go hand-in-hand; secondly, it's too short to be worth a separate file; and thirdly (and most
+ * importantly) it allows me to access DISPENSER_CASTING_CAPABILITY while keeping it private.
+ */
+ public static class Provider implements ICapabilitySerializable {
+
+ private final DispenserCastingData data;
+
+ public Provider(TileEntityDispenser dispenser){
+ data = new DispenserCastingData(dispenser);
+ }
+
+ @Override
+ public boolean hasCapability(Capability> capability, EnumFacing facing){
+ return capability == DISPENSER_CASTING_CAPABILITY;
+ }
+
+ @Override
+ public T getCapability(Capability capability, EnumFacing facing){
+
+ if(capability == DISPENSER_CASTING_CAPABILITY){
+ return DISPENSER_CASTING_CAPABILITY.cast(data);
+ }
+
+ return null;
+ }
+
+ @Override
+ public NBTTagCompound serializeNBT(){
+ return data.serializeNBT();
+ }
+
+ @Override
+ public void deserializeNBT(NBTTagCompound nbt){
+ data.deserializeNBT(nbt);
+ }
+
+ }
+
+}
diff --git a/src/main/java/electroblob/wizardry/data/IStoredVariable.java b/src/main/java/electroblob/wizardry/data/IStoredVariable.java
new file mode 100644
index 00000000..f6594f57
--- /dev/null
+++ b/src/main/java/electroblob/wizardry/data/IStoredVariable.java
@@ -0,0 +1,256 @@
+package electroblob.wizardry.data;
+
+import io.netty.buffer.ByteBuf;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.nbt.*;
+import net.minecraft.util.math.BlockPos;
+import net.minecraftforge.fml.common.network.ByteBufUtils;
+
+import java.util.UUID;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * Extension of {@link IVariable} which adds NBT read/write methods. Instances of this interface must be
+ * registered on load using {@link WizardData#registerStoredVariables(IStoredVariable...)} in order for NBT storage
+ * to work. A good place to do this is in spell constructors, if that's where the variable is being used.
+ *
+ * This interface is provided for complex cases that require custom NBT handling of some kind. In most cases,
+ * {@link StoredVariable} should be sufficient.
+ *
+ * @param The type of variable stored.
+ */
+public interface IStoredVariable extends IVariable {
+
+ /** Writes the value to the given NBT tag. */
+ void write(NBTTagCompound nbt, T value);
+
+ /** Reads the value from the given NBT tag. */
+ T read(NBTTagCompound nbt);
+
+ /**
+ * General-purpose implementation of {@link IStoredVariable}. In most cases, this should be sufficient. This class
+ * also contains a number of static methods for common implementations (primitives, {@code String}, {@code UUID},
+ * {@code BlockPos} and {@code ItemStack}).
+ *
+ * @param The type of variable stored.
+ * @param The type of NBT tag the variable will be stored as.
+ */
+ class StoredVariable implements IStoredVariable {
+
+ private final String key;
+ private final Persistence persistence;
+
+ private final Function serialiser;
+ private final Function deserialiser;
+
+ private boolean synced;
+
+ private BiFunction ticker;
+
+ /**
+ * Creates a new {@code StoredVariable} with the given key and serialisation behaviour.
+ * @param key The string key used to write the value to NBT (should be unique). This serves no other purpose.
+ * @param serialiser A function used to write the value to NBT.
+ * @param deserialiser A function used to read the value from NBT.
+ */
+ public StoredVariable(String key, Function serialiser, Function deserialiser, Persistence persistence){
+ this.key = key;
+ this.serialiser = serialiser;
+ this.deserialiser = deserialiser;
+ this.persistence = persistence;
+ this.ticker = (p, t) -> t; // Initialise this with a do-nothing function, can be overwritten later
+ }
+
+ /**
+ * Replaces this variable's update method with the given update function. Beware of auto-unboxing of
+ * primitive types! For lambda expressions, check the second parameter isn't null before operating on it.
+ * For method references, do not reference a method that takes a primitive type. Otherwise, this will cause
+ * a (difficult to debug) {@link NullPointerException} if the key was not stored.
+ * @param ticker A {@link BiFunction} specifying the actions to be performed on this variable each tick. The
+ * {@code BiFunction} returns the new value for this variable.
+ * @return This {@code StoredVariable} object, allowing this method to be chained onto object creation.
+ */
+ public StoredVariable withTicker(BiFunction ticker){
+ this.ticker = ticker;
+ return this;
+ }
+
+ /**
+ * Adds synchronisation to this variable, meaning it will be sent to clients whenever {@link WizardData#sync()}
+ * is called (this always happens on player login, but other than that you'll need to do it yourself).
+ * @return This {@code StoredVariable} object, allowing this method to be chained onto object creation.
+ */
+ public StoredVariable setSynced(){
+ this.synced = true;
+ return this;
+ }
+
+ @Override
+ public void write(NBTTagCompound nbt, T value){
+ if(value != null) nbt.setTag(key, serialiser.apply(value));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked") // Can't check it due to type erasure
+ public T read(NBTTagCompound nbt){
+ // A system allowing any kind of variable to be stored on the fly cannot be made without casting somewhere.
+ // However, doing it like this means we only cast once, below, and proper regulation of access means we
+ // can effectively guarantee the cast is safe.
+ return nbt.hasKey(key) ? deserialiser.apply((E)nbt.getTag(key)) : null; // Still gotta check it ain't null
+ }
+
+ @Override
+ public T update(EntityPlayer player, T value){
+ return ticker.apply(player, value);
+ }
+
+ @Override
+ public boolean isPersistent(boolean respawn){
+ return respawn ? persistence.persistsOnRespawn() : persistence.persistsOnDimensionChange();
+ }
+
+ @Override
+ public boolean isSynced(){
+ return synced;
+ }
+
+ @Override
+ public void write(ByteBuf buf, T value){
+ if(!synced) return;
+ NBTTagCompound nbt = new NBTTagCompound();
+ write(nbt, value);
+ ByteBufUtils.writeTag(buf, nbt); // Sure, it's not super-efficient, but it's by far the simplest way!
+ }
+
+ @Override
+ public T read(ByteBuf buf){
+ if(!synced) return null; // Better to check in here because this method should only read if it needs to
+ NBTTagCompound nbt = ByteBufUtils.readTag(buf);
+ if(nbt == null) return null;
+ return read(nbt);
+ }
+
+ // Standard implementations to shorten common usages a bit
+
+ /** Creates a new {@code StoredVariable} for a byte value with the given key. */
+ public static StoredVariable ofByte(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagByte::new, NBTTagByte::getByte, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a boolean value with the given key. As per Minecraft's usual
+ * NBT conventions, the boolean value is stored as an {@link NBTTagByte} (1 = true, 0 = false). */
+ public static StoredVariable ofBoolean(String key, Persistence persistence){
+ return new StoredVariable<>(key, b -> new NBTTagByte((byte)(b?1:0)), t -> t.getByte() == 1, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for an integer value with the given key. */
+ public static StoredVariable ofInt(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagInt::new, NBTTagInt::getInt, persistence);
+ }
+
+ // I'm not going to do byte and long arrays here, if you really need them it's pretty obvious how to do it
+
+ /** Creates a new {@code StoredVariable} for an integer array value with the given key. */
+ public static StoredVariable ofIntArray(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagIntArray::new, NBTTagIntArray::getIntArray, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a float value with the given key. */
+ public static StoredVariable ofFloat(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagFloat::new, NBTTagFloat::getFloat, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a double value with the given key. */
+ public static StoredVariable ofDouble(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagDouble::new, NBTTagDouble::getDouble, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a short value with the given key. */
+ public static StoredVariable ofShort(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagShort::new, NBTTagShort::getShort, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a long value with the given key. */
+ public static StoredVariable ofLong(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagLong::new, NBTTagLong::getLong, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a {@link String} value with the given key. */
+ public static StoredVariable ofString(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTTagString::new, NBTTagString::getString, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a {@link BlockPos} value with the given key. */
+ public static StoredVariable ofBlockPos(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTUtil::createPosTag, NBTUtil::getPosFromTag, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for a {@link UUID} value with the given key. */
+ public static StoredVariable ofUUID(String key, Persistence persistence){
+ return new StoredVariable<>(key, NBTUtil::createUUIDTag, NBTUtil::getUUIDFromTag, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for an {@link ItemStack} value with the given key. */
+ public static StoredVariable ofItemStack(String key, Persistence persistence){
+ return new StoredVariable<>(key, ItemStack::serializeNBT, ItemStack::new, persistence);
+ }
+
+ /** Creates a new {@code StoredVariable} for an {@link NBTTagCompound} value with the given key. */
+ public static StoredVariable ofNBT(String key, Persistence persistence){
+ return new StoredVariable<>(key, t -> t, t -> t, persistence); // No conversion required!
+ }
+
+ // Neither of these work just ignore them
+
+// /** Creates a new {@code StoredVariable} for an {@link NBTTagCompound} value with the given key which stores the
+// * given {@code IVariable} for an entity. Entities cannot be stored directly as an {@code IStoredVariable}
+// * because they require a world instance on construction. */
+// @SuppressWarnings("unchecked") // Can't check it due to type erasure
+// public static