package electroblob.wizardry.misc; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import electroblob.wizardry.Wizardry; import electroblob.wizardry.constants.Element; import electroblob.wizardry.constants.Tier; import electroblob.wizardry.data.WizardData; import electroblob.wizardry.entity.EntityMeteor; import electroblob.wizardry.entity.construct.*; import electroblob.wizardry.entity.living.*; import electroblob.wizardry.entity.projectile.EntityFirebomb; import electroblob.wizardry.entity.projectile.EntityMagicFireball; import electroblob.wizardry.event.SpellCastEvent; import electroblob.wizardry.event.SpellCastEvent.Source; import electroblob.wizardry.item.IManaStoringItem; import electroblob.wizardry.item.ISpellCastingItem; import electroblob.wizardry.item.ItemArtefact; import electroblob.wizardry.registry.*; import electroblob.wizardry.spell.Banish; import electroblob.wizardry.util.BlockUtils; import electroblob.wizardry.util.EntityUtils; import electroblob.wizardry.util.SpellModifiers; import electroblob.wizardry.util.SpellProperties.Context; import net.minecraft.entity.effect.EntityLightningBolt; import net.minecraft.entity.item.EntityFallingBlock; import net.minecraft.entity.passive.EntitySquid; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.init.Blocks; import net.minecraft.init.MobEffects; import net.minecraft.item.ItemStack; import net.minecraft.potion.PotionEffect; import net.minecraft.util.*; 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.world.World; import net.minecraftforge.common.IPlantable; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.eventhandler.EventPriority; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import org.apache.commons.lang3.tuple.Pair; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.function.BiConsumer; /** * A {@code Forfeit} object represents a negative effect that may happen when a player attempts to cast an * undiscovered spell. The nature and severity of the forfeit depends on the element and tier of the spell that was * attempted.
*
* Adding a new forfeit is as simple as calling {@link Forfeit#add(Tier, Element, Forfeit)} and supplying the * {@code Forfeit} instance along with a tier and element to associate it with. To create a forfeit, you may extend * this class and instantiate it, or use {@link Forfeit#create(ResourceLocation, BiConsumer)} to concisely define the * behaviour (this method is provided since most forfeits' behaviour code is fairly brief and would otherwise result in * a large number of trivial 'stub' classes - in fact, many of wizardry's forfeits are defined in a single line).
*
* This class also handles the (event-driven) selection of forfeits and determines when one should be applied. * * @author Electroblob * @since Wizardry 4.2 */ // With the exception of sound events, everything forfeit-related is done right here. How about that for modularity? :P @Mod.EventBusSubscriber public abstract class Forfeit { private static final ListMultimap, Forfeit> forfeits = ArrayListMultimap.create(); private static final float TIER_CHANGE_CHANCE = 0.2f; private final ResourceLocation name; protected final SoundEvent sound; public Forfeit(ResourceLocation name){ this.name = name; this.sound = WizardrySounds.createSound("forfeit." + name.getPath()); } public abstract void apply(World world, EntityPlayer player); /** * Returns an {@link ITextComponent} for the message displayed when this forfeit is activated. * @param implementName An {@code ITextComponent} for the name of the implement being used. This is usually * something generic like 'wand' or 'scroll'. * @return An {@code ITextComponent} representing this forfeit's message, for use in chat messages. * @see Forfeit#getMessageForWand() * @see Forfeit#getMessageForScroll() */ public ITextComponent getMessage(ITextComponent implementName){ return new TextComponentTranslation("forfeit." + name.toString(), implementName); } /** Wrapper for {@link Forfeit#getMessage(ITextComponent)} with {@code implementName} set to the lang file key * {@code item.ebwizardry:wand.generic} */ public ITextComponent getMessageForWand(){ return getMessage(new TextComponentTranslation("item." + Wizardry.MODID + ":wand.generic")); } /** Wrapper for {@link Forfeit#getMessage(ITextComponent)} with {@code implementName} set to the lang file key * {@code item.ebwizardry:scroll.generic} */ public ITextComponent getMessageForScroll(){ return getMessage(new TextComponentTranslation("item." + Wizardry.MODID + ":scroll.generic")); } /** Returns the {@link SoundEvent} played when this forfeit is activated. */ public SoundEvent getSound(){ return sound; } public static void add(Tier tier, Element element, Forfeit forfeit){ forfeits.put(Pair.of(tier, element), forfeit); } public static Forfeit getRandomForfeit(Random random, Tier tier, Element element){ float f = random.nextFloat(); if(f < TIER_CHANGE_CHANCE) tier = tier.previous(); else if(f > 1 - TIER_CHANGE_CHANCE) tier = tier.next(); List matches = forfeits.get(Pair.of(tier, element)); if(matches.isEmpty()){ Wizardry.logger.warn("No forfeits with tier {} and element {}!", tier, element); return null; } return matches.get(random.nextInt(matches.size())); } public static Collection getForfeits(){ return Collections.unmodifiableCollection(forfeits.values()); } /** Static helper method that creates a {@code Forfeit} with the given name and an effect specified by the given * consumer. This allows code to use a neater lambda expression rather than an anonymous class. */ public static Forfeit create(ResourceLocation name, BiConsumer effect){ return new Forfeit(name){ @Override public void apply(World world, EntityPlayer player){ effect.accept(world, player); } }; } /** Internal wrapper for {@link Forfeit#create(ResourceLocation, BiConsumer)} so I don't have to put wizardry's * mod ID in every time. */ private static Forfeit create(String name, BiConsumer effect){ return create(new ResourceLocation(Wizardry.MODID, name), effect); } @SubscribeEvent(priority = EventPriority.NORMAL) // Forfeits come after spell disabling but before modifiers public static void onSpellCastPreEvent(SpellCastEvent.Pre event){ if(!Wizardry.settings.discoveryMode) return; if(event.getCaster() instanceof EntityPlayer && !((EntityPlayer)event.getCaster()).isCreative() && (event.getSource() == SpellCastEvent.Source.WAND || event.getSource() == SpellCastEvent.Source.SCROLL)){ EntityPlayer player = (EntityPlayer)event.getCaster(); WizardData data = WizardData.get(player); float chance = (float)Wizardry.settings.forfeitChance; if(ItemArtefact.isArtefactActive(player, WizardryItems.amulet_wisdom)) chance *= 0.5; // Use the synchronised random to ensure the same outcome on client- and server-side if(data.synchronisedRandom.nextFloat() < chance && !data.hasSpellBeenDiscovered(event.getSpell())){ event.setCanceled(true); Forfeit forfeit = getRandomForfeit(data.synchronisedRandom, event.getSpell().getTier(), event.getSpell().getElement()); if(forfeit == null){ // Should never happen, but just in case... if(!event.getWorld().isRemote) player.sendMessage(new TextComponentTranslation("forfeit.ebwizardry:do_nothing")); return; } forfeit.apply(event.getWorld(), player); ItemStack stack = player.getHeldItemMainhand(); if(!(stack.getItem() instanceof ISpellCastingItem)){ stack = player.getHeldItemOffhand(); if(!(stack.getItem() instanceof ISpellCastingItem)) stack = ItemStack.EMPTY; } if(!stack.isEmpty()){ // Still need to charge the player mana or consume the scroll if(event.getSource() == Source.SCROLL){ if(!player.isCreative()) stack.shrink(1); }else if(stack.getItem() instanceof IManaStoringItem){ int cost = (int)(event.getSpell().getCost() * event.getModifiers().get(SpellModifiers.COST) + 0.1f); // Weird floaty rounding ((IManaStoringItem)stack.getItem()).consumeMana(stack, cost, player); } } WizardryAdvancementTriggers.spell_failure.triggerFor(player); EntityUtils.playSoundAtPlayer(player, forfeit.getSound(), WizardrySounds.SPELLS, 1, 1); if(!event.getWorld().isRemote) player.sendMessage( event.getSource() == SpellCastEvent.Source.WAND ? forfeit.getMessageForWand() : forfeit.getMessageForScroll()); } } } /** Called from the preInit method in the main mod class to set up all the forfeits. */ public static void register(){ add(Tier.NOVICE, Element.FIRE, create("burn_self", (w, p) -> p.setFire(5))); add(Tier.APPRENTICE, Element.FIRE, create("fireball", (w, p) -> { if(!w.isRemote){ EntityMagicFireball fireball = new EntityMagicFireball(w); Vec3d vec = p.getPositionEyes(1).add(p.getLookVec().scale(6)); fireball.setPosition(vec.x, vec.y, vec.z); fireball.shoot(p.posX, p.posY + p.getEyeHeight(), p.posZ, 1.5f, 1); w.spawnEntity(fireball); } })); add(Tier.APPRENTICE, Element.FIRE, create("firebomb", (w, p) -> { if(!w.isRemote){ EntityFirebomb firebomb = new EntityFirebomb(w); firebomb.setPosition(p.posX, p.posY + 5, p.posZ); w.spawnEntity(firebomb); } })); add(Tier.ADVANCED, Element.FIRE, create("explode", (w, p) -> w.createExplosion(null, p.posX, p.posY, p.posZ, 1, false))); add(Tier.ADVANCED, Element.FIRE, create("blazes", (w, p) -> { if(!w.isRemote){ for(int i = 0; i < 3; i++){ BlockPos pos = BlockUtils.findNearbyFloorSpace(p, 4, 2); if(pos == null) break; EntityBlazeMinion blaze = new EntityBlazeMinion(w); blaze.setPosition(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); w.spawnEntity(blaze); } } })); add(Tier.MASTER, Element.FIRE, create("burn_surroundings", (w, p) -> { if(!w.isRemote && EntityUtils.canDamageBlocks(p, w)){ List sphere = BlockUtils.getBlockSphere(p.getPosition(), 6); for(BlockPos pos : sphere){ if(w.rand.nextBoolean() && w.isAirBlock(pos) && BlockUtils.canPlaceBlock(p, w, pos)) w.setBlockState(pos, Blocks.FIRE.getDefaultState()); } } })); add(Tier.MASTER, Element.FIRE, create("meteors", (w, p) -> { if(!w.isRemote) for(int i=0; i<5; i++) w.spawnEntity(new EntityMeteor(w, p.posX + w.rand.nextDouble() * 16 - 8, p.posY + 40 + w.rand.nextDouble() * 30, p.posZ + w.rand.nextDouble() * 16 - 8, 1, EntityUtils.canDamageBlocks(p, w))); })); add(Tier.NOVICE, Element.ICE, create("freeze_self", (w, p) -> p.addPotionEffect(new PotionEffect(WizardryPotions.frost, 200)))); add(Tier.APPRENTICE, Element.ICE, create("freeze_self_2", (w, p) -> p.addPotionEffect(new PotionEffect(WizardryPotions.frost, 300, 1)))); add(Tier.APPRENTICE, Element.ICE, create("ice_spikes", (w, p) -> { if(!w.isRemote){ for(int i = 0; i < 5; i++){ EntityIceSpike iceSpike = new EntityIceSpike(w); double x = p.posX + 2 - w.rand.nextFloat() * 4; double z = p.posZ + 2 - w.rand.nextFloat() * 4; Integer y = BlockUtils.getNearestSurface(w, new BlockPos(x, p.posY, z), EnumFacing.UP, 2, true, BlockUtils.SurfaceCriteria.basedOn(World::isBlockFullCube)); if(y == null) break; iceSpike.setFacing(EnumFacing.UP); iceSpike.setPosition(x, y, z); w.spawnEntity(iceSpike); } } })); add(Tier.ADVANCED, Element.ICE, create("blizzard", (w, p) -> { if(!w.isRemote){ EntityBlizzard blizzard = new EntityBlizzard(w); blizzard.setPosition(p.posX, p.posY, p.posZ); w.spawnEntity(blizzard); } })); add(Tier.ADVANCED, Element.ICE, create("ice_wraiths", (w, p) -> { if(!w.isRemote){ for(int i = 0; i < 3; i++){ BlockPos pos = BlockUtils.findNearbyFloorSpace(p, 4, 2); if(pos == null) break; EntityIceWraith iceWraith = new EntityIceWraith(w); iceWraith.setPosition(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); w.spawnEntity(iceWraith); } } })); add(Tier.MASTER, Element.ICE, create("hailstorm", (w, p) -> { if(!w.isRemote){ EntityHailstorm hailstorm = new EntityHailstorm(w); hailstorm.setPosition(p.posX, p.posY + 5, p.posZ - 3); // Subtract 3 from z because it's facing south (yaw 0) w.spawnEntity(hailstorm); } })); add(Tier.MASTER, Element.ICE, create("ice_giant", (w, p) -> { if(!w.isRemote){ EntityIceGiant iceGiant = new EntityIceGiant(w); iceGiant.setPosition(p.posX + p.getLookVec().x * 4, p.posY, p.posZ + p.getLookVec().z * 4); w.spawnEntity(iceGiant); } })); add(Tier.NOVICE, Element.LIGHTNING, create("thunder", (w, p) -> { p.addVelocity(-p.getLookVec().x, 0, -p.getLookVec().z); if(w.isRemote) w.spawnParticle(EnumParticleTypes.EXPLOSION_LARGE, p.posX, p.posY, p.posZ, 0, 0, 0); })); add(Tier.APPRENTICE, Element.LIGHTNING, create("storm", (w, p) -> { if(!Spells.invoke_weather.isEnabled(Context.WANDS)) return; int standardWeatherTime = (300 + (new Random()).nextInt(600)) * 20; w.getWorldInfo().setRaining(true); w.getWorldInfo().setRainTime(standardWeatherTime); w.getWorldInfo().setThundering(true); w.getWorldInfo().setThunderTime(standardWeatherTime); })); add(Tier.APPRENTICE, Element.LIGHTNING, create("lightning_sigils", (w, p) -> { if(!w.isRemote){ for(EnumFacing direction : EnumFacing.HORIZONTALS){ BlockPos pos = p.getPosition().offset(direction, 2); Integer y = BlockUtils.getNearestFloor(w, pos, 2); if(y == null) continue; EntityLightningSigil sigil = new EntityLightningSigil(w); sigil.setPosition(pos.getX() + 0.5, y, pos.getZ() + 0.5); w.spawnEntity(sigil); } } })); add(Tier.ADVANCED, Element.LIGHTNING, create("lightning", (w, p) -> w.addWeatherEffect(new EntityLightningBolt(w, p.posX, p.posY, p.posZ, false)))); add(Tier.ADVANCED, Element.LIGHTNING, create("paralyse_self", (w, p) -> p.addPotionEffect(new PotionEffect(WizardryPotions.paralysis, 200)))); add(Tier.ADVANCED, Element.LIGHTNING, create("lightning_wraiths", (w, p) -> { if(!w.isRemote){ for(int i = 0; i < 3; i++){ BlockPos pos = BlockUtils.findNearbyFloorSpace(p, 4, 2); if(pos == null) break; EntityLightningWraith lightningWraith = new EntityLightningWraith(w); lightningWraith.setPosition(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); w.spawnEntity(lightningWraith); } } })); add(Tier.MASTER, Element.LIGHTNING, create("storm_elementals", (w, p) -> { if(!w.isRemote){ for(EnumFacing direction : EnumFacing.HORIZONTALS){ BlockPos pos = p.getPosition().offset(direction, 3); EntityStormElemental stormElemental = new EntityStormElemental(w); stormElemental.setPosition(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); w.spawnEntity(stormElemental); } } })); add(Tier.NOVICE, Element.NECROMANCY, create("nausea", (w, p) -> p.addPotionEffect(new PotionEffect(MobEffects.NAUSEA, 400)))); add(Tier.APPRENTICE, Element.NECROMANCY, create("zombie_horde", (w, p) -> { if(!w.isRemote){ for(int i = 0; i < 3; i++){ BlockPos pos = BlockUtils.findNearbyFloorSpace(p, 4, 2); if(pos == null) break; EntityZombieMinion zombie = new EntityZombieMinion(w); zombie.setPosition(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); w.spawnEntity(zombie); } } })); add(Tier.ADVANCED, Element.NECROMANCY, create("wither_self", (w, p) -> p.addPotionEffect(new PotionEffect(MobEffects.WITHER, 400)))); add(Tier.MASTER, Element.NECROMANCY, create("cripple_self", (w, p) -> p.attackEntityFrom(DamageSource.MAGIC, p.getHealth() - 1))); add(Tier.MASTER, Element.NECROMANCY, create("shadow_wraiths", (w, p) -> { if(!w.isRemote){ for(EnumFacing direction : EnumFacing.HORIZONTALS){ BlockPos pos = p.getPosition().offset(direction, 3); EntityShadowWraith wraith = new EntityShadowWraith(w); wraith.setPosition(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); w.spawnEntity(wraith); } } })); add(Tier.NOVICE, Element.EARTH, create("snares", (w, p) -> { if(!w.isRemote && EntityUtils.canDamageBlocks(p, w)){ for(EnumFacing direction : EnumFacing.HORIZONTALS){ BlockPos pos = p.getPosition().offset(direction); if(BlockUtils.canBlockBeReplaced(w, pos) && BlockUtils.canPlaceBlock(p, w, pos)) w.setBlockState(pos, WizardryBlocks.snare.getDefaultState()); } } })); add(Tier.NOVICE, Element.EARTH, create("squid", (w, p) -> { if(!w.isRemote){ EntitySquid squid = new EntitySquid(w); squid.setPosition(p.posX, p.posY + 3, p.posZ); w.spawnEntity(squid); } })); add(Tier.APPRENTICE, Element.EARTH, create("uproot_plants", (w, p) -> { if(!w.isRemote && EntityUtils.canDamageBlocks(p, w)){ List sphere = BlockUtils.getBlockSphere(p.getPosition(), 5); sphere.removeIf(pos -> !(w.getBlockState(pos).getBlock() instanceof IPlantable) || !BlockUtils.canBreakBlock(p, w, pos)); sphere.forEach(pos -> w.destroyBlock(pos, true)); } })); add(Tier.APPRENTICE, Element.EARTH, create("poison_self", (w, p) -> p.addPotionEffect(new PotionEffect(MobEffects.POISON, 400, 1)))); add(Tier.ADVANCED, Element.EARTH, create("flood", (w, p) -> { if(!w.isRemote && EntityUtils.canDamageBlocks(p, w)){ List sphere = BlockUtils.getBlockSphere(p.getPosition().up(), 2); sphere.removeIf(pos -> !BlockUtils.canBlockBeReplaced(w, pos, true) || !BlockUtils.canPlaceBlock(p, w, pos)); sphere.forEach(pos -> w.setBlockState(pos, Blocks.WATER.getDefaultState())); } })); add(Tier.MASTER, Element.EARTH, create("bury_self", (w, p) -> { if(!w.isRemote){ List sphere = BlockUtils.getBlockSphere(p.getPosition(), 4); sphere.removeIf(pos -> !w.getBlockState(pos).isFullCube() || BlockUtils.isBlockUnbreakable(w, pos) || BlockUtils.canBreakBlock(p, w, pos)); sphere.forEach(pos -> { EntityFallingBlock block = new EntityFallingBlock(w, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, w.getBlockState(pos)); block.motionY = 0.3 * (4 - (p.getPosition().getY() - pos.getY())); w.spawnEntity(block); }); } })); add(Tier.NOVICE, Element.SORCERY, create("spill_inventory", (w, p) -> { for(int i = 0; i < p.inventory.mainInventory.size(); i++){ ItemStack stack = p.inventory.mainInventory.get(i); if(!stack.isEmpty()){ p.dropItem(stack, true, false); p.inventory.mainInventory.set(i, ItemStack.EMPTY); } } })); add(Tier.APPRENTICE, Element.SORCERY, create("teleport_self", (w, p) -> ((Banish)Spells.banish).teleport(p, w, 8 + w.rand.nextDouble() * 8))); add(Tier.ADVANCED, Element.SORCERY, create("levitate_self", (w, p) -> p.addPotionEffect(new PotionEffect(MobEffects.LEVITATION, 200)))); add(Tier.ADVANCED, Element.SORCERY, create("vex_horde", (w, p) -> { if(!w.isRemote){ for(int i = 0; i < 4; i++){ BlockPos pos = BlockUtils.findNearbyFloorSpace(p, 4, 2); if(pos == null) break; EntityVexMinion vex = new EntityVexMinion(w); vex.setPosition(pos.getX() + 0.5, pos.getY() + 1, pos.getZ() + 0.5); w.spawnEntity(vex); } } })); add(Tier.MASTER, Element.SORCERY, create("black_hole", (w, p) -> { EntityBlackHole blackHole = new EntityBlackHole(w); Vec3d vec = p.getPositionEyes(1).add(p.getLookVec().scale(4)); blackHole.setPosition(vec.x, vec.y, vec.z); w.spawnEntity(blackHole); })); add(Tier.MASTER, Element.SORCERY, create("arrow_rain", (w, p) -> { if(!w.isRemote){ EntityArrowRain arrowRain = new EntityArrowRain(w); arrowRain.setPosition(p.posX, p.posY + 5, p.posZ - 3); // Subtract 3 from z because it's facing south (yaw 0) w.spawnEntity(arrowRain); } })); add(Tier.NOVICE, Element.HEALING, create("damage_self", (w, p) -> p.attackEntityFrom(DamageSource.MAGIC, 4))); add(Tier.NOVICE, Element.HEALING, create("spill_armour", (w, p) -> { for(int i = 0; i < p.inventory.armorInventory.size(); i++){ ItemStack stack = p.inventory.armorInventory.get(i); if(!stack.isEmpty()){ p.dropItem(stack, true, false); p.inventory.armorInventory.set(i, ItemStack.EMPTY); } } })); add(Tier.APPRENTICE, Element.HEALING, create("hunger", (w, p) -> p.addPotionEffect(new PotionEffect(MobEffects.HUNGER, 400, 4)))); add(Tier.APPRENTICE, Element.HEALING, create("blind_self", (w, p) -> p.addPotionEffect(new PotionEffect(MobEffects.BLINDNESS, 200)))); add(Tier.ADVANCED, Element.HEALING, create("weaken_self", (w, p) -> p.addPotionEffect(new PotionEffect(MobEffects.WEAKNESS, 600, 3)))); add(Tier.ADVANCED, Element.HEALING, create("jam_self", (w, p) -> p.addPotionEffect(new PotionEffect(WizardryPotions.arcane_jammer, 300)))); add(Tier.MASTER, Element.HEALING, create("curse_self", (w, p) -> p.addPotionEffect(new PotionEffect(WizardryPotions.curse_of_undeath, Integer.MAX_VALUE)))); } }