/** 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.block.tileentity; import net.minecraft.block.Block; import net.minecraft.block.material.Material; import net.minecraft.block.state.IBlockState; import net.minecraft.entity.EntityLiving; import net.minecraft.entity.monster.EntityCaveSpider; import net.minecraft.entity.monster.EntityCreeper; import net.minecraft.entity.monster.EntitySkeleton; import net.minecraft.entity.monster.EntitySpider; import net.minecraft.entity.monster.EntityZombie; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.init.Blocks; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.BlockPos; import net.minecraft.util.EnumFacing; import net.minecraft.util.ITickable; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3i; import net.minecraft.world.gen.structure.StructureBoundingBox; import zeldaswordskills.ZSSAchievements; import zeldaswordskills.ZSSMain; import zeldaswordskills.block.BlockDungeonStone; import zeldaswordskills.block.BlockSecretStone; import zeldaswordskills.block.BlockWarpStone; import zeldaswordskills.block.IDungeonBlock; import zeldaswordskills.block.ZSSBlocks; import zeldaswordskills.entity.IEntityVariant; import zeldaswordskills.entity.mobs.EntityChu; import zeldaswordskills.entity.mobs.EntityDarknut; import zeldaswordskills.entity.mobs.EntityKeese; import zeldaswordskills.entity.mobs.EntityOctorok; import zeldaswordskills.entity.mobs.EntitySkulltula; import zeldaswordskills.entity.mobs.EntityWizzrobe; import zeldaswordskills.entity.player.ZSSPlayerInfo; import zeldaswordskills.entity.player.ZSSPlayerInfo.Stats; import zeldaswordskills.ref.Config; import zeldaswordskills.ref.Sounds; import zeldaswordskills.util.BossType; import zeldaswordskills.util.PlayerUtils; import zeldaswordskills.world.crisis.BossBattle; import zeldaswordskills.world.gen.feature.FairySpawner; /** * * This is the core of each secret dungeon room. It is responsible for verifying the * integrity of the structure and, when the integrity is broken, playing the secret * medley as well as setting the unbreakable blocks to normal stone. * * If set to spawn fairies, each core will have its own maximum spawn count that is * occasionally reset, limiting the number of fairies that can spawn on any given day. * */ public class TileEntityDungeonCore extends TileEntityDungeonStone implements ITickable { /** The bounding box of the associated structure*/ protected StructureBoundingBox box; /** The Fairy Spawner object, if any */ private FairySpawner fairySpawner = null; /** Whether this room has a boss type and can trigger events */ private boolean isBossRoom = false; /** Type of dungeon which this is, based on biome; only used for locked dungeons */ private BossType dungeonType; /** The boss battle event currently in progress */ private BossBattle bossBattle = null; /** Set to true when the door is unlocked; allows for triggering events */ private boolean isOpened; /** The door's block type, if any */ private Block door = null; /** The door block's metadata value used to verify state for IDungeonBlocks */ private int doorMeta = 0; /** Side in which the door is located; always located at centerX or centerZ of that side */ private EnumFacing doorSide = null; /** Set to true when structure is broken; prevents second call of verifyStructure */ private boolean alreadyVerified = false; public TileEntityDungeonCore() {} /** Call after setting the block to set the dungeon's structure bounding box */ public void setDungeonBoundingBox(StructureBoundingBox box) { this.box = box; } /** Returns the bounding box for this dungeon; may be null */ public StructureBoundingBox getDungeonBoundingBox() { return box; } /** Sets the boss type for this boss room */ public void setBossType(BossType type) { dungeonType = type; isBossRoom = true; } /** The BossType of this dungeon */ public BossType getBossType() { return dungeonType; } /** * Sets the "door" block and side for this room * @param block the type of block that is acting as a door to this room * @param face Must be on the HORIZONTAL plane */ public void setDoor(Block block, int meta, EnumFacing face) { door = block; doorMeta = meta; doorSide = (face.getAxis().getPlane() == EnumFacing.Plane.HORIZONTAL ? face : doorSide); } /** Returns true if the tile entity's fairy spawner is not null */ public boolean isSpawner() { return fairySpawner != null; } /** * Sets this tile entity to act as a fairy spawner */ public void setSpawner() { if (box == null) { alreadyVerified = true; setDungeonBoundingBox(new StructureBoundingBox(pos.getX() - 3, pos.getY(), pos.getZ() - 3, pos.getX() + 3, pos.getY() + 4, pos.getZ() + 3)); } fairySpawner = new FairySpawner(this).setMaxFairies(worldObj.rand.nextInt(5) + 2); } /** * Helper method schedules fairy spawner to check for dropped items in the near future */ public void scheduleItemUpdate(EntityPlayer player) { if (isSpawner()) { fairySpawner.scheduleItemUpdate(player); } } /** * Helper method returns true if the amount of rupees was present and consumed */ public boolean consumeRupees(int amount) { return isSpawner() && fairySpawner.consumeRupees(amount); } /** * Returns true every 30 ticks if there is a nearby player and no event is active */ private boolean shouldUpdate() { return (bossBattle == null && worldObj.getTotalWorldTime() % 20 == 0 && worldObj.getClosestPlayer(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, 16.0D) != null); } @Override public void update() { if (worldObj.isRemote) { return; } if (isBossRoom && bossBattle == null) { if (box == null) { ZSSMain.logger.warn(String.format("Boss room at %d/%d/%d missing structure bounding box - dungeon is being disabled", pos.getX(), pos.getY(), pos.getZ())); verifyStructure(true); removeCoreBlock(); } else { EntityPlayer closestPlayer = worldObj.getClosestPlayer(pos.getX() + 0.5D, pos.getY() + 2.5D, pos.getZ() + 0.5D, (double)(box.getXSize() - 2) / 2.0D); if (closestPlayer != null && box.isVecInside(new Vec3i(MathHelper.floor_double(closestPlayer.posX), MathHelper.floor_double(closestPlayer.posY), MathHelper.floor_double(closestPlayer.posZ)))) { if (!isOpened) { // player got in somehow other than the door PlayerUtils.sendTranslatedChat(closestPlayer, "chat.zss.dungeon.sneak_in"); verifyStructure(true); alreadyVerified = true; } bossBattle = dungeonType.getBossBattle(this); if (bossBattle != null) { bossBattle.beginCrisis(worldObj); } } } } if (bossBattle != null) { bossBattle.onUpdate(worldObj); if (bossBattle.isFinished()) { bossBattle = null; removeCoreBlock(); } } else if (shouldUpdate()) { if (!alreadyVerified && box != null && !verifyStructure(false)) { verifyStructure(true); alreadyVerified = true; if (isBossRoom) { isOpened = true; } else { worldObj.playSoundEffect(pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, Sounds.SECRET_MEDLEY, 1.0F, 1.0F); removeCoreBlock(); } } else if (isSpawner()) { fairySpawner.onUpdate(); } } } /** * Removes the core block; if it's a spawner, the tile entity is kept intact * Called only when validation fails during an update, not when block broken */ protected void removeCoreBlock() { EntityPlayer player = worldObj.getClosestPlayer(pos.getX() + 0.5D, pos.getY() + 2.5D, pos.getZ() + 0.5D, 16.0D); if (player != null) { ZSSPlayerInfo info = ZSSPlayerInfo.get(player); if (dungeonType != null) { clearDungeon(); info.addStat(Stats.STAT_BOSS_ROOMS, 1 << dungeonType.ordinal()); player.triggerAchievement(ZSSAchievements.bossBattle); // TODO == 127 (or 255 if the End boss room ever gets made) if (info.getStat(Stats.STAT_BOSS_ROOMS) == 127) { player.triggerAchievement(ZSSAchievements.bossComplete); } if (dungeonType == BossType.DESERT || dungeonType == BossType.OCEAN || dungeonType == BossType.HELL) { player.triggerAchievement(ZSSAchievements.swordPendant); } } else { info.addStat(Stats.STAT_SECRET_ROOMS, 1); player.triggerAchievement(ZSSAchievements.bombsAway); if (info.getStat(Stats.STAT_SECRET_ROOMS) > 49) { player.triggerAchievement(ZSSAchievements.bombJunkie); } if (worldObj.rand.nextFloat() < Config.getRoomSpawnMobChance()) { spawnRandomMob(); } } } if (isSpawner()) { worldObj.setBlockState(pos, worldObj.getBlockState(pos).withProperty(BlockDungeonStone.UNBREAKABLE, Boolean.FALSE), 2); } else { worldObj.setBlockState(pos, getRenderState(), 2); } } /** * Removes the block placed to prevent early plundering of loot */ public void removeHinderBlock() { Vec3i center = box.getCenter(); int x = center.getX() + (doorSide == null ? 0 : doorSide.getFrontOffsetX()); int z = center.getZ() + (doorSide == null ? 0 : doorSide.getFrontOffsetZ()); int y = (worldObj.getBlockState(new BlockPos(x, box.minY + 2, z)).getBlock() == ZSSBlocks.secretStone ? box.minY + 2 : box.minY + 3); worldObj.setBlockToAir(new BlockPos(x, y, z)); } /** * Spawns a mob inside of non-boss secret rooms (call when breached) * Only allows spawning for rooms size 5+ (at least a 3x3x3 space inside) */ private void spawnRandomMob() { if (worldObj.isRemote || box == null || box.getXSize() < 5) { return; } // get the block above the tile to check for liquids Block block = worldObj.getBlockState(pos.up()).getBlock(); EntityLiving mob = null; float f = worldObj.getDifficultyForLocation(pos).getClampedAdditionalDifficulty(); int rarity = worldObj.rand.nextInt(64) - MathHelper.floor_double(f * 2); int type = -1; // for IEntityVariants to set a specific type if (block.getMaterial() == Material.water) { mob = new EntityOctorok(worldObj); type = (rarity < 8 ? 1 : 0); } else if (block.getMaterial() == Material.lava) { mob = new EntityKeese(worldObj).setSpawnSwarm(false); type = (rarity > 7 ? EntityKeese.KeeseType.FIRE.ordinal() : EntityKeese.KeeseType.CURSED.ordinal()); // START rarity 'switch', starting with most likely cases } else if (rarity > 50) { mob = new EntityZombie(worldObj); } else if (rarity > 42) { mob = new EntitySkeleton(worldObj); } else if (rarity > 35) { mob = new EntitySkulltula(worldObj); if (worldObj.rand.nextInt(10) == 0) { type = 1; } } else if (rarity > 28) { mob = (worldObj.rand.nextInt(8) > 1 ? new EntitySpider(worldObj) : new EntityCaveSpider(worldObj)); } else if (rarity > 20) { mob = new EntityCreeper(worldObj); } else if (rarity > 10) { mob = new EntityKeese(worldObj).setSpawnSwarm(false); } else if (rarity > 4) { mob = new EntityChu(worldObj); } else if (rarity > -2) { mob = new EntityWizzrobe(worldObj); // TODO ((EntityWizzrobe) mob).getTeleportAI().setTeleBounds(box); } else { mob = new EntityDarknut(worldObj); } if (mob != null) { mob.setPosition(pos.getX() + 0.5D, pos.getY() + 1.5D, pos.getZ() + 0.5D); mob.onInitialSpawn(worldObj.getDifficultyForLocation(pos), null); if (type > -1 && mob instanceof IEntityVariant) { ((IEntityVariant) mob).setType(type); } worldObj.spawnEntityInWorld(mob); mob.playLivingSound(); } } /** * Opens dungeon door after the boss battle has finished; places Warp Stone if available */ private void clearDungeon() { if (doorSide == null) { doorSide = EnumFacing.EAST; } Vec3i center = box.getCenter(); int x = center.getX(); int z = center.getZ(); switch(doorSide) { case SOUTH: z = box.maxZ - 1; break; case NORTH: z = box.minZ + 1; break; case EAST: x = box.maxX - 1; break; case WEST: x = box.minX + 1; break; default: // UP and DOWN not possible } BlockPos base = new BlockPos(x, box.minY, z); // remove webs blocking door for forest temple if (worldObj.getBlockState(base.up()).getBlock() == Blocks.web) { worldObj.setBlockToAir(base.up()); } if (worldObj.getBlockState(base.up(2)).getBlock() == Blocks.web) { worldObj.setBlockToAir(base.up(2)); } placeOpenDoor((worldObj.getBlockState(base.up()).getBlock().getMaterial().isLiquid() ? 2 : 1)); // Place warp stone if (dungeonType.warpSong != null) { BlockWarpStone.EnumWarpSong warpSong = BlockWarpStone.EnumWarpSong.bySong(dungeonType.warpSong); if (warpSong != null) { worldObj.setBlockState(base, ZSSBlocks.warpStone.getDefaultState().withProperty(BlockWarpStone.WARP_SONG, warpSong), 2); } } } /** * Sets 2 air blocks where door used to be, after dungeon defeated * @param dy Amount to adjust y position above box.minY; usually either 1 or 2 */ private void placeOpenDoor(int dy) { if (doorSide == null) { doorSide = EnumFacing.EAST; } Vec3i center = box.getCenter(); int x = center.getX(); int z = center.getZ(); switch(doorSide) { case SOUTH: z = box.maxZ; break; case NORTH: z = box.minZ; break; case EAST: x = box.maxX; break; case WEST: x = box.minX; break; default: // UP and DOWN not possible } worldObj.setBlockToAir(new BlockPos(x, box.minY + dy, z)); worldObj.setBlockToAir(new BlockPos(x, box.minY + dy + 1, z)); } /** * Returns false if the structure is locked and the door blocks have been removed. * This is only called when the door and bounding box fields are not null. */ private boolean verifyDoor() { Vec3i center = box.getCenter(); int x = center.getX(); int z = center.getZ(); switch(doorSide) { case SOUTH: z = box.maxZ; break; case NORTH: z = box.minZ; break; case EAST: x = box.maxX; break; case WEST: x = box.minX; break; default: ZSSMain.logger.warn(String.format("Verifying door in Dungeon Core with invalid door side at %d/%d/%d", pos.getX(), pos.getY(), pos.getZ())); } for (int y = box.minY; y < box.maxY; ++y) { BlockPos pos = new BlockPos(x, y, z); IBlockState state = worldObj.getBlockState(pos); if (state.getBlock() == door) { return (door instanceof IDungeonBlock) ? ((IDungeonBlock) door).isSameVariant(worldObj, pos, state, doorMeta) : true; } } return false; } /** * Called when core block is broken; re-verifies structure and/or sets all blocks to stone */ public void onBlockBroken() { if (!alreadyVerified) { worldObj.playSoundEffect(pos.getX() + 0.5D, pos.getY() + 1, pos.getZ() + 0.5D, Sounds.SECRET_MEDLEY, 1.0F, 1.0F); verifyStructure(true); } if (isSpawner()) { fairySpawner.onBlockBroken(); } } /** * Returns true if the structure is missing fewer than one block * @param replace if true, all remaining boundary blocks will be replaced with normal stone */ protected boolean verifyStructure(boolean replace) { if (box == null || !hasWorldObj()) { return false; } else if (!replace && door != null) { return verifyDoor(); } int invalid = 0; 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); IBlockState state = worldObj.getBlockState(pos); if (replace) { if (state.getBlock() == ZSSBlocks.secretStone) { worldObj.setBlockState(pos, ((BlockSecretStone.EnumType) state.getValue(BlockSecretStone.VARIANT)).getDroppedBlock().getDefaultState(), 2); } else if (state.getBlock() == ZSSBlocks.dungeonStone) { // don't use instanceof because we don't want to replace dungeon cores TileEntity te = worldObj.getTileEntity(pos); if (te instanceof TileEntityDungeonStone) { worldObj.setBlockState(pos, ((TileEntityDungeonStone) te).getRenderState(), 2); } else { worldObj.setBlockState(pos, ((BlockDungeonStone) state.getBlock()).getDefaultRenderState(false), 2); } } } else if (!(state.getBlock() instanceof IDungeonBlock)) { if (++invalid > 2) { return false; } } } } } } return true; } @Override public void writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); NBTTagCompound tag = new NBTTagCompound(); tag.setBoolean("hasBB", box != null); if (box != null) { tag.setTag("boxBounds", box.toNBTTagIntArray()); } tag.setBoolean("verified", alreadyVerified); tag.setBoolean("isBossRoom", isBossRoom); if (isBossRoom) { tag.setString("dungeonName", dungeonType.getUnlocalizedName()); tag.setBoolean("isOpened", isOpened); tag.setBoolean("hasBossBattle", bossBattle != null); if (bossBattle != null) { bossBattle.writeToNBT(tag); } } tag.setBoolean("hasDoor", door != null); if (door != null) { tag.setInteger("doorBlockId", Block.getIdFromBlock(door)); tag.setInteger("doorMeta", doorMeta); tag.setInteger("doorSide", doorSide.getIndex()); } if (isSpawner()) { fairySpawner.writeToNBT(tag); } compound.setTag("ZSSDungeonCore", tag); } @Override public void readFromNBT(NBTTagCompound compound) { super.readFromNBT(compound); NBTTagCompound tag = compound.getCompoundTag("ZSSDungeonCore"); if (tag.hasKey("boxBounds")) { this.box = new StructureBoundingBox(tag.getIntArray("boxBounds")); } alreadyVerified = tag.hasKey("verified") ? tag.getBoolean("verified") : false; isBossRoom = tag.getBoolean("isBossRoom"); if (isBossRoom) { dungeonType = BossType.getBossType(tag.getString("dungeonName")); isOpened = tag.getBoolean("isOpened"); if (dungeonType == null) { ZSSMain.logger.error(String.format("Error retrieving Boss Type from string %s while loading Dungeon Core from NBT at %d/%d/%d", tag.getString("dungeonName"), pos.getX(), pos.getY(), pos.getZ())); } else if (tag.getBoolean("hasBossBattle")) { bossBattle = dungeonType.getBossBattle(this); if (bossBattle != null) { bossBattle.readFromNBT(tag); } else { ZSSMain.logger.warn(String.format("Error retrieving Boss Battle while loading Dungeon Core from NBT at %d/%d/%d - returned NULL", pos.getX(), pos.getY(), pos.getZ())); } } } if (tag.getBoolean("hasDoor")) { int id = tag.getInteger("doorBlockId"); door = id > 0 ? Block.getBlockById(id) : null; doorMeta = tag.getInteger("doorMeta"); doorSide = EnumFacing.getFront(tag.getInteger("doorSide")); } if (tag.hasKey("FairySpawner")) { fairySpawner = new FairySpawner(this); fairySpawner.readFromNBT(tag); } } }