From a0eedeec86d9c206c98f5cd290821f43377f4463 Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Sat, 16 Dec 2023 17:04:00 +0100 Subject: [PATCH] Initial commit --- .gitignore | 113 +++++++++ pom.xml | 86 +++++++ .../questmobspawns/QuestMobSpawns.java | 218 ++++++++++++++++++ .../listener/MobBlockDamageListener.java | 121 ++++++++++ .../listener/MobDeathListener.java | 27 +++ .../questmobspawns/util/QuestMobHelper.java | 75 ++++++ src/main/resources/plugin.yml | 6 + 7 files changed, 646 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/net/knarcraft/questmobspawns/QuestMobSpawns.java create mode 100644 src/main/java/net/knarcraft/questmobspawns/listener/MobBlockDamageListener.java create mode 100644 src/main/java/net/knarcraft/questmobspawns/listener/MobDeathListener.java create mode 100644 src/main/java/net/knarcraft/questmobspawns/util/QuestMobHelper.java create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e27b98b --- /dev/null +++ b/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + net.knarcraft + QuestMobSpawns + 1.0-SNAPSHOT + jar + + QuestMobSpawns + + + 1.8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 16 + 16 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + false + + + + + + + + src/main/resources + true + + + + + + + spigotmc-repo + https://hub.spigotmc.org/nexus/content/repositories/snapshots/ + + + codemc-repo + https://repo.codemc.io/repository/maven-public/ + + + + + + org.spigotmc + spigot-api + 1.20.1-R0.1-SNAPSHOT + provided + + + me.blackvein.quests + quests-api + 4.8.1 + provided + + + me.blackvein.quests + quests-core + 4.8.1 + provided + + + diff --git a/src/main/java/net/knarcraft/questmobspawns/QuestMobSpawns.java b/src/main/java/net/knarcraft/questmobspawns/QuestMobSpawns.java new file mode 100644 index 0000000..ac91dd6 --- /dev/null +++ b/src/main/java/net/knarcraft/questmobspawns/QuestMobSpawns.java @@ -0,0 +1,218 @@ +package net.knarcraft.questmobspawns; + +import me.blackvein.quests.QuestsAPI; +import me.blackvein.quests.player.IQuester; +import me.blackvein.quests.quests.IQuest; +import me.blackvein.quests.quests.IStage; +import net.knarcraft.questmobspawns.listener.MobBlockDamageListener; +import net.knarcraft.questmobspawns.listener.MobDeathListener; +import net.knarcraft.questmobspawns.util.QuestMobHelper; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Sound; +import org.bukkit.SoundCategory; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +public final class QuestMobSpawns extends JavaPlugin { + + private final Random random = new Random(); + private static QuestMobSpawns plugin; + private QuestsAPI questsAPI; + + public static QuestMobSpawns getPlugin() { + return plugin; + } + + @Override + public void onEnable() { + plugin = this; + // Plugin startup logic + questsAPI = (QuestsAPI) Bukkit.getServer().getPluginManager().getPlugin("Quests"); + if (questsAPI == null) { + return; + } + Bukkit.getPluginManager().registerEvents(new MobDeathListener(), this); + Bukkit.getPluginManager().registerEvents(new MobBlockDamageListener(questsAPI), this); + + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, this::spawnMobs, 200, 30 * 20); + } + + @Override + public void onDisable() { + // Plugin shutdown logic + } + + /** + * Spawns mobs for all kill areas as necessary + */ + private void spawnMobs() { + // Find the players taking each quest + Set questsWithPlayers = new HashSet<>(); + Map> stagePlayerMap = new HashMap<>(); + Collection onlineQuestTakers = questsAPI.getOnlineQuesters(); + + // If no quest takers are online, abort + if (onlineQuestTakers == null || onlineQuestTakers.isEmpty()) { + return; + } + + for (IQuester questPlayer : onlineQuestTakers) { + for (IQuest quest : questPlayer.getQuestData().keySet()) { + questsWithPlayers.add(quest); + IStage playerStage = questPlayer.getCurrentStage(quest); + for (int i = 0; i < playerStage.getMobsToKill().size(); i++) { + // If the player has not killed enough mobs, spawn the mobs + if (questPlayer.getQuestData(quest).getMobNumKilled().get(i) >= playerStage.getMobNumToKill().get(i)) { + continue; + } + + if (!stagePlayerMap.containsKey(playerStage)) { + stagePlayerMap.put(playerStage, new HashSet<>()); + } + stagePlayerMap.get(playerStage).add(questPlayer); + break; + } + } + } + + // If no players are online, abort + if (stagePlayerMap.isEmpty()) { + return; + } + + for (IQuest quest : questsAPI.getLoadedQuests()) { + // Ignore quests with no players + if (!questsWithPlayers.contains(quest)) { + continue; + } + + for (IStage stage : quest.getStages()) { + for (int i = 0; i < stage.getLocationsToKillWithin().size(); i++) { + spawnMobsForStage(stagePlayerMap, quest, stage, i); + } + } + } + } + + private void spawnMobsForStage(Map> questPlayerMap, IQuest quest, IStage stage, int index) { + Location killLocation = stage.getLocationsToKillWithin().get(index).clone(); + int radius = stage.getRadiiToKillWithin().get(index); + + // TODO: De-spawn custom mobs if players are no longer in the area (should probably save the state) + + // If no players are at this stage, abort + if (questPlayerMap.get(stage) == null) { + return; + } + + // Calculate the number of mobs to spawn at once + int maxMobs = radius / 2; + int mobsToSpawn = calculateMobNumber(questPlayerMap, quest, stage, index, killLocation, radius, maxMobs); + if (mobsToSpawn <= 0 || killLocation.getWorld() == null) { + return; + } + + // If there are a bunch of mobs in the area, don't spawn more + EntityType entityType = stage.getMobsToKill().get(index); + int nearby = entitiesInKillZone(killLocation, radius, entityType); + if (nearby >= maxMobs) { + return; + } + + // Spawn entities + for (int counter = 0; counter < mobsToSpawn; counter++) { + int x = (int) Math.round((random.nextInt(radius * 2) - radius) + killLocation.getX()); + int z = (int) Math.round((random.nextInt(radius * 2) - radius) + killLocation.getZ()); + Block spawnBlock = killLocation.getWorld().getHighestBlockAt(x, z).getRelative(BlockFace.UP); + Entity spawnedEntity = killLocation.getWorld().spawnEntity(spawnBlock.getLocation(), entityType); + + // Treat the spawned entity as the result of a spawner for plugins like mcMMO + if (spawnedEntity instanceof LivingEntity) { + CreatureSpawnEvent spawnEvent = new CreatureSpawnEvent((LivingEntity) spawnedEntity, + CreatureSpawnEvent.SpawnReason.SPAWNER); + Bukkit.getPluginManager().callEvent(spawnEvent); + } + + QuestMobHelper.setSpawnedMob(spawnedEntity); + QuestMobHelper.setQuestData(spawnedEntity, quest.getId(), quest.getStages().indexOf(stage), index, + new int[]{killLocation.getBlockX(), killLocation.getBlockY(), killLocation.getBlockZ(), radius}); + // Play a sound so players won't be too surprised + killLocation.getWorld().playSound(spawnBlock.getLocation(), Sound.ITEM_GOAT_HORN_SOUND_2, + SoundCategory.HOSTILE, 1F, 1F); + } + } + + /** + * Counts the number of entities of the necessary type already in the kill zone + * + * @param killLocation

