package com.teamwizardry.wizardry.api.spell; import com.google.common.collect.ArrayListMultimap; import com.teamwizardry.wizardry.Wizardry; import com.teamwizardry.wizardry.api.ConfigValues; import com.teamwizardry.wizardry.api.capability.player.mana.IManaCapability; import com.teamwizardry.wizardry.api.capability.player.mana.ManaCapabilityProvider; import com.teamwizardry.wizardry.api.capability.player.mana.ManaManager; import com.teamwizardry.wizardry.api.item.BaublesSupport; import com.teamwizardry.wizardry.api.spell.attribute.AttributeModifier; import com.teamwizardry.wizardry.api.spell.attribute.AttributeRange; import com.teamwizardry.wizardry.api.spell.attribute.AttributeRegistry; import com.teamwizardry.wizardry.api.spell.attribute.AttributeRegistry.Attribute; import com.teamwizardry.wizardry.api.spell.attribute.Operation; import com.teamwizardry.wizardry.api.spell.module.ModuleInstance; import com.teamwizardry.wizardry.api.spell.module.ModuleInstanceModifier; import com.teamwizardry.wizardry.api.spell.module.ModuleOverrideHandler; import com.teamwizardry.wizardry.api.util.FixedPointUtils; import com.teamwizardry.wizardry.init.ModItems; import com.teamwizardry.wizardry.init.ModSounds; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.SoundCategory; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import net.minecraftforge.common.util.Constants; import net.minecraftforge.common.util.INBTSerializable; import org.apache.commons.lang3.tuple.Pair; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.awt.*; import java.util.*; import java.util.List; /** * Modules ala IBlockStates. <br /> * <b>IMPORTANT</b>: All new NBT fields, which are float or double should be stored in their fixed point form. * Also lists and compounds must be sorted by some arbitrary, but fixed, order. * This way it is assured that the {@link NBTBase#equals} in the resulting NBT is reliable. */ public class SpellRing implements INBTSerializable<NBTTagCompound> { /** * Mostly used as a cache key. <br/> * <b>NOTE</b>: Must be initialized only by {@link #serializeNBT()} to have a normalized key! */ private NBTTagCompound serializedTag = null; /** * Store all processed modifier info and any extra you want here. * Used by modifier processing and the WorktableGUI to save GUI in TileWorktable <br/> * <b>NOTE</b>: Must be initialized only by {@link #processModifiers()} or {@link #deserializeNBT(NBTTagCompound)} * to have normalized keys, used for cache nbt! */ private NBTTagCompound informationTag = new NBTTagCompound(); /** * A map holding compile time modifiers. */ @Nonnull private ArrayListMultimap<Operation, AttributeModifierSpellRing> compileTimeModifiers = ArrayListMultimap.create(); /** * Primary rendering color. */ @Nonnull private Color primaryColor = Color.WHITE; /** * Secondary rendering color. */ @Nonnull private Color secondaryColor = Color.WHITE; /** * The Module of this Ring. */ @Nullable private ModuleInstance module; /** * The parent ring of this Ring, the ring that will have been run before this. */ @Nullable private SpellRing parentRing = null; /** * The child ring of this Ring, the ring that will run after this. */ @Nullable private SpellRing childRing = null; /** * A module override handler. */ private ModuleOverrideHandler lazy_overrideHandler = null; // "lazy" means, that access to variable should be done only over getter /** * Differentiate between spellrings easily. */ @Nonnull private UUID uniqueID = UUID.randomUUID(); /** * The constructor.<br/> * <b>NOTE</b>: Called only for deserialization. */ private SpellRing() { } /** * The constructor. <br/> * <b>NOTE</b>: Called only by {@link SpellBuilder}. * * @param module the module to construct the spell from. */ SpellRing(@Nonnull ModuleInstance module) { setModule(module); } /** * Deserializes a spell ring from given NBT. <br/> * Please don't misuse this. * * @param compound the tag compound to deserialize from * @return A created spell ring. */ public static SpellRing deserializeRing(NBTTagCompound compound) { SpellRing ring = new SpellRing(); ring.deserializeNBT(compound); SpellRing lastRing = ring; while (lastRing != null) { if (lastRing.getChildRing() == null) break; lastRing = lastRing.getChildRing(); } if (lastRing != null) lastRing.updateColorChain(); return ring; } /** * Returns a normalized NBT tag compound for information from a source. * * @param informationMap a source key-value list, storing information. * @return normalized information NBT compound */ private static NBTTagCompound sortInformationTag(Map<String, Float> informationMap) { ArrayList<Pair<String, Float>> sortedInformationList = new ArrayList<>(informationMap.size()); informationMap.forEach((key, val) -> sortedInformationList.add(Pair.of(key, FixedPointUtils.doubleToFixed(val)))); sortedInformationList.sort(Comparator.comparing(Pair::getKey)); NBTTagCompound newInformationTag = new NBTTagCompound(); for (Pair<String, Float> entry : sortedInformationList) { FixedPointUtils.setFixedToNBT(newInformationTag, entry.getKey(), entry.getValue()); } return newInformationTag; } /** * Returns a normalized NBT tag compound for information from a source. * * @param informationNbt a source NBT compound * @return normalized information NBT compound */ private static NBTTagCompound sortInformationTag(NBTTagCompound informationNbt) { ArrayList<Pair<String, Float>> sortedInformationList = new ArrayList<>(informationNbt.getSize()); for (String key : informationNbt.getKeySet()) { sortedInformationList.add(Pair.of(key, FixedPointUtils.getFixedFromNBT(informationNbt, key))); } sortedInformationList.sort(Comparator.comparing(Pair::getKey)); NBTTagCompound newInformationTag = new NBTTagCompound(); for (Pair<String, Float> entry : sortedInformationList) { FixedPointUtils.setFixedToNBT(newInformationTag, entry.getKey(), entry.getValue()); } return newInformationTag; } /** * Returns a normalized modifier tag list. * * @param modifierList the modifier list. * @return the normalized tag list. */ private static NBTTagList sortModifierList(List<NBTTagCompound> modifierList) { NBTTagList attribs = new NBTTagList(); modifierList.sort(SpellRing::compareModifierCompounds); for (NBTTagCompound modifierCompound : modifierList) { attribs.appendTag(modifierCompound); } return attribs; } private static int compareModifierCompounds(NBTTagCompound nbt1, NBTTagCompound nbt2) { int op1 = nbt1.getInteger("operation"); int op2 = nbt2.getInteger("operation"); if (op1 != op2) return (op1 - op2) > 0 ? 1 : -1; return nbt1.getString("attribute").compareTo(nbt2.getString("attribute")); } /** * Will run the spellData from this ring and down to it's children including rendering. * * @param world * @param data The SpellData object. * @param makeSpellIndependant This field will make the spell run with another unique id so it doesn't */ public void runSpellRing(@Nonnull World world, SpellData data, boolean makeSpellIndependant) { if (module == null) return; SpellRing modifRing = makeSpellIndependant ? changeUUID() : this; SpellData modifData = makeSpellIndependant ? data.copy() : data; if (modifData.getCaster(world) != null) modifData.processCastTimeModifiers(modifData.getCaster(world), modifRing); boolean success = module.castSpell(world, modifData, modifRing); if (success && module.shouldRunChildren()) { if (getChildRing() != null) { getChildRing().runSpellRing(world, modifData, makeSpellIndependant); } } } /** * Get a modifier in this ring between the range. Returns the true attribute value, unmodified by any other attributeModifiers. * * @param attribute The attribute you want. List in {@link AttributeRegistry} for default attributeModifiers. * @return The {@code double} potency of a modifier. */ public final double getTrueAttributeValue(Attribute attribute) { if (module == null) return 0; double current = FixedPointUtils.getDoubleFromNBT(informationTag, attribute.getNbtName()); AttributeRange range = module.getAttributeRanges().get(attribute); return MathHelper.clamp(current, range.min, range.max); } //TODO: orb holders public boolean taxCaster(@Nonnull World world, SpellData data, double multiplier, boolean failSound) { if(data.getData(SpellData.DefaultKeys.CASTER) == null) return true; Entity caster = world.getEntityByID(data.getData(SpellData.DefaultKeys.CASTER)); if(caster == null) { Wizardry.LOGGER.warn("Caster was null!"); return true; } IManaCapability cap = ManaCapabilityProvider.getCap(caster); if (cap == null) return false; double manaDrain = getManaDrain(data) * multiplier; double burnoutFill = getBurnoutFill(data) * multiplier; boolean fail = false; try (ManaManager.CapManagerBuilder mgr = ManaManager.forObject(cap)) { if (mgr.getMana() < manaDrain) fail = true; mgr.removeMana(manaDrain); mgr.addBurnout(burnoutFill); } if (fail && failSound) { Vec3d origin = data.getOriginWithFallback(world); if (origin != null) world.playSound(null, new BlockPos(origin), ModSounds.SPELL_FAIL, SoundCategory.NEUTRAL, 1f, 1f); } return !fail; } public boolean taxCaster(@Nonnull World world, SpellData data, boolean failSound) { return taxCaster(world, data, 1, failSound); } public boolean isContinuous() { if (module != null) { return module.getModuleClass() instanceof IContinuousModule; } return false; } // public final float getCapeReduction(EntityLivingBase caster) { // ItemStack stack = BaublesSupport.getItem(caster, ModItems.CAPE); // if (stack != ItemStack.EMPTY) { // float time = NBTHelper.getInt(stack, "maxTick", 0); // return (float) MathHelper.clamp(1 - (time / 1000000.0), 0.25, 1); // } // return 1; // } /** * Get all the children rings of this ring excluding itself. */ public final Set<SpellRing> getAllChildRings() { Set<SpellRing> childRings = new HashSet<>(); if (childRing == null) return childRings; SpellRing tempModule = childRing; while (tempModule != null) { childRings.add(tempModule); tempModule = tempModule.getChildRing(); } return childRings; } /** * Get a modifier in this ring between the range. Returns the attribute value, modified by burnout and multipliers, for use in a spell. * * @param world * @param attribute The attribute you want. List in {@link AttributeRegistry} for default attributeModifiers. * @param data The data of the spell being cast, used to get caster-specific modifiers. * @return The {@code double} potency of a modifier. */ public final float getAttributeValue(World world, Attribute attribute, SpellData data) { if (module == null) return 0; float current = FixedPointUtils.getDoubleFromNBT(informationTag, attribute.getNbtName()); AttributeRange range = module.getAttributeRanges().get(attribute); current = MathHelper.clamp(current, range.min, range.max); current = data.getCastTimeValue(attribute, current); current *= getPlayerBurnoutMultiplier(world, data); current *= getPowerMultiplier(); return current; } @Nullable public SpellRing getChildRing() { return childRing; } /** * Will process all modifiers and attributeModifiers set. * WILL RESET THE INFORMATION TAG. <br/> * <b>NOTE</b>: Called only by {@link SpellBuilder}. */ void processModifiers() { HashMap<String, Float> informationMap = new HashMap<>(); if (module != null) { // So first of all we take all the ranges possible for the modifiers of this module // We then add them all to the informationMap in their minimum values to buff them later below. module.getAttributeRanges().forEach((attribute, range) -> { informationMap.put(attribute.getNbtName(), range.min); }); } // Look through every operation we can do // If the compileTimeModifiers map has modifiers for the operation, lets do them. for (Operation op : Operation.values()) { if (!compileTimeModifiers.containsKey(op)) continue; for (AttributeModifier modifier : compileTimeModifiers.get(op)) { if (!informationMap.containsKey(modifier.getAttribute().getNbtName())) continue; // Get the minimum range we gave the informationMap above (the minimum values of all the modifiers // this module supports. float current = informationMap.get(modifier.getAttribute().getNbtName()); // Buff the minimum value according to however many modifiers where applied float newValue = modifier.apply(current); // Update the map's entry to the buffed modifier value. informationMap.put(modifier.getAttribute().getNbtName(), newValue); if (ConfigValues.debugInfo) Wizardry.LOGGER.info(module == null ? "<null module>" : module.getNBTKey() + ": Attribute: " + modifier.getAttribute() + ": " + current + "-> " + newValue); } } // Output a sorted list of tags to informationTag informationTag = sortInformationTag(informationMap); } @Nullable public SpellRing getParentRing() { return parentRing; } public SpellRing changeUUID() { uniqueID = UUID.randomUUID(); return this; } /** * Sets a child ring.<br/> * <b>NOTE</b>: Called only by {@link SpellBuilder}. * * @param childRing the child ring. */ void setChildRing(@Nonnull SpellRing childRing) { this.childRing = childRing; } @Nullable public ModuleInstance getModule() { return module; } /** * Sets a parent ring.<br/> * <b>NOTE</b>: Called only by {@link SpellBuilder}. * * @param parentRing the parent ring to set */ void setParentRing(@Nullable SpellRing parentRing) { this.parentRing = parentRing; } @Nonnull public Color getPrimaryColor() { return primaryColor; } /** * Returns the handler to invoke overrides of the whole spell chain. * * @return the override handler of the spell chain. */ @Nonnull public synchronized ModuleOverrideHandler getOverrideHandler() { if (lazy_overrideHandler == null) { if (parentRing != null) lazy_overrideHandler = parentRing.getOverrideHandler(); else { lazy_overrideHandler = new ModuleOverrideHandler(this); } } return lazy_overrideHandler; } @Nonnull public Color getSecondaryColor() { return secondaryColor; } /** * Sets a module.<br/> * <b>NOTE</b>: Called from constructor and implicitly only by {@link SpellBuilder}. * * @param module the module to set */ void setModule(@Nonnull ModuleInstance module) { this.module = module; setPrimaryColor(module.getPrimaryColor()); setSecondaryColor(module.getSecondaryColor()); } /** * Propagates color settings to parent.<br/> * <b>NOTE</b>: Called only by {@link SpellBuilder}. */ void updateColorChain() { if (getParentRing() == null) return; getParentRing().setPrimaryColor(getPrimaryColor()); getParentRing().setSecondaryColor(getSecondaryColor()); getParentRing().updateColorChain(); } public double getPowerMultiplier() { return getTrueAttributeValue(AttributeRegistry.POWER_MULTI); } public double getManaMultiplier() { return getTrueAttributeValue(AttributeRegistry.MANA_MULTI); } public double getBurnoutMultiplier() { return getTrueAttributeValue(AttributeRegistry.BURNOUT_MULTI); } /** * Sets a primary color.<br/> * <b>NOTE</b>: Called implicitly only by {@link SpellBuilder}. * * @param primaryColor the primary color to set */ void setPrimaryColor(@Nonnull Color primaryColor) { this.primaryColor = primaryColor; updateColorChain(); } /** * Sets a secondary color.<br/> * <b>NOTE</b>: Called implicitly only by {@link SpellBuilder}. * * @param secondaryColor */ void setSecondaryColor(@Nonnull Color secondaryColor) { this.secondaryColor = secondaryColor; } /** * Returns mana drain value. If spell data is passed, then the value is modified additionally by runtime data, * e.g. by cape and halo attributeModifiers of caster. * * @param data runtime data of active spell. Can be <code>null</code>. * @return mana drain value */ public double getManaDrain(SpellData data) { float value = FixedPointUtils.getDoubleFromNBT(informationTag, AttributeRegistry.MANA.getNbtName()); if (data != null) value = data.getCastTimeValue(AttributeRegistry.MANA, value); return value * getManaMultiplier(); } /** * Returns burnout fill value. If spell data is passed, then the value is modified additionally by runtime data, * e.g. by cape and halo attributeModifiers of caster. * * @param data runtime data of active spell. Can be <code>null</code>. * @return burnout fill value */ public double getBurnoutFill(SpellData data) { float value = FixedPointUtils.getDoubleFromNBT(informationTag, AttributeRegistry.BURNOUT.getNbtName()); if (data != null) value = data.getCastTimeValue(AttributeRegistry.BURNOUT, value); return value * getBurnoutMultiplier(); } /** * Adds a modifier module to spell ring. <br/> * <b>NOTE</b>: In actual implementation, only attributeModifiers are overtaken. <br/> * <b>NOTE</b>: Called only by {@link SpellBuilder}. * * @param moduleModifier the modifier module instance */ void addModifier(ModuleInstanceModifier moduleModifier) { moduleModifier.getAttributeModifiers().forEach(modifier -> compileTimeModifiers.put(modifier.getOperation(), new AttributeModifierSpellRing(modifier))); } /** * Adds an attribute modifier to spell ring. <br/> * <b>NOTE</b>: Called only by {@link SpellBuilder}. * * @param attributeModifier the attribute modifier */ void addModifier(AttributeModifier attributeModifier) { compileTimeModifiers.put(attributeModifier.getOperation(), new AttributeModifierSpellRing(attributeModifier)); } public int getChargeUpTime() { return (int) FixedPointUtils.getDoubleFromNBT(informationTag, AttributeRegistry.CHARGEUP.getNbtName()); } public int getCooldownTime(@Nullable World world, @Nullable SpellData data) { if (module != null && data != null && world != null && module.getModuleClass() instanceof IOverrideCooldown) return ((IOverrideCooldown) module.getModuleClass()).getNewCooldown(world, data, this); return (int) FixedPointUtils.getDoubleFromNBT(informationTag, AttributeRegistry.COOLDOWN.getNbtName()); } @Nullable public String getModuleReadableName() { return module != null ? module.getReadableName() : null; } public NBTTagCompound getInformationTag() { return informationTag; } public int getCooldownTime() { return getCooldownTime(null, null); } @Override public String toString() { StringBuilder builder = new StringBuilder(); SpellRing ring = this; while (ring != null) { builder.append(ring.getModuleReadableName()).append(ring.getChildRing() == null ? "" : " > "); ring = ring.getChildRing(); } return builder.toString(); } /** * All non mana, burnout, and multiplier attributeModifiers are reduced based on the caster's burnout level. This returns how much to reduce them by. * * @return The INVERTED burnout multiplier. */ public double getPlayerBurnoutMultiplier(World world, SpellData data) { Entity caster = data.getCaster(world); if (caster == null || caster instanceof EntityLivingBase && BaublesSupport.getItem((EntityLivingBase) caster, ModItems.CREATIVE_HALO, ModItems.FAKE_HALO, ModItems.REAL_HALO).isEmpty()) return 1; double multiplier = ManaManager.getBurnout(caster) / ManaManager.getMaxBurnout(caster); double burnoutLimit = 0.5; //TODO: Probably put this into config, limit to [0, 1) return Math.min(1, 1 - (multiplier - burnoutLimit) / (1 - burnoutLimit)); } @Nonnull public UUID getUniqueID() { return uniqueID; } @Override public NBTTagCompound serializeNBT() { if (serializedTag == null) { serializedTag = internalSerializeNBT(); } return serializedTag; } /** * Core of {@link #serializeNBT()}. Doesn't set serializedTag. * * @return the serialized tag. */ private NBTTagCompound internalSerializeNBT() { NBTTagCompound compound = new NBTTagCompound(); if (!compileTimeModifiers.isEmpty()) { // Retrieve all modifier compounds ArrayList<NBTTagCompound> modifierList = new ArrayList<>(compileTimeModifiers.size()); compileTimeModifiers.forEach((op, modifier) -> { NBTTagCompound modifierCompound = new NBTTagCompound(); modifierCompound.setInteger("operation", modifier.getOperation().ordinal()); modifierCompound.setString("attribute", modifier.getAttribute().getNbtName()); FixedPointUtils.setFixedToNBT(modifierCompound, "modifier", modifier.getModifierFixed()); modifierList.add(modifierCompound); }); // Sort and store them NBTTagList attribs = sortModifierList(modifierList); compound.setTag("modifiers", attribs); } compound.setTag("extra", informationTag); compound.setString("primary_color", String.valueOf(primaryColor.getRGB())); compound.setString("secondary_color", String.valueOf(secondaryColor.getRGB())); if (childRing != null) compound.setTag("child_ring", this.childRing.serializeNBT()); if (module != null) compound.setString("module", module.getNBTKey()); compound.setString("uuid", uniqueID.toString()); return compound; } @Override public void deserializeNBT(NBTTagCompound nbt) { // NOTE: Don't store nbt argument to serializedNBT. This one must be generated only by serializeNBT() if (nbt.hasKey("module")) this.module = ModuleInstance.deserialize(nbt.getString("module")); if (nbt.hasKey("extra")) informationTag = sortInformationTag(nbt.getCompoundTag("extra")); if (nbt.hasKey("primary_color")) primaryColor = Color.decode(nbt.getString("primary_color")); if (nbt.hasKey("secondary_color")) secondaryColor = Color.decode(nbt.getString("secondary_color")); if (nbt.hasKey("modifiers")) { compileTimeModifiers.clear(); for (NBTBase base : nbt.getTagList("modifiers", Constants.NBT.TAG_COMPOUND)) { if (base instanceof NBTTagCompound) { NBTTagCompound modifierCompound = (NBTTagCompound) base; if (modifierCompound.hasKey("operation") && modifierCompound.hasKey("attribute") && modifierCompound.hasKey("modifier")) { Operation operation = Operation.values()[modifierCompound.getInteger("operation") % Operation.values().length]; Attribute attribute = AttributeRegistry.getAttributeFromName(modifierCompound.getString("attribute")); float modifierFixed = FixedPointUtils.getFixedFromNBT(modifierCompound, "modifier"); compileTimeModifiers.put(operation, new AttributeModifierSpellRing(attribute, modifierFixed, operation)); } } } } if (nbt.hasKey("child_ring")) { SpellRing childRing = deserializeRing(nbt.getCompoundTag("child_ring")); childRing.setParentRing(this); setChildRing(childRing); } if (nbt.hasKey("uuid")) uniqueID = UUID.fromString(nbt.getString("uuid")); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SpellRing ring = (SpellRing) o; return Objects.equals(uniqueID, ring.uniqueID); } @Override public int hashCode() { return Objects.hash(uniqueID); } //////////////////// /** * Storage class for attribute modifiers. An extension class to {@link AttributeModifier} * is necessary to store values in their fixed value form to avoid conversions and roundup errors. <br /> * <b>NOTE</b>: Helps to avoid using double values in NBT. As the {@link #equals(Object)} method isn't reliable for them. * * @author Avatair */ private static class AttributeModifierSpellRing extends AttributeModifier { private float modifierFixed; public AttributeModifierSpellRing(AttributeModifier modifier) { this(modifier.getAttribute(), modifier.getModifier(), modifier.getOperation()); } public AttributeModifierSpellRing(Attribute attribute, float modifier, Operation op) { super(attribute, modifier, op); this.modifierFixed = FixedPointUtils.doubleToFixed(modifier); } public AttributeModifierSpellRing(Attribute attribute, long modifierFixed, Operation op) { super(attribute, FixedPointUtils.fixedToDouble(modifierFixed), op); } public float getModifierFixed() { return this.modifierFixed; } public void setModifier(float newValue) { this.modifierFixed = FixedPointUtils.doubleToFixed(newValue); super.setModifier(newValue); } @Override public AttributeModifier copy() { return new AttributeModifierSpellRing(getAttribute(), modifierFixed, getOperation()); } } }