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.item.ItemArtefact;
import electroblob.wizardry.registry.*;
import electroblob.wizardry.spell.Banish;
import electroblob.wizardry.util.WizardryUtilities;
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.ordinal() > 0) tier = Tier.values()[tier.ordinal() - 1];
else if(f > 1 - TIER_CHANGE_CHANCE && tier.ordinal() < Tier.values().length-1) tier = Tier.values()[tier.ordinal() + 1];
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(event.getWorld().rand, 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);
WizardryAdvancementTriggers.spell_failure.triggerFor(player);
WizardryUtilities.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(0).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 = WizardryUtilities.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){
List sphere = WizardryUtilities.getBlockSphere(p.getPosition(), 6);
for(BlockPos pos : sphere){
if(w.rand.nextBoolean() && w.isAirBlock(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, WizardryUtilities.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 = WizardryUtilities.getNearestSurface(w, new BlockPos(x, p.posY, z), EnumFacing.UP, 2, true,
WizardryUtilities.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 = WizardryUtilities.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) -> {
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 = WizardryUtilities.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 = WizardryUtilities.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 = WizardryUtilities.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){
for(EnumFacing direction : EnumFacing.HORIZONTALS){
BlockPos pos = p.getPosition().offset(direction);
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){
List sphere = WizardryUtilities.getBlockSphere(p.getPosition(), 5);
sphere.removeIf(pos -> !(w.getBlockState(pos).getBlock() instanceof IPlantable));
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){
List sphere = WizardryUtilities.getBlockSphere(p.getPosition().up(), 2);
sphere.removeIf(pos -> !WizardryUtilities.canBlockBeReplaced(w, pos, true));
sphere.forEach(pos -> w.setBlockState(pos, Blocks.WATER.getDefaultState()));
}
}));
add(Tier.MASTER, Element.EARTH, create("bury_self", (w, p) -> {
if(!w.isRemote){
List sphere = WizardryUtilities.getBlockSphere(p.getPosition(), 4);
sphere.removeIf(pos -> !w.getBlockState(pos).isFullCube());
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 = WizardryUtilities.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))));
}
}