The location of the kill location's centre

+ * @param radius

The radius of the kill zone

+ * @param entityType

The type of entity to kill in the kill zone

+ * @return

+ */ + private int entitiesInKillZone(Location killLocation, int radius, EntityType entityType) { + if (killLocation.getWorld() == null) { + return 0; + } + Collection nearby = killLocation.getWorld().getNearbyEntities(killLocation, radius, 10, radius, + (entity -> entity.getType() == entityType)); + List tooFar = new ArrayList<>(); + for (Entity entity : nearby) { + Location entityLocation = entity.getLocation().clone(); + entityLocation.setY(0); + if (QuestMobHelper.get2dDistance(entityLocation, killLocation) > radius) { + tooFar.add(entity); + } + } + nearby.removeAll(tooFar); + return nearby.size(); + } + + /** + * Calculates the number of mobs that should be spawned at once + * + * @param questPlayerMap

The map for which players are on which stage

+ * @param quest

The quest a mob is spawned for

+ * @param stage

The stage a mob is spawned for

+ * @param index

The index of the task to spawn a mob for

+ * @param killLocation

The centre of the kill location

+ * @param radius

The radius of the kill location

+ * @param maxMobs

The maximum amount of mobs allowed in the kill zone

+ * @return

The number of mobs to spawn

+ */ + private int calculateMobNumber(Map> questPlayerMap, IQuest quest, IStage stage, int index, + Location killLocation, int radius, int maxMobs) { + int mobsToSpawn = 0; + for (IQuester player : questPlayerMap.get(stage)) { + Location playerLocation = player.getPlayer().getLocation().clone(); + playerLocation.setY(0); + if (player.getQuestData(quest).getMobNumKilled().get(index) < stage.getMobNumToKill().get(index) && + QuestMobHelper.get2dDistance(killLocation, playerLocation) < radius) { + mobsToSpawn++; + } + } + if (mobsToSpawn < 1 || killLocation.getWorld() == null) { + return 0; + } + mobsToSpawn *= radius / 5.0; + mobsToSpawn = Math.max(mobsToSpawn, 1); + mobsToSpawn = Math.min(maxMobs, mobsToSpawn); + return mobsToSpawn; + } + +} diff --git a/src/main/java/net/knarcraft/questmobspawns/listener/MobBlockDamageListener.java b/src/main/java/net/knarcraft/questmobspawns/listener/MobBlockDamageListener.java new file mode 100644 index 0000000..7a31577 --- /dev/null +++ b/src/main/java/net/knarcraft/questmobspawns/listener/MobBlockDamageListener.java @@ -0,0 +1,121 @@ +package net.knarcraft.questmobspawns.listener; + +import me.blackvein.quests.QuestsAPI; +import me.blackvein.quests.player.IQuester; +import me.blackvein.quests.quests.IQuest; +import me.blackvein.quests.quests.IStage; +import net.knarcraft.questmobspawns.util.QuestMobHelper; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityBreakDoorEvent; +import org.bukkit.event.entity.EntityChangeBlockEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.entity.EntityInteractEvent; +import org.bukkit.event.entity.EntityTargetLivingEntityEvent; + +public class MobBlockDamageListener implements Listener { + + private final QuestsAPI questsAPI; + + public MobBlockDamageListener(QuestsAPI questsAPI) { + this.questsAPI = questsAPI; + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onExplode(EntityExplodeEvent event) { + if (QuestMobHelper.isSpawnedMob(event.getEntity())) { + event.blockList().clear(); + event.setCancelled(false); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onBlockChange(EntityChangeBlockEvent event) { + cancelIfQuestMob(event, event.getEntity()); + } + + @EventHandler + public void onDoorBreak(EntityBreakDoorEvent event) { + cancelIfQuestMob(event, event.getEntity()); + } + + @EventHandler + public void onEntityTarget(EntityTargetLivingEntityEvent event) { + Entity entity = event.getEntity(); + if (!QuestMobHelper.isSpawnedMob(entity) || !(event.getTarget() instanceof Player player)) { + return; + } + + if (!hasQuest(entity, player)) { + event.setCancelled(true); + } else { + // Cancel the targeting if the player is outside the kill-zone + int[] killLocationData = QuestMobHelper.getKillZoneLocation(entity); + Location killLocation = new Location(entity.getWorld(), killLocationData[0], killLocationData[1], + killLocationData[2]); + event.setCancelled(QuestMobHelper.get2dDistance(player.getLocation(), killLocation) > killLocationData[3]); + } + } + + @EventHandler + public void onTrample(EntityInteractEvent event) { + cancelIfQuestMob(event, event.getEntity()); + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onEntityDamage(EntityDamageByEntityEvent event) { + if (!QuestMobHelper.isSpawnedMob(event.getDamager())) { + return; + } + + Entity damagingEntity = event.getDamager(); + if (event.getEntity() instanceof Player player) { + event.setCancelled(!hasQuest(damagingEntity, player)); + } else { + event.setCancelled(true); + } + } + + /** + * Checks whether the given player has the quest the given entity was spawned for + * + * @param entity

The entity with stored quest data

+ * @param player

A player

+ * @return

True if the player has the quest stored in the entity

+ */ + private Boolean hasQuest(Entity entity, Player player) { + IQuester questPlayer = questsAPI.getQuester(player.getUniqueId()); + IQuest quest = getQuest(QuestMobHelper.getQuestId(entity)); + if (quest == null) { + // Remove the entity if the quest it belongs to no longer exists + entity.remove(); + return false; + } + IStage stage = quest.getStage(QuestMobHelper.getStageId(entity)); + int index = QuestMobHelper.getIndex(entity); + return questPlayer.getQuestData().containsKey(quest) && questPlayer.getCurrentStage(quest) == stage && + questPlayer.getQuestData(quest).getMobNumKilled().get(index) < stage.getMobNumToKill().get(index); + } + + private IQuest getQuest(String questId) { + for (IQuest quest : questsAPI.getLoadedQuests()) { + if (quest.getId().equalsIgnoreCase(questId)) { + return quest; + } + } + return null; + } + + private void cancelIfQuestMob(Cancellable cancellable, Entity entity) { + if (QuestMobHelper.isSpawnedMob(entity)) { + cancellable.setCancelled(true); + } + } + +} diff --git a/src/main/java/net/knarcraft/questmobspawns/listener/MobDeathListener.java b/src/main/java/net/knarcraft/questmobspawns/listener/MobDeathListener.java new file mode 100644 index 0000000..b3e492e --- /dev/null +++ b/src/main/java/net/knarcraft/questmobspawns/listener/MobDeathListener.java @@ -0,0 +1,27 @@ +package net.knarcraft.questmobspawns.listener; + +import net.knarcraft.questmobspawns.util.QuestMobHelper; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityDropItemEvent; + +public class MobDeathListener implements Listener { + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onMobDeath(EntityDeathEvent deathEvent) { + if (QuestMobHelper.isSpawnedMob(deathEvent.getEntity())) { + deathEvent.getDrops().clear(); + deathEvent.setDroppedExp(0); + } + } + + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onMobDrop(EntityDropItemEvent dropEvent) { + if (QuestMobHelper.isSpawnedMob(dropEvent.getEntity())) { + dropEvent.setCancelled(true); + } + } + +} diff --git a/src/main/java/net/knarcraft/questmobspawns/util/QuestMobHelper.java b/src/main/java/net/knarcraft/questmobspawns/util/QuestMobHelper.java new file mode 100644 index 0000000..67dabd3 --- /dev/null +++ b/src/main/java/net/knarcraft/questmobspawns/util/QuestMobHelper.java @@ -0,0 +1,75 @@ +package net.knarcraft.questmobspawns.util; + +import net.knarcraft.questmobspawns.QuestMobSpawns; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +public class QuestMobHelper { + + private static final NamespacedKey questMobKey = new NamespacedKey(QuestMobSpawns.getPlugin(), "questMob"); + private static final NamespacedKey questIdKey = new NamespacedKey(QuestMobSpawns.getPlugin(), "questId"); + private static final NamespacedKey stageIdKey = new NamespacedKey(QuestMobSpawns.getPlugin(), "stageId"); + private static final NamespacedKey mobIndexKey = new NamespacedKey(QuestMobSpawns.getPlugin(), "mobIndex"); + private static final NamespacedKey killZoneLocationKey = new NamespacedKey(QuestMobSpawns.getPlugin(), "killZoneLocationKey"); + + public static boolean isSpawnedMob(Entity entity) { + return Boolean.TRUE.equals(entity.getPersistentDataContainer().get(questMobKey, PersistentDataType.BOOLEAN)); + } + + public static void setSpawnedMob(Entity entity) { + entity.getPersistentDataContainer().set(questMobKey, PersistentDataType.BOOLEAN, true); + } + + public static String getQuestId(Entity entity) { + return entity.getPersistentDataContainer().get(questIdKey, PersistentDataType.STRING); + } + + public static Integer getStageId(Entity entity) { + return entity.getPersistentDataContainer().get(stageIdKey, PersistentDataType.INTEGER); + } + + public static Integer getIndex(Entity entity) { + return entity.getPersistentDataContainer().get(mobIndexKey, PersistentDataType.INTEGER); + } + + /** + * Gets information about the mob's kill zone + * + * @param entity

The entity to get data for

+ * @return

An array containing x,y,z,radius

+ */ + public static int[] getKillZoneLocation(Entity entity) { + return entity.getPersistentDataContainer().get(killZoneLocationKey, PersistentDataType.INTEGER_ARRAY); + } + + /** + * Stores quest info to a mob + * + * @param entity

The entity to store quest data to

+ * @param questId

The id of the quest the mob was spawned for

+ * @param killLocationInfo

Information about the kill location (x, y, z, range)

+ */ + public static void setQuestData(Entity entity, String questId, int stageId, int mobIndex, int[] killLocationInfo) { + PersistentDataContainer persistentDataContainer = entity.getPersistentDataContainer(); + persistentDataContainer.set(questIdKey, PersistentDataType.STRING, questId); + persistentDataContainer.set(stageIdKey, PersistentDataType.INTEGER, stageId); + persistentDataContainer.set(mobIndexKey, PersistentDataType.INTEGER, mobIndex); + persistentDataContainer.set(killZoneLocationKey, PersistentDataType.INTEGER_ARRAY, killLocationInfo); + } + + /** + * Gets the 2d (x,z) distance between two locations + * + * @param location1

The first location

+ * @param location2

The second location

+ * @return

The 2d distance between the locations

+ */ + public static double get2dDistance(Location location1, Location location2) { + return Math.sqrt(Math.pow(location2.getX() - location1.getX(), 2) + + Math.pow(location2.getZ() - location1.getZ(), 2)); + } + +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..4afcbe3 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,6 @@ +name: QuestMobSpawns +version: '${project.version}' +main: net.knarcraft.questmobspawns.QuestMobSpawns +api-version: '1.20' +depend: + - Quests