/** Copyright (C) <2017> <coolAlias> This file is part of coolAlias' Zelda Sword Skills Minecraft Mod; as such, you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package zeldaswordskills.world.crisis; import java.util.UUID; import com.google.common.base.Predicate; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; import net.minecraft.enchantment.Enchantment; import net.minecraft.enchantment.EnchantmentHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLiving; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.SharedMonsterAttributes; import net.minecraft.entity.ai.attributes.AttributeModifier; import net.minecraft.entity.ai.attributes.IAttributeInstance; import net.minecraft.entity.monster.EntitySkeleton; import net.minecraft.entity.monster.EntityZombie; import net.minecraft.entity.monster.IMob; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.init.Items; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.BlockPos; import net.minecraft.util.ChatComponentTranslation; import net.minecraft.util.EnumFacing; import net.minecraft.util.Vec3i; import net.minecraft.world.EnumDifficulty; import net.minecraft.world.World; import net.minecraft.world.gen.structure.StructureBoundingBox; import zeldaswordskills.api.entity.BombType; import zeldaswordskills.api.entity.CustomExplosion; import zeldaswordskills.block.BlockAncientTablet; import zeldaswordskills.block.BlockSecretStone; import zeldaswordskills.block.ZSSBlocks; import zeldaswordskills.block.tileentity.TileEntityDungeonCore; import zeldaswordskills.entity.mobs.EntityOctorok; import zeldaswordskills.ref.Config; import zeldaswordskills.ref.ModInfo; import zeldaswordskills.ref.Sounds; import zeldaswordskills.util.BossType; import zeldaswordskills.util.StructureGenUtils; import zeldaswordskills.util.TimedChatDialogue; import zeldaswordskills.util.WorldUtils; import zeldaswordskills.world.gen.AntiqueAtlasHelper; /** * * Base class for boss dungeon battle events can also be used for generic battles. * Extend to add more specific behaviors. * */ public class BossBattle extends AbstractCrisis { protected static final Predicate <? super Entity> SELECTOR = new Predicate<Entity>() { @Override public boolean apply(Entity entity) { return entity instanceof IMob; } }; /** The dungeon core in which the battle is occurring */ protected final TileEntityDungeonCore core; /** The bounding box of the associated boss dungeon*/ protected final StructureBoundingBox box; /** The difficulty setting in effect when the battle was begun */ protected int difficulty = -1; /** * Sets event timer to default value of 6000 (5 minutes). * When the core loads from NBT, the core's world object is null, so expect that. * @param core must have both a valid BossType and a valid StructureBoundingBox */ public BossBattle(TileEntityDungeonCore core) { this.core = core; this.box = core.getDungeonBoundingBox(); this.eventTimer = 6000; if (core.getBossType() == null) { throw new IllegalArgumentException("Dungeon Core must have a valid BossType!"); } if (box == null) { throw new IllegalArgumentException("Dungeon Core bounding box can not be null!"); } } /** * Commences the epic battle: sets difficulty, fills in the dungeon door, spawns boss mobs, etc. * Be sure to call super and schedule the first update tick when overriding. */ @Override public void beginCrisis(World world) { // TODO play boss battle music difficulty = world.getDifficulty().ordinal(); fillAllGaps(world); generateBossMobs(world, getNumBosses()); core.removeHinderBlock(); } /** * Handles everything that happens at the end of battle; * default spawns xp, plays victory sound, and marks the dungeon as complete on the Atlas Map */ @Override protected void endCrisis(World world) { // TODO play victory music instead of secret medley: Vec3i center = box.getCenter(); world.playSoundEffect(center.getX() + 0.5D, center.getY() + 1, center.getZ() + 0.5D, Sounds.SECRET_MEDLEY, 1.0F, 1.0F); if (world.getDifficulty() != EnumDifficulty.PEACEFUL) { WorldUtils.spawnXPOrbsWithRandom(world, world.rand, center.getX(), center.getY(), center.getZ(), 1000 * difficulty); } AntiqueAtlasHelper.placeCustomTile(world, ModInfo.ATLAS_DUNGEON_ID + core.getBossType().ordinal() + "_fin", (center.getX() << 4), (center.getZ() << 4)); generateAncientTablet(world); } @Override protected boolean canCrisisConclude(World world) { return areAllEnemiesDead(world); } /** * Nothing happens in generic boss battle update tick; no need to call super. */ @Override protected void onUpdateTick(World world) {} /** * Returns true if all boss enemies have been defeated and the crisis should end */ private boolean areAllEnemiesDead(World world) { // TODO instead, add all enemies by id to a List and check if still alive in world Vec3i center = box.getCenter(); return (world.getEntitiesWithinAABB(Entity.class, new AxisAlignedBB( center.getX() - 0.5D, center.getY(), center.getZ() - 0.5D, center.getX() + 0.5D, center.getY() + 1, center.getZ() + 0.5D). expand(box.getXSize() / 2, box.getYSize() / 2, box.getZSize() / 2), SELECTOR).isEmpty()); } /** * Destroys part of a random pillar during boss event * @param explode whether a difficulty-scaled explosion should be created as well */ protected void destroyRandomPillar(World world, boolean explode) { Vec3i center = box.getCenter(); int corner = world.rand.nextInt(4); int offset = (box.getXSize() < 11 ? 2 : 3); int x = (corner < 2 ? ((box.getXSize() < 11 ? center.getX() : box.minX) + offset) : ((box.getXSize() < 11 ? center.getX() : box.maxX) - offset)); int y = box.minY + (world.rand.nextInt(3) + 1); int z = (corner % 2 == 0 ? ((box.getZSize() < 11 ? center.getZ() : box.minZ) + offset) : ((box.getZSize() < 11 ? center.getZ() : box.maxZ) - offset)); if (!world.isAirBlock(new BlockPos(x, y, z))) { if (explode) { float radius = 1.5F + (float)(difficulty * 0.5F); CustomExplosion.createExplosion(world, x, y, z, radius, BombType.BOMB_STANDARD); } world.playSoundEffect(x + 0.5D, center.getY(), z + 0.5D, Sounds.ROCK_FALL, 1.0F, 1.0F); StructureGenUtils.destroyBlocksAround(world, x - 1, x + 2, y, box.maxY - 2, z - 1, z + 2, null, false); } } /** * Fills in the doorway and all other holes in the structure with appropriate blocks */ protected void fillAllGaps(World world) { IBlockState state = core.getRenderState(); if (state == null) { state = BlockSecretStone.getDroppedBlock(core.getBlockMetadata()).getDefaultState(); } for (int i = box.minX; i <= box.maxX; ++i) { for (int j = box.minY; j <= box.maxY; ++j) { for (int k = box.minZ; k <= box.maxZ; ++k) { if (i == box.minX || i == box.maxX || j == box.minY || j == box.maxY || k == box.minZ || k == box.maxZ) { BlockPos pos = new BlockPos(i, j, k); if (!world.getBlockState(pos).getBlock().isFullBlock()) { world.setBlockState(pos, state, 2); } } } } } } /** * Sets dungeon floor to block and meta given * @param toReplace if null, checks for standard dungeon blocks instead */ protected void setDungeonFloorTo(World world, IBlockState state, Block toReplace) { Block replace = (toReplace != null ? toReplace : BlockSecretStone.getDroppedBlock(core.getBlockMetadata())); for (int i = box.minX + 1; i < box.maxX; ++i) { for (int j = box.minZ + 1; j < box.maxZ; ++j) { BlockPos pos = new BlockPos(i, box.minY, j); if (world.getBlockState(pos).getBlock() == replace) { world.setBlockState(pos, state, 2); } } } } /** * Sets a random block in the structure to the block given if the current block is air * @param sound the sound to play, if any */ protected void setRandomBlockTo(World world, IBlockState state, String sound) { BlockPos pos = getRandomPlaceablePosition(world, state.getBlock(), box.minX + 1, box.minY + 3, box.minZ + 1, box.getXSize() - 1, box.getYSize() - 3, box.getZSize() - 1); if (pos != null && world.isAirBlock(pos)) { world.setBlockState(pos, state, 3); if (sound.length() > 0) { world.playSoundEffect(pos.getX(), pos.getY(), pos.getZ(), sound, 1.0F, 1.0F); } } } /** * Returns a random placeable block position (see {@link Block#canPlaceBlockAt}) within * a certain range of the minimum x/y/z coordinates, or null if not placeable. * @param block if null, checks if the current block at the position is replaceable * @return BlockPos, if not null, is not guaranteed to be within the structure bounding box */ protected BlockPos getRandomPlaceablePosition(World world, Block block, int minX, int minY, int minZ, int dx, int dy, int dz) { int i = minX + (dx > 1 ? world.rand.nextInt(dx) : 0); int j = minY + (dy > 1 ? world.rand.nextInt(dy) : 0); int k = minZ + (dz > 1 ? world.rand.nextInt(dz) : 0); BlockPos pos = new BlockPos(i, j, k); if (block != null && block.canPlaceBlockAt(world, pos)) { return pos; } else if (block == null && world.getBlockState(pos).getBlock().isReplaceable(world, pos)) { return pos; } return null; } /** * Called from {@link #endCrisis} to attempt to generate an Ancient Tablet */ protected void generateAncientTablet(World world) { if (world.rand.nextFloat() < 1.0F) { // TODO Config.getAncientTabletGenChance() // Attempt to find a valid block position n times BlockPos pos = null; // TODO allow tablet to spawn up to several chunks away? for (int n = 0; n < 4 && pos == null; ++n) { pos = getRandomPlaceablePosition(world, ZSSBlocks.ancientTablet, box.minX + 1, box.maxY + 1, box.minZ + 1, box.getXSize() - 1, 1, box.getZSize() - 1); } if (pos != null) { BlockAncientTablet.EnumType type = BlockAncientTablet.EnumType.byMetadata(world.rand.nextInt(BlockAncientTablet.EnumType.values().length)); EnumFacing facing = (world.rand.nextInt(2) == 0 ? EnumFacing.SOUTH : EnumFacing.EAST); world.setBlockState(pos, ZSSBlocks.ancientTablet.getDefaultState().withProperty(BlockAncientTablet.VARIANT, type).withProperty(BlockAncientTablet.FACING, facing)); world.playSoundEffect(pos.getX(), pos.getY(), pos.getZ(), Sounds.ROCK_FALL, 1.0F, 1.0F); Vec3i center = box.getCenter(); EntityPlayer player = world.getClosestPlayer(center.getX(), center.getY(), center.getZ(), 16.0D); if (player != null) { new TimedChatDialogue(player, 1250, 1250, new ChatComponentTranslation("chat.zss.ancient_tablet.spawn")); } } } } /** * Spawns the dungeon's boss or mini-boss * @param number the number of boss entities to spawn */ protected void generateBossMobs(World world, int number) { for (int i = 0; i < number; ++i) { Entity mob = core.getBossType().getNewMob(world); if (mob != null) { spawnMobInCorner(world, mob, i, true, true); } } } /** * Return the number of bosses to spawn; default returns Config setting */ protected int getNumBosses() { return Config.getNumBosses(); } /** * Sets the entity's position near the given corner (0-3) * @param corner 0 NW, 1 NE, 2 SW, 3 SE * @param equip whether to equip the entity with boss-level gear * @param health whether to grant the entity boss-level health */ protected void spawnMobInCorner(World world, Entity mob, int corner, boolean equip, boolean health) { int x = (corner < 2 ? box.minX + 2 : box.maxX - 2); int z = (corner % 2 == 0 ? box.minZ + 2 : box.maxZ - 2); int y = (World.doesBlockHaveSolidTopSurface(world, new BlockPos(x, box.minY + 1, z)) ? box.minY + 1 : box.minY + 3); WorldUtils.setEntityInStructure(world, mob, new BlockPos(x, y, z)); if (mob instanceof EntityLivingBase) { if (health) { boostHealth(world, (EntityLivingBase) mob); } if (equip) { equipEntity(world, (EntityLivingBase) mob); } } if (mob instanceof EntityLiving) { ((EntityLiving) mob).enablePersistence(); } if (!world.isRemote) { // TODO boss spawn sound 'roar!' or whatever world.spawnEntityInWorld(mob); } } /** * Multiplies the entity's health based on difficulty and config settings */ protected void boostHealth(World world, EntityLivingBase entity) { double d = (2 * difficulty * Config.getBossHealthFactor()); entity.getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(entity.getEntityAttribute(SharedMonsterAttributes.maxHealth).getAttributeValue() * d); entity.setHealth(entity.getMaxHealth()); } /** * Equips entity with appropriate weapon and armor */ protected void equipEntity(World world, EntityLivingBase entity) { ItemStack melee = null; ItemStack ranged = new ItemStack(Items.bow); Item[] armorSet = null; switch(difficulty) { case 1: armorSet = new Item[]{Items.chainmail_boots, Items.chainmail_leggings, Items.chainmail_chestplate, Items.chainmail_helmet}; melee = new ItemStack(Items.iron_sword); ranged.addEnchantment(Enchantment.power, 1); break; case 2: armorSet = new Item[]{Items.iron_boots, Items.iron_leggings, Items.iron_chestplate, Items.iron_helmet}; melee = new ItemStack(Items.iron_sword); melee.addEnchantment(Enchantment.sharpness, 2); ranged.addEnchantment(Enchantment.punch, 1); ranged.addEnchantment(Enchantment.power, 3); break; case 3: armorSet = new Item[]{Items.diamond_boots, Items.diamond_leggings, Items.diamond_chestplate, Items.diamond_helmet}; melee = new ItemStack(Items.diamond_sword); melee.addEnchantment(Enchantment.sharpness, 4); melee.addEnchantment(Enchantment.fireAspect, 1); ranged.addEnchantment(Enchantment.flame, 1); ranged.addEnchantment(Enchantment.punch, 2); ranged.addEnchantment(Enchantment.power, 5); break; } if (armorSet != null) { for (int i = 0; i < armorSet.length; ++i) { ItemStack armor = new ItemStack(armorSet[i]); EnchantmentHelper.addRandomEnchantment(world.rand, armor, difficulty + world.rand.nextInt(difficulty * 5)); entity.setCurrentItemOrArmor(i + 1, armor); } } if (entity instanceof EntityZombie) { ((EntityZombie) entity).setCurrentItemOrArmor(0, melee); } else if (entity instanceof EntitySkeleton) { EntitySkeleton skeleton = (EntitySkeleton) entity; skeleton.setCurrentItemOrArmor(0, ranged); if (core.getBossType() == BossType.HELL) { skeleton.setSkeletonType(1); skeleton.setCurrentItemOrArmor(0, melee); } else { skeleton.setCurrentItemOrArmor(0, ranged); } } else { if (entity instanceof EntityOctorok) { ((EntityOctorok) entity).setType((byte) 1); } IAttributeInstance iattribute = entity.getEntityAttribute(SharedMonsterAttributes.attackDamage); AttributeModifier modifier = (new AttributeModifier(UUID.randomUUID(), "Boss Attack Bonus", difficulty * 2.0D, 0)).setSaved(true); iattribute.applyModifier(modifier); } } @Override public void writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); compound.setInteger("difficulty", difficulty); } @Override public void readFromNBT(NBTTagCompound compound) { super.readFromNBT(compound); difficulty = compound.getInteger("difficulty"); } }