diff --git a/TODO b/TODO index 86ab0cc..09afaf1 100644 --- a/TODO +++ b/TODO @@ -1,7 +1,3 @@ -* teleport from one to the other a set time after entering a teleportation chamber and closing the door -* automatically open the door after effects wear off -* teleport everyone inside the portal, not just the person who closed the door -* interrupt teleportation if portal is destroyed mid-teleportation, or if a new portal is constructed during teleportation, changing the destination * cancel teleportation if someone moves out of the teleporter during teleportation (maybe prevent them from starting?) * save any effects that would be overwritten by teleportation (including to disk!), and put them back when the player opens the door or arrives -* add portal particles and ambient sounds to the teleporter when portals are paired \ No newline at end of file +* fix facing and location when teleporting in different directions \ No newline at end of file diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt index 3078a35..9e31644 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/MinecraftPortalPlugin.kt @@ -7,18 +7,19 @@ import net.deliciousreya.minecraftportal.model.DOOR_TYPES import net.deliciousreya.minecraftportal.model.PortalDataStore import net.deliciousreya.minecraftportal.model.PortalFrame import net.deliciousreya.minecraftportal.model.findPortalFrameConnectedTo -import org.bukkit.Particle import org.bukkit.Sound import org.bukkit.block.Block import org.bukkit.block.data.type.Door +import org.bukkit.entity.Entity +import org.bukkit.entity.Player import org.bukkit.event.EventHandler import org.bukkit.event.Listener import org.bukkit.event.block.* import org.bukkit.event.entity.EntityChangeBlockEvent import org.bukkit.event.entity.EntityExplodeEvent import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.event.player.PlayerMoveEvent import org.bukkit.plugin.java.JavaPlugin -import org.bukkit.potion.PotionEffect import org.bukkit.potion.PotionEffectType class MinecraftPortalPlugin() : JavaPlugin(), Listener @@ -28,6 +29,8 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener override fun onEnable() { super.onEnable() portals.loadFromAndAutoSaveTo(server, dataFolder.resolve("portal_data.binproto"), dataFolder.resolve("portal_data.binproto.bak")) + portals.validateWorldOnStartup(logger) + portals.launchEffectsOnStartup(this) server.pluginManager.registerEvents(this, this) } @@ -51,6 +54,7 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener if (otherPortal != null) { newPortal.open() otherPortal.open() + portals.startEffectsFor(newPortal, this) } else { newPortal.ejectEntities() newPortal.close() @@ -113,28 +117,26 @@ class MinecraftPortalPlugin() : JavaPlugin(), Listener return } if (!door.isOpen) { - // door is about to be opened, so undrug the player - e.player.stopSound(Sound.BLOCK_PORTAL_TRIGGER) - val otherPortal = portals.getOtherPortal(frame = portal) ?: return - otherPortal.open() - /* e.player.removePotionEffect(PotionEffectType.BLINDNESS) - e.player.removePotionEffect(PotionEffectType.CONFUSION) - e.player.removePotionEffect(PotionEffectType.INVISIBILITY) */ + portals.stopTeleporting(portal) } else { - // door is about to be closed, so drug the player - val otherPortal = portals.getOtherPortal(frame = portal) ?: return - otherPortal.close() - val relativeLocation = portal.getRelativeLocationFromAbsoluteLocation(location) - val destination = otherPortal.getAbsoluteLocationFromRelativeLocation(relativeLocation) - relativeLocation.world?.playSound(e.player.location, Sound.BLOCK_PORTAL_TRIGGER, 20f, 1f) - destination.world?.playSound(destination, Sound.BLOCK_PORTAL_TRAVEL, 20f, 1f) - e.player.teleport(destination) - /* e.player.addPotionEffect(PotionEffect(PotionEffectType.CONFUSION, 200, 1, false, false, false)) - e.player.addPotionEffect(PotionEffect(PotionEffectType.BLINDNESS, 200, 1, false, false, false)) - e.player.addPotionEffect(PotionEffect(PotionEffectType.INVISIBILITY, 200, 1, false, false, false)) */ + portals.startTeleporting(portal, this) } } + fun onPlayerMove(e: PlayerMoveEvent) { + val player = e.player + val oldLocation = e.from + val portal = portals.isLocationInPortalChamber(oldLocation) ?: return + val newLocation = e.to ?: return + if (oldLocation.world != newLocation.world || oldLocation.toVector() !in portal.portalInsideBoundingBox) { + cancelTeleportFor(player) + } + } + + private fun cancelTeleportFor(entity: Entity) { + + } + fun onDestroyedBlock(block: Block) { if (block.type !in PortalFrame.State.ACTIVE.allValidBlocks) { return diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt index 74eb00a..5d6287d 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/extensions/Vector.kt @@ -33,4 +33,8 @@ operator fun Vector.times(v:Vector):Vector { operator fun Vector.div(v:Vector):Vector { return this.clone().divide(v) +} + +operator fun Vector.unaryMinus(): Vector { + return this * -1 } \ No newline at end of file diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt index 62a0795..cb71219 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/Portal.kt @@ -7,16 +7,35 @@ import net.deliciousreya.minecraftportal.extensions.toProto import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos import org.bukkit.Material import org.bukkit.Server +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitRunnable import java.lang.IllegalArgumentException fun PortalSaveDataProtos.Portal.toPortal(server: Server, portalTypes: Map): Portal { val portalType = portalTypes[mineral.toMaterial()] ?: throw IllegalArgumentException("No portal type for $mineral") val result = Portal(PortalFrame(lowerLeftFrontCorner.toLocation(server), exitDirection.toDirection()), portalType) - // TODO: assert that the saved portal and mineral match the actual state of the world return result } class Portal(val frame: PortalFrame, val type: PortalType) { + var portalSounds: PortalFrame.MakePortalSound? = null + var portalSparkles: PortalFrame.MakePortalSparkles? = null + + fun startEffects(plugin: Plugin) { + stopEffects() + portalSounds = frame.MakePortalSound() + portalSparkles = frame.MakePortalSparkles() + portalSounds?.runTaskTimer(plugin, 0, 120) + portalSparkles?.runTaskTimer(plugin, 0, 20) + } + + fun stopEffects() { + portalSounds?.cancel() + portalSparkles?.cancel() + portalSounds = null + portalSparkles = null + } + fun toProto(): PortalSaveDataProtos.Portal { return PortalSaveDataProtos.Portal.newBuilder() .setLowerLeftFrontCorner(frame.lowerLeftFrontCorner.toProto()) diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt index 147208b..659fc0f 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalDataStore.kt @@ -7,11 +7,13 @@ import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos import org.bukkit.Location import org.bukkit.Material import org.bukkit.Server +import org.bukkit.plugin.Plugin import java.io.File import java.io.IOException import java.lang.IllegalArgumentException +import java.util.logging.Logger -class PortalDataStore { +class PortalDataStore (){ var saveDataTo: File? = null var useBackup: File? = null val portalTypes: ImmutableMap @@ -87,6 +89,52 @@ class PortalDataStore { this.useBackup = backupFile } + fun validateWorldOnStartup(logger: Logger) { + for (portalType in this.portalTypes.values) { + val oldPortal = portalType.oldestActivePortal + val newPortal = portalType.newestActivePortal + if (newPortal != null && !validatePortal(newPortal)) { + logger.warning("Portal $newPortal was no longer valid!") + portalType.newestActivePortal = null + } + if (oldPortal != null && !validatePortal(oldPortal)) { + logger.warning("Portal $oldPortal was no longer valid!") + portalType.oldestActivePortal = portalType.newestActivePortal + portalType.newestActivePortal = null + } + } + } + + fun validatePortal(portal: Portal): Boolean { + return portal.frame.mineral == portal.type.mineral && portal.frame.isActivePortal() + } + + fun launchEffectsOnStartup(plugin: Plugin) { + for (portalType in this.portalTypes.values) { + startEffectsFor(portalType, plugin) + } + } + + fun startEffectsFor(portal: PortalFrame, plugin: Plugin) { + startEffectsFor(getPortalType(portal), plugin) + } + + fun startEffectsFor(portalType: PortalType, plugin: Plugin) { + val oldPortal = portalType.oldestActivePortal + val newPortal = portalType.newestActivePortal + if (portalType.isPaired) { + oldPortal?.startEffects(plugin) + oldPortal?.frame?.open() + newPortal?.startEffects(plugin) + newPortal?.frame?.open() + } else if (!portalType.isEmpty) { + oldPortal?.stopEffects() + oldPortal?.frame?.close() + newPortal?.stopEffects() + newPortal?.frame?.close() + } + } + fun unload() { this.clear() this.saveDataTo = null @@ -112,17 +160,17 @@ class PortalDataStore { /** Marks the new portal as active, and removes the one that needs to be deactivated, if any. */ fun activateAndReplacePortal(frame: PortalFrame): PortalFrame? { val type = getPortalType(frame) - val result = type.addOrReplacePortal(Portal(frame, type))?.frame + val result = type.addOrReplacePortal(Portal(frame, type)) onAfterChanged() - return result + return result?.frame } /** Deactivates the given portal. Returns the other portal if it needs to be closed. */ fun deactivatePortal(frame: PortalFrame): PortalFrame? { val type = getPortalType(frame) - val result = type.removePortalWithFrame(frame)?.frame + val result = type.removePortalWithFrame(frame) onAfterChanged() - return result + return result?.frame } fun getPortalType(frame: PortalFrame): PortalType { @@ -138,6 +186,8 @@ class PortalDataStore { /** Forgets all the data without actually affecting the world. */ fun clear() { for (portalType in this.portalTypes.values) { + portalType.oldestActivePortal?.stopEffects() + portalType.newestActivePortal?.stopEffects() portalType.clear() } } @@ -175,4 +225,12 @@ class PortalDataStore { } return builder.build() } + + fun startTeleporting(portal: PortalFrame, plugin: Plugin) { + getPortalType(portal).beginTeleportation(plugin) + } + + fun stopTeleporting(portal: PortalFrame) { + getPortalType(portal).cancelTeleportation() + } } \ No newline at end of file diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt index 90d913d..16b1faa 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalFrame.kt @@ -15,6 +15,8 @@ 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() @@ -174,6 +176,7 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: ExitDi } 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) @@ -238,8 +241,7 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: ExitDi for (offset in direction.glassOffsets) { (lowerLeftFrontCorner + offset).block.type = MINERAL_TYPES.getOrDefault(mineral.type, BROWN_STAINED_GLASS) } - val midCenter = portalCenter + MID_BLOCK - portalCenter.world?.playSound(midCenter, Sound.BLOCK_BEACON_ACTIVATE, 20f, 1f) + portalCenter.world?.playSound(midCenter, Sound.ENTITY_EVOKER_CAST_SPELL, 20f, 1f) portalCenter.world?.spawnParticle(Particle.SPELL, midCenter, 75) open() } @@ -248,8 +250,54 @@ data class PortalFrame(val lowerLeftFrontCorner: Location, val direction: ExitDi for (offset in direction.glassOffsets) { (lowerLeftFrontCorner + offset).block.type = GLASS } - val midCenter = portalCenter + MID_BLOCK - portalCenter.world?.playSound(midCenter, Sound.BLOCK_BEACON_DEACTIVATE, 20f, 1f) + portalCenter.world?.playSound(midCenter, Sound.ENTITY_ILLUSIONER_CAST_SPELL, 20f, 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, 20f, 1f) + portalCenter.world?.spawnParticle(Particle.SPELL, midCenter, 30) + } + + fun playPortalsUnlinkedEffect() { + portalCenter.world?.playSound(midCenter, Sound.BLOCK_BEACON_DEACTIVATE, 20f, 1f) + portalCenter.world?.spawnParticle(Particle.SMOKE_NORMAL, midCenter, 30) + } + + fun playTeleporterActivatedEffect() { + portalCenter.world?.playSound(midCenter, Sound.BLOCK_PORTAL_TRIGGER, 20f, 1f) + } + + fun playTeleportTravelEffect() { + portalCenter.world?.playSound(midCenter, Sound.BLOCK_PORTAL_TRAVEL, 20f, 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, 20f, 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, 3f, 0f) + } + } + inner class MakePortalSparkles : BukkitRunnable() { + override fun run() { + portalCenter.world?.spawnParticle(Particle.PORTAL, portalCenter + MID_BLOCK, 20) + } + } } \ No newline at end of file diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt index 717f594..b581443 100644 --- a/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/PortalType.kt @@ -3,6 +3,7 @@ package net.deliciousreya.minecraftportal.model import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos import org.bukkit.Location import org.bukkit.Material +import org.bukkit.plugin.Plugin import java.lang.IllegalArgumentException import java.lang.IllegalStateException @@ -39,6 +40,12 @@ class PortalType(val mineral: Material) { } else -> { val replacedPortal = newestActivePortal + replacedPortal?.stopEffects() + replacedPortal?.frame?.playPortalsUnlinkedEffect() + if (replacedPortal != null) { + cancelTeleportation() + } + portal.frame.playPortalsLinkedEffect() this.newestActivePortal = portal replacedPortal } @@ -69,6 +76,19 @@ class PortalType(val mineral: Material) { this.newestActivePortal = newerPortal } + private var teleportation: Teleportation? = null + + fun beginTeleportation(plugin: Plugin) { + val teleportation = Teleportation(this, plugin) + teleportation.start() + this.teleportation = teleportation + } + + fun cancelTeleportation() { + teleportation?.cancelTeleportation() + teleportation = null + } + fun clear() { oldestActivePortal = null newestActivePortal = null @@ -83,11 +103,26 @@ class PortalType(val mineral: Material) { * returns it. Otherwise (if that frame wasn't for either portal or if there are none left) returns null. */ fun removePortalWithFrame(frame: PortalFrame): Portal? { + val isPaired = this.isPaired if (oldestActivePortal?.frame == frame) { + oldestActivePortal?.stopEffects() + newestActivePortal?.stopEffects() + if (isPaired) { + oldestActivePortal?.frame?.playPortalsUnlinkedEffect() + newestActivePortal?.frame?.playPortalsUnlinkedEffect() + } + cancelTeleportation() oldestActivePortal = newestActivePortal } else if (newestActivePortal?.frame != frame) { return null } + oldestActivePortal?.stopEffects() + newestActivePortal?.stopEffects() + cancelTeleportation() + if (isPaired) { + oldestActivePortal?.frame?.playPortalsUnlinkedEffect() + newestActivePortal?.frame?.playPortalsUnlinkedEffect() + } newestActivePortal = null return oldestActivePortal } diff --git a/src/main/kotlin/net/deliciousreya/minecraftportal/model/Teleportation.kt b/src/main/kotlin/net/deliciousreya/minecraftportal/model/Teleportation.kt new file mode 100644 index 0000000..a47dd78 --- /dev/null +++ b/src/main/kotlin/net/deliciousreya/minecraftportal/model/Teleportation.kt @@ -0,0 +1,118 @@ +package net.deliciousreya.minecraftportal.model + +import net.deliciousreya.minecraftportal.proto.PortalSaveDataProtos +import org.bukkit.Location +import org.bukkit.entity.Entity +import org.bukkit.entity.LivingEntity +import org.bukkit.plugin.Plugin +import org.bukkit.potion.PotionEffect +import org.bukkit.potion.PotionEffectType +import org.bukkit.scheduler.BukkitRunnable + +class Teleportation(val portalType: PortalType, val plugin: Plugin) { + val leftEntities = LinkedHashSet() + val rightEntities = LinkedHashSet() + val allEntities = LinkedHashSet() + var currentTask: BukkitRunnable? = null + var sparklesTask: DoSparkles? = null + val leftPortal = portalType.newestActivePortal!! + val rightPortal = portalType.oldestActivePortal!! + + fun cancelFor(entity: Entity) { + leftEntities.remove(entity) + rightEntities.remove(entity) + allEntities.remove(entity) + if (allEntities.isEmpty()) { + cancelTeleportation() + } + } + + fun cancelEffectsOn(entity: Entity) { + if (entity is LivingEntity) { + entity.removePotionEffect(PotionEffectType.BLINDNESS) + entity.removePotionEffect(PotionEffectType.CONFUSION) + } + } + + fun cancelTeleportation() { + leftPortal.frame.open() + rightPortal.frame.open() + leftPortal.frame.playTeleportCanceledEffect() + rightPortal.frame.playTeleportCanceledEffect() + cancelEffectsOnAll() + leftEntities.clear() + rightEntities.clear() + allEntities.clear() + currentTask?.cancel() + currentTask = null + sparklesTask?.cancel() + sparklesTask = null + } + + fun cancelEffectsOnAll() { + for (entity in allEntities) { + cancelEffectsOn(entity) + } + } + + fun start() { + leftEntities.addAll(leftPortal.frame.getEntities()) + rightEntities.addAll(rightPortal.frame.getEntities()) + allEntities.addAll(leftEntities) + allEntities.addAll(rightEntities) + for (entity in allEntities) { + if (entity is LivingEntity) { + entity.addPotionEffect(PotionEffect(PotionEffectType.CONFUSION, 200, 1, false, false, false)) + entity.addPotionEffect(PotionEffect(PotionEffectType.BLINDNESS, 200, 1, false, false, false)) + } + } + leftPortal.frame.close() + rightPortal.frame.close() + leftPortal.frame.playTeleporterActivatedEffect() + rightPortal.frame.playTeleporterActivatedEffect() + val task = DoTeleport() + task.runTaskLater(plugin, 100) + currentTask = task + val sparkles = DoSparkles() + sparkles.runTaskTimer(plugin, 10, 15) + sparklesTask = sparkles + } + + inner class DoSparkles : BukkitRunnable() { + override fun run() { + leftPortal.frame.playTeleporterActiveEffect() + rightPortal.frame.playTeleporterActiveEffect() + } + } + + inner class DoTeleport : BukkitRunnable() { + override fun run() { + for(leftEntity in leftEntities) { + val relativeLocation = leftPortal.frame.getRelativeLocationFromAbsoluteLocation(leftEntity.location) + val destination = rightPortal.frame.getAbsoluteLocationFromRelativeLocation(relativeLocation) + leftEntity.teleport(destination) + } + for(rightEntity in rightEntities) { + val relativeLocation = rightPortal.frame.getRelativeLocationFromAbsoluteLocation(rightEntity.location) + val destination = leftPortal.frame.getAbsoluteLocationFromRelativeLocation(relativeLocation) + rightEntity.teleport(destination) + } + leftPortal.frame.playTeleportTravelEffect() + rightPortal.frame.playTeleportTravelEffect() + val task = OpenDoors() + task.runTaskLater(plugin, 100) + currentTask = task + } + } + + inner class OpenDoors: BukkitRunnable() { + override fun run() { + leftPortal.frame.open() + rightPortal.frame.open() + cancelEffectsOnAll() + currentTask = null + sparklesTask?.cancel() + sparklesTask = null + } + } +} \ No newline at end of file