package net.deliciousreya.minecraftportal.model import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import org.bukkit.Location import org.bukkit.block.Block import org.bukkit.util.Vector import net.deliciousreya.minecraftportal.extensions.* import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos import org.bukkit.Material import org.bukkit.Material.* import org.bukkit.Particle import org.bukkit.Sound import org.bukkit.block.BlockFace import org.bukkit.block.data.Directional import org.bukkit.block.data.Openable import org.bukkit.entity.Entity import org.bukkit.scheduler.BukkitRunnable import org.bukkit.util.BoundingBox val MINERAL_TYPES: ImmutableMap = ImmutableMap.Builder() .put(COAL_BLOCK, BLACK_STAINED_GLASS) .put(REDSTONE_BLOCK, RED_STAINED_GLASS) .put(LAPIS_BLOCK, BLUE_STAINED_GLASS) .put(GOLD_BLOCK, YELLOW_STAINED_GLASS) .put(DIAMOND_BLOCK, CYAN_STAINED_GLASS) .put(EMERALD_BLOCK, GREEN_STAINED_GLASS) .put(IRON_BLOCK, GRAY_STAINED_GLASS) .put(QUARTZ_BLOCK, WHITE_STAINED_GLASS) .put(TERRACOTTA, ORANGE_STAINED_GLASS) .put(SMOOTH_RED_SANDSTONE, PINK_STAINED_GLASS) .put(END_STONE_BRICKS, LIME_STAINED_GLASS) .put(PRISMARINE, LIGHT_BLUE_STAINED_GLASS) .put(BRICKS, MAGENTA_STAINED_GLASS) .put(PURPUR_BLOCK, PURPLE_STAINED_GLASS) .put(POLISHED_GRANITE, BROWN_STAINED_GLASS) .put(POLISHED_ANDESITE, LIGHT_GRAY_STAINED_GLASS) .build() val DOOR_TYPES: ImmutableSet = ImmutableSet.of(ACACIA_DOOR, BIRCH_DOOR, DARK_OAK_DOOR, IRON_DOOR, JUNGLE_DOOR, OAK_DOOR, SPRUCE_DOOR) fun findPortalFrameConnectedTo(block:Block, state: PortalFrame.State): PortalFrame? { for (blockSet in state.blockSets) { for (direction in PortalFrame.ExitDirection.values()) { for (offset in blockSet.offsets(direction)) { val scanResult = checkPortalFrameAt( block.location.subtract(offset), direction, state ) if (scanResult != null) { return scanResult } } } } return null } /** Detects whether there is a portal frame in the given state at the given location, extending in the given direction. */ fun checkPortalFrameAt(location:Location, direction: PortalFrame.ExitDirection, state: PortalFrame.State): PortalFrame? { val mineral = (location + direction.mineralOffset).block if (mineral.type !in state.mineralBlocks) { return null } val color = (location + direction.colorOffset).block if (color.type !in state.colorBlocks) { return null } for (vector in direction.glassOffsets) { val block = (location + vector).block if (block.type !in state.glassBlocks) { return null } } for (vector in direction.doorOffsets) { val block = (location + vector).block val door = block.blockData if (block.type !in state.doorBlocks || (door is Directional && door.facing != direction.doorDirection)) { return null } } return PortalFrame(location, direction) } fun PortalSaveDataProtos.Portal.Direction.toDirection(): PortalFrame.ExitDirection { for (direction in PortalFrame.ExitDirection.values()) { if (direction.protoEnum == this) { return direction } } throw IllegalStateException("Bad enum value: $this") } /** Information about a portal frame. */ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: ExitDirection) { enum class State(val glassBlocks: ImmutableSet, val colorBlocks: ImmutableSet, val doorBlocks: ImmutableSet, val mineralBlocks: ImmutableSet) { ACTIVE(ImmutableSet.copyOf(MINERAL_TYPES.values), ImmutableSet.copyOf(COLOR_MAPPING.keys), DOOR_TYPES, ImmutableSet.copyOf(MINERAL_TYPES.keys)), INACTIVE(ImmutableSet.of(GLASS), ImmutableSet.builder().addAll(COLOR_MAPPING.keys).add(GLASS).build(), DOOR_TYPES, ImmutableSet.copyOf(MINERAL_TYPES.keys)); val allValidBlocks:ImmutableSet = ImmutableSet.Builder().addAll(glassBlocks).addAll(doorBlocks).addAll(mineralBlocks).addAll(colorBlocks).build() data class BlockSet(val blockTypes: Set, val offsets: (direction: ExitDirection) -> List) val blockSets = listOf( BlockSet(glassBlocks, offsets = { it.glassOffsets }), BlockSet(doorBlocks, offsets = { it.doorOffsets }), BlockSet(colorBlocks, offsets = { listOf(it.colorOffset) }), BlockSet(mineralBlocks, offsets = { listOf(it.mineralOffset) }) ) } /** The direction a player faces when exiting the portal. */ enum class ExitDirection(val toRight: Vector, val toBack: Vector, val doorDirection: BlockFace, val protoEnum: PortalSaveDataProtos.Portal.Direction) { NORTH(Vector(1, 0, 0), Vector(0, 0, 1), BlockFace.SOUTH, PortalSaveDataProtos.Portal.Direction.NORTH), SOUTH(Vector(-1, 0, 0), Vector(0, 0, -1), BlockFace.NORTH, PortalSaveDataProtos.Portal.Direction.SOUTH), EAST(Vector(0, 0, 1), Vector(-1, 0, 0), BlockFace.WEST, PortalSaveDataProtos.Portal.Direction.EAST), WEST(Vector(0, 0, -1), Vector(1, 0, 0), BlockFace.EAST, PortalSaveDataProtos.Portal.Direction.WEST); val glassOffsets: ImmutableList = ImmutableList.of( // check corners first: // bottom left, front ZERO, // top right, back UP * 3 + toBack + toRight * 2, // bottom right, front toRight * 2, // top right, front UP * 3 + toRight * 2, // top left, front UP * 3, // bottom left, back toBack, // top left, back UP * 3 + toBack, // bottom right, back toBack + toRight * 2, // then do front walls // left front wall UP, UP * 2, // right front wall UP + toRight * 2, UP * 2 + toRight * 2, // now do the rest of the back // left back wall UP + toBack, UP * 2 + toBack, // center back wall toBack + toRight, UP + toBack + toRight, UP * 2 + toBack + toRight, UP * 3 + toBack + toRight, // right back wall UP + toBack + toRight * 2, UP * 2 + toBack + toRight * 2 ) val mineralOffset: Vector = toRight val colorOffset: Vector = UP * 3 + toRight val doorOffsets: ImmutableList = ImmutableList.of( UP + toRight, UP * 2 + toRight ) fun toProto():PortalSaveDataProtos.Portal.Direction { return this.protoEnum } } val portalCenter = lowerLeftFrontCorner + direction.doorOffsets[0] val midCenter = portalCenter + MID_BLOCK val fullPortalBoundingBox = BoundingBox.of(lowerLeftFrontCorner.block, (lowerLeftFrontCorner + UP * 3 + direction.toRight * 2 + direction.toBack).block) val portalInsideBoundingBox = BoundingBox.of(portalCenter.block, (portalCenter + UP).block) /** Gets the worldless location (direction and position vectors) relative to the center of the chamber, rotating so that the exit is north. */ fun getRelativeLocationFromAbsoluteLocation(location: Location): Location { if (portalCenter.world != location.world) { throw IllegalArgumentException("can't get relative position from ${portalCenter.world} to ${location.world}") } if (portalCenter.world == null) { throw IllegalArgumentException("How did we end up with a relative portal center??") } val oldDirection = location.direction val translatedPosition = location.toVector() - (portalCenter + MID_BLOCK_BOTTOM).toVector() val newLocation = Location(null, translatedPosition.dot(direction.toRight), translatedPosition.dot(UP), translatedPosition.dot(direction.toBack), 0f, 0f) newLocation.direction = Vector(oldDirection.dot(direction.toRight), oldDirection.dot(UP), oldDirection.dot(direction.toBack)) return newLocation } fun getAbsoluteLocationFromRelativeLocation(location: Location): Location { if (location.world != null) { throw IllegalArgumentException("can't get absolute position from already absolute position") } val world = portalCenter.world ?: throw IllegalArgumentException("How did we end up with a relative portal center??") val oldDirection = location.direction val oldPosition = location.toVector() val rotatedPosition = Vector(oldPosition.dot(direction.toRight), oldPosition.dot(UP), oldPosition.dot(direction.toBack)) val newLocation = (rotatedPosition + portalCenter.toVector() + MID_BLOCK_BOTTOM).toLocation(world) newLocation.direction = Vector(oldDirection.dot(direction.toRight), oldDirection.dot(UP), oldDirection.dot(direction.toBack)) return newLocation } val mineral = (lowerLeftFrontCorner + direction.mineralOffset).block.type var color: Material get() = (lowerLeftFrontCorner + direction.colorOffset).block.type set(newColor) { (lowerLeftFrontCorner + direction.colorOffset).block.type = newColor } fun open() { val block = portalCenter.block val data = block.blockData if (data is Openable) { data.isOpen = true block.blockData = data } } fun close() { val block = portalCenter.block val data = block.blockData if (data is Openable) { data.isOpen = false block.blockData = data } } fun ejectEntities() { val world = portalCenter.world ?: return for (entity in world.getNearbyEntities(portalInsideBoundingBox)) { // kick 'em out the front! entity.teleport(entity.location - direction.toBack) } } fun activate() { val mineral = (lowerLeftFrontCorner + direction.mineralOffset).block for (offset in direction.glassOffsets) { (lowerLeftFrontCorner + offset).block.type = MINERAL_TYPES.getOrDefault(mineral.type, GLASS) } portalCenter.world?.playSound(midCenter, Sound.ENTITY_EVOKER_CAST_SPELL, 1f, 1f) portalCenter.world?.spawnParticle(Particle.SPELL, midCenter, 75) open() } fun deactivate() { for (offset in direction.glassOffsets) { (lowerLeftFrontCorner + offset).block.type = GLASS } portalCenter.world?.playSound(midCenter, Sound.ENTITY_ILLUSIONER_CAST_SPELL, 1f, 1f) portalCenter.world?.spawnParticle(Particle.SMOKE_NORMAL, midCenter, 75) } fun isActivePortal(): Boolean { return checkPortalFrameAt(lowerLeftFrontCorner, direction, State.ACTIVE) == this } fun playPortalsLinkedEffect() { portalCenter.world?.playSound(midCenter, Sound.BLOCK_BEACON_ACTIVATE, 1f, 1f) portalCenter.world?.spawnParticle(Particle.SPELL, midCenter, 30) } fun playPortalsUnlinkedEffect() { portalCenter.world?.playSound(midCenter, Sound.BLOCK_BEACON_DEACTIVATE, 1f, 1f) portalCenter.world?.spawnParticle(Particle.SMOKE_NORMAL, midCenter, 30) } fun playTeleporterActivatedEffect() { portalCenter.world?.playSound(midCenter, Sound.BLOCK_PORTAL_TRIGGER, 1f, 1f) } fun playTeleportTravelEffect() { portalCenter.world?.playSound(midCenter, Sound.BLOCK_PORTAL_TRAVEL, 1f, 1f) portalCenter.world?.spawnParticle(Particle.FIREWORKS_SPARK, midCenter, 75) } fun getEntities(): Collection { return portalCenter.world?.getNearbyEntities(portalInsideBoundingBox) ?: ImmutableList.of() } fun playTeleporterActiveEffect() { portalCenter.world?.spawnParticle(Particle.SPELL_MOB_AMBIENT, midCenter, 10) } fun playTeleportCanceledEffect() { portalCenter.world?.playSound(midCenter, Sound.BLOCK_LAVA_EXTINGUISH, 1f, 1f) portalCenter.world?.spawnParticle(Particle.SMOKE_NORMAL, midCenter, 30) } inner class MakePortalSound : BukkitRunnable() { override fun run() { portalCenter.world?.playSound(portalCenter + MID_BLOCK, Sound.BLOCK_PORTAL_AMBIENT, 0.1f, 0f) } } inner class MakePortalSparkles : BukkitRunnable() { override fun run() { portalCenter.world?.spawnParticle(Particle.PORTAL, portalCenter + MID_BLOCK, 20) } } }