diff --git a/README.md b/README.md index f66699473..2c4beb06d 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,12 @@ to load and save all of your fields to the file automatically) - Automatic YamlStaticConfig (SimpleSettings & SimpleLocalization) loading (use @AutoStaticConfig) - Automatic ConfigSerializable loading (use @AutoSerialize) - Custom recipes' wrapper (use SimpleCraft and CraftingHandler) +- Custom bosses (use SimpleBoss and SpawnedBoss) - Better CompMetadata - now you can store and remove temporary and PERSISTENT metadata for all objects that support that regardless of your MC version. - Some small but useful features (new Logger, ItemCreator attribute modifiers) +- More legacy-compatible methods (in Remain class) - Some fixes (e.g. Boss-bar HEX colors) diff --git a/src/main/java/org/mineacademy/fo/RandomUtil.java b/src/main/java/org/mineacademy/fo/RandomUtil.java index b07a060d8..84403de48 100644 --- a/src/main/java/org/mineacademy/fo/RandomUtil.java +++ b/src/main/java/org/mineacademy/fo/RandomUtil.java @@ -66,7 +66,6 @@ public static Random getRandom() { * Return true if the given percent was matched * * @param percent the percent, from 0 to 100 - * @return */ public static boolean chance(final long percent) { return chance((int) percent); @@ -76,9 +75,17 @@ public static boolean chance(final long percent) { * Return true if the given percent was matched * * @param percent the percent, from 0 to 100 - * @return */ public static boolean chance(final int percent) { + return chance((double) percent); + } + + /** + * Return true if the given percent was matched + * + * @param percent the percent, from 0.0 to 100.0 + */ + public static boolean chance(final double percent){ return random.nextDouble() * 100D < percent; } diff --git a/src/main/java/org/mineacademy/fo/boss/SimpleBoss.java b/src/main/java/org/mineacademy/fo/boss/SimpleBoss.java new file mode 100644 index 000000000..950c39e25 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SimpleBoss.java @@ -0,0 +1,705 @@ +package org.mineacademy.fo.boss; + +import lombok.Getter; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityCombustEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.mineacademy.fo.MinecraftVersion; +import org.mineacademy.fo.annotation.AutoConfig; +import org.mineacademy.fo.menu.model.ItemCreator; +import org.mineacademy.fo.remain.*; +import org.mineacademy.fo.settings.YamlConfig; + +import java.io.File; +import java.util.*; + +import static org.mineacademy.fo.boss.SimpleBossEquipment.Part.*; + +/** + * Simple wrapper class for custom mobs and bosses.
+ * Here you can: + * + * + * @author Rubix327 + */ +@Getter +@AutoConfig +@SuppressWarnings({"unchecked", "unused", "deprecation"}) +public class SimpleBoss extends YamlConfig implements Listener { + + /** + * Loaded singleton instances of the bosses. + * Only used in internal purposes (to correctly load bosses after restart). + * Do not use at your own. + */ + private static final Map instances = new HashMap<>(); + private static boolean SAVE_TO_FILES = false; + /** + * The metadata key under which all bosses store their IDs. + */ + private static @NotNull String META_KEY = "Fo_Boss"; + /** + * The metadata key which all boss spawners have. + */ + private static @NotNull String SPAWNER_KEY = "Fo_Boss_Spawner"; + /** + * The path where all boss' files are located. + */ + private static @NotNull String PATH = "bosses/"; + + protected final @NotNull String id; + protected @NotNull EntityType type; + protected String name = "Unnamed"; + protected Double health; + protected Double damage; + protected SimpleBossEquipment equipment = new SimpleBossEquipment(); + @AutoConfig(autoLoad = false) + protected LinkedHashSet dropTables = new LinkedHashSet<>(); + @AutoConfig(autoLoad = false) + protected LinkedHashSet potionEffects = new LinkedHashSet<>(); + @AutoConfig(false) + protected LinkedHashMap skills = new LinkedHashMap<>(); + protected boolean hasVanillaDrops = true; + protected boolean hasEquipmentDrops = true; + protected Integer experienceDrop = 0; + protected String passenger; + protected boolean isBaby = false; + protected boolean isRemovedWhenFarAway = true; + protected boolean isBurnOnSunlight = true; + protected boolean isGlowing = false; + protected CompColor glowColor = CompColor.WHITE; + protected int taskDelay = 0; + protected int taskPeriod = -1; + protected SpawnerData spawnerData; + + public SimpleBoss(@NotNull String id, @NotNull EntityType type) { + String fullPath = getSavePath() + id + ".yml"; + + this.id = id; + this.type = type; + this.spawnerData = SpawnerData.of(getType()).build(); + register(this); + + // Call class type to check if the getEntityType is not null and is alive + getClassType(); + + // Set a file to get no errors when saving is disabled + this.setFile(new File(fullPath)); + + // Load fields from the file + if (SAVE_TO_FILES) { + loadConfiguration(getDefaultFile(), fullPath); + } + + // Setup from the child class + setup(); + // Save to the file + save(); + } + + protected String getDefaultFile(){ + return null; + } + + protected void setup(){} + + /* + * Setters for every field + */ + + public final void setType(@NotNull EntityType type) { + this.type = type; + this.save(); + } + + public final void setName(String name) { + this.name = name; + this.save(); + } + + public final void setHealth(Double health) { + this.health = health; + this.save(); + } + + public void setDamage(Double damage) { + this.damage = damage; + this.save(); + } + + public final void setEquipment(EntityEquipment equipment) { + ItemStack offHand = new ItemStack(Material.AIR); + if (MinecraftVersion.atLeast(MinecraftVersion.V.v1_9)){ + offHand = equipment.getItemInOffHand(); + } + ItemStack[] items = new ItemStack[]{ + equipment.getHelmet(), + equipment.getChestplate(), + equipment.getLeggings(), + equipment.getBoots(), + equipment.getItemInHand(), + offHand + }; + this.equipment = new SimpleBossEquipment(items); + this.save(); + } + + public final void setEquipment(SimpleBossEquipment equipment){ + this.equipment = equipment; + this.save(); + } + + public final void setDropTables(LinkedHashSet dropTables) { + this.dropTables = dropTables; + this.save(); + } + + public final void setDropTables(SimpleDropTable... tables){ + this.dropTables = new LinkedHashSet<>(Arrays.asList(tables)); + this.save(); + } + + public final void addDropTable(SimpleDropTable table){ + this.dropTables.add(table); + this.save(); + } + + public final void removeDropTable(SimpleDropTable table){ + this.dropTables.remove(table); + this.save(); + } + + public final void setSkills(LinkedHashMap skills){ + this.skills = skills; + this.save(); + } + + public final void addSkill(SimpleBossSkill skill){ + this.skills.put(skill.getId(), skill); + this.save(); + } + + public final void removeSkill(SimpleBossSkill skill){ + this.skills.remove(skill.getId()); + this.save(); + } + + public final void setPotionEffects(LinkedHashSet potionEffects) { + this.potionEffects = potionEffects; + this.save(); + } + + public final void addPotionEffect(PotionEffect effect){ + this.potionEffects.add(new SimplePotionEffect(effect)); + this.save(); + } + + public final void addPotionEffect(SimplePotionEffect effect){ + this.potionEffects.add(effect); + this.save(); + } + + public final void removePotionEffect(SimplePotionEffect effect){ + this.potionEffects.remove(effect); + this.save(); + } + + public final void setHasVanillaDrops(boolean hasVanillaDrops) { + this.hasVanillaDrops = hasVanillaDrops; + this.save(); + } + + public final void setHasEquipmentDrops(boolean hasEquipmentDrops) { + this.hasEquipmentDrops = hasEquipmentDrops; + this.save(); + } + + public final void setExperienceDrop(Integer experienceDrop) { + this.experienceDrop = experienceDrop; + this.save(); + } + + @Nullable + public final SimpleBoss getPassenger(){ + return SimpleBoss.get(this.passenger); + } + + public final void setPassenger(SimpleBoss passenger) { + this.passenger = passenger.getId(); + this.save(); + } + + public final void setBaby(boolean baby) { + isBaby = baby; + this.save(); + } + + public final void setRemoveWhenFarAway(boolean removedWhenFarAway) { + isRemovedWhenFarAway = removedWhenFarAway; + this.save(); + } + + public final void setBurnOnSunlight(boolean burnOnSunlight) { + isBurnOnSunlight = burnOnSunlight; + this.save(); + } + + public final void setGlowing(boolean glowing) { + isGlowing = glowing; + this.save(); + } + + public final void setGlowColor(CompColor glowColor) { + this.glowColor = glowColor; + this.save(); + } + + public final void setTaskDelay(int taskDelay) { + this.taskDelay = taskDelay; + this.save(); + } + + public final void setTaskPeriod(int taskPeriod) { + this.taskPeriod = taskPeriod; + this.save(); + } + + public final void setSpawnerData(SpawnerData spawnerData) { + this.spawnerData = spawnerData; + this.save(); + } + + /** + * Get the boss attack damage. + * By default, it would be the same as in vanilla Minecraft with the boss' weapon taken into account + * @param event EntityDamageEvent + * @return the attack damage + */ + public double getDamage(EntityDamageByEntityEvent event){ + if (this.damage != null) return damage; + return Remain.getFinalDamage(event); + } + + /** + * Called when the boss attacks another entity. + * @param event EntityDeathEvent + */ + protected void onAttack(Entity target, EntityDamageByEntityEvent event){} + + /** + * Called when the boss gets damage from another entity. + * @param event EntityDeathEvent + */ + protected void onGetDamage(Entity damager, EntityDamageByEntityEvent event){} + + /** + * Called when the boss dies + * @param event EntityDeathEvent + */ + protected void onDie(EntityDeathEvent event){} + + /** + * Called when the boss is fired by the sun (zombie, skeleton, etc.). + * @param event EntityCombustEvent + */ + protected void onBurn(EntityCombustEvent event){} + + /** + * Called when someone places a spawner of this boss. + * @param spawner the spawner + * @param event BlockPlaceEvent + */ + protected void onSpawnerPlace(CreatureSpawner spawner, BlockPlaceEvent event){} + + /** + * Called when the boss is spawned.
+ * This method is the last opportunity to somehow change the boss entity. + * @param entity the instance of entity being spawned + */ + protected void onSpawn(LivingEntity entity){} + + /** + * Called when the boss is loaded when a chunk is loaded by a player.
+ * Not called when the boss is spawned. + * @param entity the entity of this boss + */ + protected void onChunkLoad(LivingEntity entity){} + + /** + * Called when the boss is despawned by the server due to the player is too far away. + * @param entity the despawned entity + */ + protected void onDespawn(LivingEntity entity){} + + /** + * Run the task of this boss on the given entity.
+ * When overriding, here you should specify only single execution of the task (not a timer!). + * @param entity the entity. + */ + public void runTask(LivingEntity entity){} + + /** + * Get the class type of specified EntityType.
+ * This method includes checks if EntityType is not null and alive. + * @return the class type + */ + @SuppressWarnings("ConstantValue") + final Class getClassType(){ + if (getType() == null){ + throw new IllegalStateException("Method getType must not return null. " + + "Please override this method manually and return a living EntityType."); + } + if (!type.isAlive() || !type.isSpawnable()){ + throw new IllegalArgumentException("Boss entity type must be alive (LivingEntity) and spawnable."); + } + return (Class) type.getEntityClass(); + } + + /** + * Spawn the entity in the world at the given location. + * @param location the location. + * @return the SpawnedBoss instance + * @throws IllegalArgumentException if world in the given location is undefined + */ + public final SpawnedBoss spawn(Location location) throws IllegalArgumentException{ + if (location.getWorld() == null) throw new IllegalArgumentException("Invalid world given (not set in Location?)"); + LivingEntity ent = location.getWorld().spawn(location, getClassType(), entity -> { + // Add boss meta-tag to the entity + CompMetadata.setMetadata(entity, META_KEY, this.getId()); + + // Let the user customize this entity at his will + onSpawn(entity); + + // Set the specified display name + if (this.getName() != null) { + Remain.setCustomName(entity, this.getName()); + } + + // Set the max health of the entity + if (getHealth() != null && getHealth() > 0){ + CompAttribute.GENERIC_MAX_HEALTH.set(entity, this.getHealth()); + } + + // Make the entity a baby + Remain.setBabyOrAdult(entity, isBaby()); + + // Remove equipment drops if they are disabled + // or set chances from SimpleBossEquipment if enabled + EntityEquipment eq = entity.getEquipment(); + SimpleBossEquipment sbe = getEquipment(); + boolean hasEq = isHasEquipmentDrops(); + if (eq != null){ + eq.setHelmetDropChance(hasEq ? sbe.getChanceOrDefault(HELMET) : 0); + eq.setChestplateDropChance(hasEq ? sbe.getChanceOrDefault(CHESTPLATE) : 0); + eq.setLeggingsDropChance(hasEq ? sbe.getChanceOrDefault(LEGGINGS) : 0); + eq.setBootsDropChance(hasEq ? sbe.getChanceOrDefault(BOOTS) : 0); + eq.setItemInHandDropChance(hasEq ? sbe.getChanceOrDefault(MAIN_HAND) : 0); + if (MinecraftVersion.atLeast(MinecraftVersion.V.v1_9)) { + eq.setItemInOffHandDropChance(hasEq ? sbe.getChanceOrDefault(OFF_HAND) : 0); + } + } + + // Set equipment from the boss to the entity + entity.getEquipment().setArmorContents(sbe.getArmorContents()); + entity.getEquipment().setItemInHand(sbe.getMainHand()); + if (MinecraftVersion.atLeast(MinecraftVersion.V.v1_9)){ + entity.getEquipment().setItemInOffHand(sbe.getOffHand()); + } + + // Add potion effects + if (potionEffects != null){ + for (SimplePotionEffect effect : potionEffects){ + entity.addPotionEffect(effect.toPotionEffect()); + } + } + + // Remove entity when far away from player + entity.setRemoveWhenFarAway(isRemovedWhenFarAway()); + + // Add glowing effect to the boss + if (isGlowing()){ + Remain.setGlowing(entity, getGlowColor()); + } + + // Lastly, add passenger + SimpleBoss pass = getPassenger(); + if (pass != null){ + Remain.addPassenger(entity, pass.spawn(entity.getLocation()).getEntity()); + } + }); + + // Load the boss to the plugin + return SpawnedBoss.load(ent); + } + + /** + * Spawn boss with the given ID on the given location. + * If there is no boss with that ID, it will throw NPE. + * @param id the id of the boss + * @param location the location + * @return new SpawnedBoss instance + * @throws NullPointerException if no boss with that ID found + */ + public static SpawnedBoss spawn(String id, Location location) throws NullPointerException{ + SimpleBoss boss = instances.get(id); + if (boss == null){ + throw new NullPointerException("No boss found with the given name."); + } + return boss.spawn(location); + } + + /** + * Get the spawner item for this boss. + * @return the spawner + */ + public final ItemStack getSpawner(){ + String name = getName() + " Spawner"; + if (getSpawnerData().getItemName() != null && !getSpawnerData().getItemName().isEmpty()){ + name = getSpawnerData().getItemName().replace("%name%", getName()); + } + return ItemCreator.of(CompMaterial.SPAWNER).name(name).metadata(SPAWNER_KEY, getId()).make(); + } + + /** + * Kill all bosses within a radius of 200 blocks from the given location. + * @param location the location + * @return amount of mobs killed + */ + public static int killAll(Location location){ + return killAll(location, 200, null); + } + + /** + * Kill specific bosses within a given radius from the given location. + * @param location the location + * @param radius radius + * @param boss the boss, specify null to ignore boss' class + * @return amount of mobs killed + */ + public static int killAll(Location location, int radius, SimpleBoss boss){ + World world = location.getWorld(); + if (world == null) return 0; + int counter = 0; + for (Entity entity : world.getNearbyEntities(location, radius, radius, radius, + e -> e instanceof LivingEntity && !(e instanceof Player))){ + SpawnedBoss spawned = SpawnedBoss.get((LivingEntity) entity); + if (spawned != null){ + if (boss != null && !boss.equals(spawned.getBoss())) continue; + spawned.remove(); + counter++; + } + } + return counter; + } + + /** + * Check if the entity is a boss of this specific class. + * @param entity the entity + * @return is specific boss + */ + public final boolean is(Entity entity){ + return isBoss(entity) && getId().equals(getMeta(entity)); + } + + /** + * Check if the entity is a SimpleBoss.
+ * To check if the entity is spawned and loaded use {@link SpawnedBoss#isLoaded(Entity)}. + * @param entity the entity + * @return true if the entity has {@link #META_KEY} and is a LivingEntity + */ + public static boolean isBoss(Entity entity){ + if (!(entity instanceof LivingEntity)) return false; + return exists(getMeta(entity)); + } + + public static boolean exists(String id){ + return id != null && getIDs().contains(id); + } + + /** + * Try to get the SimpleBoss instance of this entity. + * @param entity the entity + * @return the SimpleBoss or null if the entity is not a boss ({@link #isBoss}) + */ + public static SimpleBoss get(Entity entity){ + if (!(entity instanceof LivingEntity)) return null; + return get(getMeta(entity)); + } + + /** + * Get metadata from the given entity by the {@link #META_KEY} key. + * @param entity the entity + * @return the id of the boss or null if it is not a boss + */ + public static String getMeta(Entity entity){ + return CompMetadata.getMetadata(entity, META_KEY); + } + + /** + * Try to get the SimpleBoss instance from the ID. + * @param simpleBossID ID of the SimpleBoss + * @return the SimpleBoss or null if this ID does not belong to any SimpleBoss + */ + public static SimpleBoss get(String simpleBossID){ + return instances.get(simpleBossID); + } + + /** + * Get all bosses' ids. + * @return the list of IDs + */ + public static List getIDs(){ + return new ArrayList<>(instances.keySet()); + } + + /** + * Get all instances of SimpleBosses. + * @return the list of instances. + */ + public static List getInstances(){ + return new ArrayList<>(instances.values()); + } + + /** + * Register the given boss. + * @param boss the boss + */ + private static void register(SimpleBoss boss){ + instances.put(boss.id, boss); + } + + /** + * Get the metadata key under which all bosses store their IDs. + * @return the key + */ + @NotNull + public static String getMetaKey(){ + return META_KEY; + } + + /** + * Set the metadata key under which all bosses store their IDs. + * @param newTag the new key + */ + public static void setMetaKey(@NotNull String newTag){ + META_KEY = newTag; + } + + /** + * Get the metadata key which all boss spawners have. + * @return the key + */ + @NotNull + public static String getSpawnerKey(){ + return SPAWNER_KEY; + } + + /** + * Set the metadata key which all boss spawners have. + * @param newKey the new key + */ + public static void setSpawnerKey(@NotNull String newKey){ + SPAWNER_KEY = newKey; + } + + + /** + * Get the path where all boss' files are located. + * @return the path + */ + @NotNull + public static String getSavePath() { + return PATH; + } + + /** + * Set the path where all boss' files are located. + * @param path the new path + */ + public static void setSavePath(@NotNull String path) { + SimpleBoss.PATH = path; + } + + public static void setSaveEnabled(boolean isEnabled){ + SAVE_TO_FILES = isEnabled; + } + + public static boolean isSaveEnabled(){ + return SAVE_TO_FILES; + } + + @Override + protected void onSave() { + this.set("skills", this.skills.values()); + } + + @Override + protected void onLoad() { + this.dropTables = getSet("drop_tables", SimpleDropTable.class, new LinkedHashSet<>()); + this.potionEffects = getSet("potion_effects", SimplePotionEffect.class, new LinkedHashSet<>()); + List skills = getList("skills", SimpleBossSkill.class); + for (SimpleBossSkill sk : skills){ + this.skills.put(sk.getId(), sk); + } + } + + @Override + public String toString() { + return "SimpleBoss{" + + "id='" + id + '\'' + + ", type=" + type + + ", name='" + name + '\'' + + ", health=" + health + + ", equipment=" + equipment + + ", dropTables=" + dropTables + + ", potionEffects=" + potionEffects + + ", skills=" + skills + + ", hasVanillaDrops=" + hasVanillaDrops + + ", hasEquipmentDrops=" + hasEquipmentDrops + + ", experienceDrop=" + experienceDrop + + ", passenger='" + passenger + '\'' + + ", isBaby=" + isBaby + + ", isRemovedWhenFarAway=" + isRemovedWhenFarAway + + ", isBurnOnSunlight=" + isBurnOnSunlight + + ", isGlowing=" + isGlowing + + ", glowColor=" + glowColor + + ", taskDelay=" + taskDelay + + ", taskPeriod=" + taskPeriod + + ", spawnerData=" + spawnerData + + '}'; + } + + @Override + public boolean equals(Object obj) { + return super.equals(obj) + && obj instanceof SimpleBoss + && ((SimpleBoss) obj).getId().equals(this.getId()); + } + + @Override + public int hashCode() { + return this.getId().hashCode(); + } +} \ No newline at end of file diff --git a/src/main/java/org/mineacademy/fo/boss/SimpleBossEquipment.java b/src/main/java/org/mineacademy/fo/boss/SimpleBossEquipment.java new file mode 100644 index 000000000..5ec0d8550 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SimpleBossEquipment.java @@ -0,0 +1,168 @@ +package org.mineacademy.fo.boss; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.mineacademy.fo.collection.SerializedMap; +import org.mineacademy.fo.model.ConfigSerializable; +import org.mineacademy.fo.model.ItemStackSerializer; +import org.mineacademy.fo.remain.CompMaterial; + +/** + * Represents equipment of a boss.
+ * Here you can set every item of armor, items in hands and their chances to drop.
+ * If you use no-args constructor, all items would be empty and the chances are 10% by default. + * + * @author Rubix327 + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public final class SimpleBossEquipment implements ConfigSerializable { + private ItemStack helmet = (new ItemStack(Material.AIR)); + private ItemStack chestplate = (new ItemStack(Material.AIR)); + private ItemStack leggings = (new ItemStack(Material.AIR)); + private ItemStack boots = (new ItemStack(Material.AIR)); + private ItemStack mainHand = (new ItemStack(Material.AIR)); + private ItemStack offHand = (new ItemStack(Material.AIR)); + private Float helmetDropChance; + private Float chestplateDropChance; + private Float leggingsDropChance; + private Float bootsDropChance; + private Float mainHandDropChance; + private Float offHandDropChance; + + public SimpleBossEquipment(ItemStack[] items) { + if (items.length != 6){ + throw new IllegalArgumentException("Simple Boss Equipment must consist of 6 items."); + } + + this.helmet = new ItemStack(items[0]); + this.chestplate = new ItemStack(items[1]); + this.leggings = new ItemStack(items[2]); + this.boots = new ItemStack(items[3]); + this.mainHand = new ItemStack(items[4]); + this.offHand = new ItemStack(items[5]); + } + + public ItemStack[] getArmorContents(){ + return new ItemStack[]{ + boots, + leggings, + chestplate, + helmet + }; + } + + /** + * Get the chance if it is set. If not, get the default Minecraft drop chance - 8.5%. + * @param part the body part + * @return chance or 0.085f + */ + public float getChanceOrDefault(Part part){ + Float val = getUnsafeChance(part); + return val == null ? 0.085f : val / 100; + } + + /** + * Get the drop chance of the given body part.
+ * May return null if not set. Use {@link #getChanceOrDefault(Part)} to get safe value. + * @param part the body part + * @return chance or null + */ + public Float getUnsafeChance(Part part){ + switch (part){ + case HELMET: return getHelmetDropChance(); + case CHESTPLATE: return getChestplateDropChance(); + case LEGGINGS: return getLeggingsDropChance(); + case BOOTS: return getBootsDropChance(); + case MAIN_HAND: return getMainHandDropChance(); + case OFF_HAND: return getOffHandDropChance(); + default: return null; + } + } + + @Override + public SerializedMap serialize() { + SerializedMap map = new SerializedMap(); + + if (!CompMaterial.isAir(helmet)){ + map.put("helmet", new ItemStackSerializer(helmet, helmetDropChance)); + } + if (!CompMaterial.isAir(chestplate)){ + map.put("chestplate", new ItemStackSerializer(chestplate)); + } + if (!CompMaterial.isAir(leggings)){ + map.put("leggings", new ItemStackSerializer(leggings)); + } + if (!CompMaterial.isAir(boots)){ + map.put("boots", new ItemStackSerializer(boots)); + } + if (!CompMaterial.isAir(mainHand)){ + map.put("main_hand", new ItemStackSerializer(mainHand)); + } + if (!CompMaterial.isAir(offHand)){ + map.put("off_hand", new ItemStackSerializer(offHand)); + } + + return map; + } + + public static SimpleBossEquipment deserialize(SerializedMap map){ + ItemStackSerializer helmet = getItem(map, "helmet"); + ItemStackSerializer chestplate = getItem(map, "chestplate"); + ItemStackSerializer leggings = getItem(map, "leggings"); + ItemStackSerializer boots = getItem(map, "boots"); + ItemStackSerializer mainHand = getItem(map, "main_hand"); + ItemStackSerializer offHand = getItem(map, "off_hand"); + + Float helmetC = getChance(helmet); + Float chestplateC = getChance(chestplate); + Float leggingsC = getChance(leggings); + Float bootsC = getChance(boots); + Float mainHandC = getChance(mainHand); + Float offHandC = getChance(offHand); + return new SimpleBossEquipment(helmet.toItemStack(), chestplate.toItemStack(), leggings.toItemStack(), + boots.toItemStack(), mainHand.toItemStack(), offHand.toItemStack(), + helmetC, chestplateC, leggingsC, bootsC, mainHandC, offHandC); + } + + private static ItemStackSerializer getItem(SerializedMap map, String path){ + return map.get(path, ItemStackSerializer.class, ItemStackSerializer.empty()); + } + + private static Float getChance(ItemStackSerializer item){ + return item == null ? null : item.getDropChance(); + } + + @Override + public String toString() { + return "SimpleBossEquipment{" + + "helmet=" + helmet + + ", chestplate=" + chestplate + + ", leggings=" + leggings + + ", boots=" + boots + + ", mainHand=" + mainHand + + ", offHand=" + offHand + + ", helmetChance=" + helmetDropChance + + ", chestplateChance=" + chestplateDropChance + + ", leggingsChance=" + leggingsDropChance + + ", bootsChance=" + bootsDropChance + + ", mainHandChance=" + mainHandDropChance + + ", offHandChance=" + offHandDropChance + + '}'; + } + + public enum Part{ + HELMET, + CHESTPLATE, + LEGGINGS, + BOOTS, + MAIN_HAND, + OFF_HAND + } +} diff --git a/src/main/java/org/mineacademy/fo/boss/SimpleBossListener.java b/src/main/java/org/mineacademy/fo/boss/SimpleBossListener.java new file mode 100644 index 000000000..784301279 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SimpleBossListener.java @@ -0,0 +1,154 @@ +package org.mineacademy.fo.boss; + +import org.bukkit.block.CreatureSpawner; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityCombustEvent; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.SpawnerSpawnEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.mineacademy.fo.Common; +import org.mineacademy.fo.RandomUtil; +import org.mineacademy.fo.Valid; +import org.mineacademy.fo.annotation.AutoRegister; +import org.mineacademy.fo.model.ItemStackSerializer; +import org.mineacademy.fo.remain.CompMetadata; + +/** + * This class listens to events related to SimpleBosses.
+ * See {@link SimpleBoss} and {@link SpawnedBoss}. + * + * @author Rubix327 + */ +@AutoRegister +public final class SimpleBossListener implements Listener { + + @EventHandler + public void onBossAttack(EntityDamageByEntityEvent event){ + Entity damager = event.getDamager(); + Entity victim = event.getEntity(); + + if (SimpleBoss.isBoss(damager)){ + SimpleBoss boss = SimpleBoss.get(damager); + if (boss.getDamage(event) > 0){ + event.setDamage(boss.getDamage(event)); + } + boss.onAttack(victim, event); + } + } + + @EventHandler + public void onBossGetDamage(EntityDamageByEntityEvent event){ + Entity damager = event.getDamager(); + Entity victim = event.getEntity(); + + if (SimpleBoss.isBoss(victim)){ + SimpleBoss boss = SimpleBoss.get(victim); + + boss.onGetDamage(damager, event); + Common.tell(damager, "" + event.getFinalDamage()); + } + } + + @EventHandler + public void onBossDie(EntityDeathEvent event){ + LivingEntity entity = event.getEntity(); + if (!SimpleBoss.isBoss(entity)) return; + + SimpleBoss boss = SimpleBoss.get(entity); + + if (boss.getExperienceDrop() != null){ + event.setDroppedExp(boss.getExperienceDrop()); + } + + if (!boss.isHasVanillaDrops()){ + event.getDrops().clear(); + } + + // Adding custom drops + if (!Valid.isNullOrEmpty(boss.getDropTables())){ + for (SimpleDropTable table : boss.getDropTables()){ + if (!RandomUtil.chance(table.getChance())) continue; + + for (ItemStackSerializer item : table.getTable()){ + if (RandomUtil.chance(item.getDropChance())){ + event.getDrops().add(item.toItemStack()); + } + } + + break; + } + } + + // Call onDie method so the user can customize this event + boss.onDie(event); + + // Unload the boss from the memory + SpawnedBoss.unload(entity); + } + + @EventHandler + public void onBurnSunlight(EntityCombustEvent event){ + Entity entity = event.getEntity(); + if (SimpleBoss.isBoss(entity)){ + SimpleBoss boss = SimpleBoss.get(entity); + if (!boss.isBurnOnSunlight()){ + event.setCancelled(true); + return; + } + boss.onBurn(event); + } + } + + @EventHandler + public void onChunkLoad(ChunkLoadEvent event){ + for (Entity entity : event.getChunk().getEntities()){ + if (entity instanceof LivingEntity){ + LivingEntity ent = ((LivingEntity) entity); + SpawnedBoss boss = SpawnedBoss.load(ent); + if (boss != null){ + boss.getBoss().onChunkLoad(ent); + } + } + } + } + + @EventHandler + public void onSpawnerPlace(BlockPlaceEvent event){ + String meta = CompMetadata.getMetadata(event.getPlayer().getInventory().getItemInMainHand(), SimpleBoss.getSpawnerKey()); + if (SimpleBoss.exists(meta)){ + SimpleBoss boss = SimpleBoss.get(meta); + SpawnerData data = boss.getSpawnerData(); + CreatureSpawner spawner = (CreatureSpawner) event.getBlock().getState(); + + // Setting values from SimpleBoss#getSpawnerData() to the spawner + spawner.setSpawnedType(data.getType()); + spawner.setDelay(data.getStartDelay()); + spawner.setMinSpawnDelay(data.getMinSpawnDelay()); + spawner.setMaxSpawnDelay(data.getMaxSpawnDelay()); + spawner.setSpawnCount(data.getSpawnCount()); + spawner.setMaxNearbyEntities(data.getMaxNearbyEntities()); + spawner.setRequiredPlayerRange(data.getRequiredPlayerRange()); + spawner.setSpawnRange(data.getSpawnRange()); + spawner.update(); + + // Adding metadata to the block after the spawner is updated + CompMetadata.setMetadata(event.getBlock(), SimpleBoss.getSpawnerKey(), meta); + boss.onSpawnerPlace(spawner, event); + } + } + + @EventHandler + public void onSpawnerSpawn(SpawnerSpawnEvent event){ + String meta = CompMetadata.getMetadata(event.getSpawner().getBlock(), SimpleBoss.getSpawnerKey()); + if (SimpleBoss.exists(meta)){ + event.setCancelled(true); + SimpleBoss.get(meta).spawn(event.getLocation()); + } + } + +} diff --git a/src/main/java/org/mineacademy/fo/boss/SimpleBossSkill.java b/src/main/java/org/mineacademy/fo/boss/SimpleBossSkill.java new file mode 100644 index 000000000..64f92119e --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SimpleBossSkill.java @@ -0,0 +1,193 @@ +package org.mineacademy.fo.boss; + +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.Listener; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.mineacademy.fo.Logger; +import org.mineacademy.fo.SerializeUtil; +import org.mineacademy.fo.collection.SerializedMap; +import org.mineacademy.fo.menu.AdvancedMenu; +import org.mineacademy.fo.menu.button.Button; +import org.mineacademy.fo.menu.model.ItemCreator; +import org.mineacademy.fo.model.ConfigSerializable; +import org.mineacademy.fo.plugin.SimplePlugin; +import org.mineacademy.fo.remain.CompMaterial; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class represents a SimpleBoss' skill container.
+ * You can listen to any events here and handle them as you wish.
+ * To add a skill to a boss, use {@link SimpleBoss#addSkill(SimpleBossSkill)}.

+ * Don't forget to register the skill using register method or @AutoRegister(priority=1). + * + * @author Rubix327 + */ +@Getter +@Setter +public abstract class SimpleBossSkill implements Listener, ConfigSerializable { + @Getter + protected final static Map> classes = new HashMap<>(); + + /** + * The name of the skill that is shown in the game menu + */ + protected final String name; + /** + * The description of the skill that is shown in the game menu + */ + protected String description = ""; + + public SimpleBossSkill(String name) { + this.name = name; + } + + public SimpleBossSkill(String name, String description) { + this.name = name; + this.description = description; + } + + /** + * The id of the skill, used for saving purposes.
+ * Not related to the skill name and not shown in the game. + */ + @NotNull + public String getId(){ + return this.getClass().getSimpleName(); + } + + /** + * Register the skill and all listeners inside. + */ + public final void register(){ + Bukkit.getPluginManager().registerEvents(this, SimplePlugin.getInstance()); + classes.put(getId(), this.getClass()); + } + + /** + * Check if the given entity is a boss and has this skill with any values.
+ * You can override it to make a more specific checks. + * @param entity the entity + * @return has skill (no properties are taken into account) + */ + public boolean hasSkill(Entity entity){ + if (!(entity instanceof LivingEntity)) return false; + SpawnedBoss boss = SpawnedBoss.get((LivingEntity) entity); + return boss != null && boss.getBoss().getSkills().containsValue(this); + } + + /** + * Get the skill item which is shown in the menu.
+ * Default is Enchanted Book with the name of the skill. + * @return the item + */ + @NotNull + public ItemStack getMenuItem(){ + return ItemCreator.of(CompMaterial.ENCHANTED_BOOK).name(getName()).make(); + } + + /** + * Get the menu which should be opened when a player clicks on the skill button.
+ * Default is null. + * @param player the player who clicked the button + * @param boss the boss who has this skill + * @return the menu to be opened + */ + public AdvancedMenu getSettingsMenu(Player player, SimpleBoss boss){ + return null; + } + + /** + * Get buttons for each property (field) which value you can set in the menu. + * The key is supposed to be used as a slot for the button.
+ * Default is empty HashMap. + * @return the map of the buttons: slot:button + */ + @NotNull + public Map getSettingsButtons(){ + return new HashMap<>(); + } + + /** + * Serialize 'id' parameter to the map. + *

+ * Don't forget to add the id to the map + * OR use super.serialize() to get the map + * OR use @AutoRegister(deep=true)! + * @return the map + */ + @Override + public SerializedMap serialize() { + SerializedMap map = new SerializedMap(); + map.put("id", getId()); + return map; + } + + /** + * Deserialize the 'id' parameter and call child class' deserialization + * based on the id of the child class to deserialize other properties. + * @param map the map from the file + * @return a new instance of specific SimpleBossSkill + */ + public static SimpleBossSkill deserialize(SerializedMap map){ + String id = map.getString("id"); + if (id == null){ + Logger.printErrors( + "Parameter 'id' is missing in the Skill config section. ", + "For all of your skills, please: ", + " use 'map.put(\"id\", getId())'", + " OR use 'map = super.serialize()' to get the map", + " OR use '@AutoSerialize(deep=true)' above the class!" + ); + throw new NullPointerException("Parameter 'id' is missing in the Skill config section. " + + "Please add it to the map or use @AutoSerialize(deep=true)."); + } + + return deserialize(id, map); + } + + public static SimpleBossSkill deserialize(String id, SerializedMap map){ + Class clazz = classes.get(id); + if (clazz == null) { + Logger.printErrors( + id + ": Skill with that name does not exist, or its class is not registered yet. ", + "Skills must be registered before any class where is used (e.g. SimpleBoss).", + "If you use @AutoRegister, please set the 'priority' parameter to higher value " + + "to load the skill before the boss class." + ); + throw new NullPointerException("Skill does not exist or is not registered."); + } + + return SerializeUtil.deserialize(SerializeUtil.Mode.YAML, clazz, map); + } + + @Override + public String toString() { + return "SimpleBossSkill{" + + "name='" + name + '\'' + + ", description='" + description + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SimpleBossSkill skill = (SimpleBossSkill) o; + + return getId().equals(skill.getId()); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/src/main/java/org/mineacademy/fo/boss/SimpleDropTable.java b/src/main/java/org/mineacademy/fo/boss/SimpleDropTable.java new file mode 100644 index 000000000..89f803629 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SimpleDropTable.java @@ -0,0 +1,102 @@ +package org.mineacademy.fo.boss; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.mineacademy.fo.annotation.AutoSerialize; +import org.mineacademy.fo.model.ConfigSerializable; +import org.mineacademy.fo.model.ItemStackSerializer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a container of items that a boss can drop on death.
+ * Every item in a table has its own chance to drop and the table itself + * has the chance to be picked when a boss dies. + * + * @author Rubix327 + */ +@Getter +@AutoSerialize +public final class SimpleDropTable implements ConfigSerializable { + + private List table = new ArrayList<>(); + private double chance = 50; + + public SimpleDropTable(){ + } + + public SimpleDropTable(List table, double chance) { + this.table = table; + this.chance = chance; + } + + public static Builder builder(){ + return new SimpleDropTable().new Builder(); + } + + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public class Builder{ + public Builder add(ItemStack item){ + return add(item, 100); + } + + public Builder add(ItemStack item, float chance){ + SimpleDropTable.this.table.add(new ItemStackSerializer(item, chance)); + + return this; + } + + public Builder add(Material mat){ + return add(mat, 100); + } + + public Builder add(Material mat, float chance){ + SimpleDropTable.this.table.add(new ItemStackSerializer(new ItemStack(mat), chance)); + + return this; + } + + public Builder setChance(double chance){ + SimpleDropTable.this.chance = chance; + + return this; + } + + public SimpleDropTable build(){ + return SimpleDropTable.this; + } + } + + @Override + public String toString() { + return "DropTable{" + + "items=" + table + + ", chance=" + chance + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SimpleDropTable dropTable = (SimpleDropTable) o; + + if (Double.compare(dropTable.chance, chance) != 0) return false; + return table.equals(dropTable.table); + } + + @Override + public int hashCode() { + int result; + long temp; + result = table.hashCode(); + temp = Double.doubleToLongBits(chance); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } +} diff --git a/src/main/java/org/mineacademy/fo/boss/SimplePotionEffect.java b/src/main/java/org/mineacademy/fo/boss/SimplePotionEffect.java new file mode 100644 index 000000000..5e396ee93 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SimplePotionEffect.java @@ -0,0 +1,116 @@ +package org.mineacademy.fo.boss; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.mineacademy.fo.annotation.SerializeToString; +import org.mineacademy.fo.model.ConfigSerializable; + +/** + * Potion effect wrapper that can be easily serialized to a full-properties string. + * Contains a new 'equals' check, so now the same effects are equal to each other. + * + * @author Rubix327 + */ +@Getter +@Setter +@SerializeToString +@AllArgsConstructor +public final class SimplePotionEffect implements ConfigSerializable { + + /** + * The type of the potion. + */ + private final PotionEffectType type; + /** + * The duration (in ticks) that this effect will run for when applied to a LivingEntity. + */ + private final int duration; + /** + * The amplifier of this effect. A higher amplifier means the potion effect + * happens more often over its duration and in some cases has more effect on its target. + */ + private final int amplifier; + /** + * Makes potion effect produce more, translucent, particles. + */ + private final boolean ambient; + /** + * Whether this effect has particles or not. + */ + private final boolean particles; + /** + * Whether this effect has an icon or not. + */ + private final boolean icon; + + public SimplePotionEffect(PotionEffect effect) { + this.type = effect.getType(); + this.duration = effect.getDuration(); + this.amplifier = effect.getAmplifier(); + this.ambient = effect.isAmbient(); + this.particles = effect.hasParticles(); + this.icon = effect.hasIcon(); + } + + /** + * Make a new potion effect out of this instance. + * @return the new potion effect + */ + public PotionEffect toPotionEffect(){ + return new PotionEffect(type, duration, amplifier, ambient, particles, icon); + } + + @Override + public String serializeToString(){ + return type.getName() + " " + duration + " " + amplifier + " " + ambient + " " + particles + " " + icon; + } + + public static SimplePotionEffect deserialize(String s){ + String[] w = s.split(" "); + return new SimplePotionEffect( + PotionEffectType.getByName(w[0]), Integer.parseInt(w[1]), + Integer.parseInt(w[2]), Boolean.parseBoolean(w[3]), + Boolean.parseBoolean(w[4]), Boolean.parseBoolean(w[5])); + } + + @Override + public String toString() { + return "SimplePotionEffect{" + + "type=" + type + + ", duration=" + duration + + ", amplifier=" + amplifier + + ", ambient=" + ambient + + ", particles=" + particles + + ", icon=" + icon + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SimplePotionEffect that = (SimplePotionEffect) o; + + if (duration != that.duration) return false; + if (amplifier != that.amplifier) return false; + if (ambient != that.ambient) return false; + if (particles != that.particles) return false; + if (icon != that.icon) return false; + return type.equals(that.type); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + duration; + result = 31 * result + amplifier; + result = 31 * result + (ambient ? 1 : 0); + result = 31 * result + (particles ? 1 : 0); + result = 31 * result + (icon ? 1 : 0); + return result; + } +} diff --git a/src/main/java/org/mineacademy/fo/boss/SpawnedBoss.java b/src/main/java/org/mineacademy/fo/boss/SpawnedBoss.java new file mode 100644 index 000000000..4c3dfeed0 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SpawnedBoss.java @@ -0,0 +1,218 @@ +package org.mineacademy.fo.boss; + +import lombok.Getter; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.scheduler.BukkitTask; +import org.mineacademy.fo.Common; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * The wrapper of the boss that has been spawned to a world. + * The instance of this class stores the living entity, the SimpleBoss + * class to which he belongs and the BukkitTask that runs once at a certain time. + * + * @author Rubix327 + */ +@Getter +public final class SpawnedBoss { + + /** + * All found loaded bosses in the worlds.
+ * This map is filled automatically when a boss spawns + * or someone loads a chunk with a boss inside. + */ + private static final Map bosses = new HashMap<>(); + /** + * The period between each removal of invalid bosses. + */ + @Getter + private static int removeInvalidPeriod = 200; + + /** + * The SimpleBoss belonging to this SpawnedBoss. + */ + private final SimpleBoss boss; + /** + * The real entity of this boss.
+ */ + private final LivingEntity entity; + /** + * The task belonging to this boss. This task is ran when the boss is spawned.
+ * Also, you can manually run this task by {@link #runTaskTimers()}. + */ + private BukkitTask task; + + private SpawnedBoss(SimpleBoss boss, LivingEntity entity) { + this.boss = boss; + this.entity = entity; + bosses.put(entity, this); + } + + /** + * Remove the entity from the world. + */ + public void remove(){ + unload(entity); + entity.remove(); + } + + /** + * Run task timers to execute them in the specified repeat rate.
+ * If the task already exist, it will not be run anymore.

+ * These timers are already run automatically when the entity is spawned or loaded + * and generally should not be used externally. Use it on your own risk! + */ + public void runTaskTimers(){ + if (task == null && entity.isValid()){ + // If the task should only be run once + if (boss.getTaskPeriod() < 0){ + task = Common.runLater(boss.getTaskDelay(), this::runTaskOnce); + Common.runLater(boss.getTaskDelay() + 1, this::cancelTask); + } + // If the task is repeatable + else{ + task = Common.runTimer(boss.getTaskDelay(), boss.getTaskPeriod(), this::runTaskOnce); + } + } + } + + /** + * Run the task if the entity is still alive. Otherwise - cancel it.

+ * + * This method is already called automatically and generally should + * not be used externally. Use it on your own risk! + */ + public void runTaskOnce(){ + if (entity.isValid()){ + boss.runTask(entity); + } + else{ + cancelTask(); + } + } + + /** + * Cancel the task for this boss if it exists. + */ + public void cancelTask(){ + if (task != null){ + task.cancel(); + task = null; + } + } + + /** + * Check is the entity is loaded to the plugin as a SpawnedBoss. + * @param entity the entity + * @return true if it's loaded + */ + public static boolean isLoaded(Entity entity){ + if (!SimpleBoss.isBoss(entity)) return false; + return bosses.get((LivingEntity) entity) != null; + } + + /** + * Get the SpawnedBoss of the entity. + * @param entity the entity + * @return the SpawnedBoss or null if this entity is not a boss or not loaded ({@link #isLoaded(Entity)}) + */ + public static SpawnedBoss get(LivingEntity entity){ + return bosses.get(entity); + } + + /** + * Get all loaded SpawnBosses on the server. + * @return collection of spawned bosses. + */ + public static Collection getAll(){ + return bosses.values(); + } + + /** + * Load the entity to the plugin. + * @param entity the entity + * @return the SpawnedBoss or null if this entity is not a boss + */ + public static SpawnedBoss load(LivingEntity entity){ + if (SimpleBoss.isBoss(entity)){ + SpawnedBoss boss = new SpawnedBoss(SimpleBoss.get(entity), entity); + boss.runTaskTimers(); + removeInvalidBosses(); + return boss; + } + return null; + } + + /** + * Unload the entity from the plugin. + * From this moment it is no longer a SpawnedBoss. + * @param entity the entity + */ + public static void unload(LivingEntity entity){ + if (SimpleBoss.isBoss(entity)){ + bosses.get(entity).cancelTask(); + bosses.remove(entity); + } + } + + /** + * Set the period between each removal of invalid bosses.
+ * Decreasing the value may reduce performance.
+ * Increasing the value will mean that the bosses can stay longer + * in the container ({@link #getAll()}) and take up server memory.
+ * Default value is 200 ticks (10 seconds). + * @param period the period, in ticks + */ + @SuppressWarnings("unused") + public static void setRemoveInvalidPeriod(int period) { + removeInvalidPeriod = period; + } + + /** + * Loop all the SpawnedBosses and remove invalid ones.
+ * Bosses can become invalid when the server removes them due to the player being too far away. + */ + private static void removeInvalidBosses(){ + Common.runTimer(removeInvalidPeriod, () -> { + Collection> dead = new ArrayList<>(); + for (Map.Entry entry : bosses.entrySet()){ + if (!entry.getKey().isValid()){ + dead.add(entry); + } + } + dead.forEach(b -> { + bosses.remove(b.getKey()); + b.getValue().getBoss().onDespawn(b.getKey()); + }); + dead.clear(); + }); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SpawnedBoss that = (SpawnedBoss) o; + + return entity.getUniqueId().equals(that.entity.getUniqueId()); + } + + @Override + public int hashCode() { + return entity.hashCode(); + } + + @Override + public String toString() { + return "SpawnedBoss{" + + "entityUUID=" + entity.getUniqueId() + + ", boss=" + boss + + '}'; + } +} diff --git a/src/main/java/org/mineacademy/fo/boss/SpawnerData.java b/src/main/java/org/mineacademy/fo/boss/SpawnerData.java new file mode 100644 index 000000000..f580e7d8d --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SpawnerData.java @@ -0,0 +1,91 @@ +package org.mineacademy.fo.boss; + +import lombok.*; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.NotNull; +import org.mineacademy.fo.annotation.AutoSerialize; +import org.mineacademy.fo.model.ConfigSerializable; + +/** + * This class encapsulates data for spawners. + * To create SpawnerData, use the all-arguments constructor or + * the convenient builder - {@link SpawnerData#of(EntityType)}. + * + * @author Rubix327 + */ +@Getter +@Setter +@AutoSerialize +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class SpawnerData implements ConfigSerializable { + /** + * The type of the entity which is spinning in the spawner.
+ * Default: EntityType.PIG + */ + private @NotNull EntityType type = EntityType.PIG; + /** + * The item name which is given to the player.
+ * Used in {@link SimpleBoss#getSpawner()}. + */ + private String itemName; + /** + * The starting delay of the spawner which is decreasing every tick.
+ * When the value reaches -1, the spawn delay is reset to a random value between + * {@link #getMinSpawnDelay} and {@link #getMaxSpawnDelay()}.
+ * See {@link CreatureSpawner#getDelay()} + */ + private int startDelay; + /** + * The minimum time (in ticks) that must elapse after mob spawn to spawn a new mob.
+ * See {@link CreatureSpawner#getMinSpawnDelay()} + */ + private int minSpawnDelay; + /** + * The minimum time (in ticks) that must elapse after mob spawn to spawn a new mob.
+ * See {@link CreatureSpawner#getMaxSpawnDelay()} + */ + private int maxSpawnDelay; + /** + * See {@link CreatureSpawner#getSpawnCount()} + */ + private int spawnCount; + /** + * See {@link CreatureSpawner#getMaxNearbyEntities()} + */ + private int maxNearbyEntities; + /** + * See {@link CreatureSpawner#getRequiredPlayerRange()} + */ + private int requiredPlayerRange; + /** + * See {@link CreatureSpawner#getSpawnRange()} + */ + private int spawnRange; + + /** + * Create a new builder for the SpawnedData and set the given type to the spawner. + * @param type the spinning type + * @return builder SpawnerDataBuilder + */ + public static SpawnerDataBuilder of(EntityType type){ + return SpawnerDataBuilder.of(type); + } + + @Override + public String toString() { + return "SpawnerData{" + + "type=" + type + + ", itemName='" + itemName + '\'' + + ", startDelay=" + startDelay + + ", minSpawnDelay=" + minSpawnDelay + + ", maxSpawnDelay=" + maxSpawnDelay + + ", spawnCount=" + spawnCount + + ", maxNearbyEntities=" + maxNearbyEntities + + ", requiredPlayerRange=" + requiredPlayerRange + + ", spawnRange=" + spawnRange + + '}'; + } +} + diff --git a/src/main/java/org/mineacademy/fo/boss/SpawnerDataBuilder.java b/src/main/java/org/mineacademy/fo/boss/SpawnerDataBuilder.java new file mode 100644 index 000000000..c22852ab7 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/boss/SpawnerDataBuilder.java @@ -0,0 +1,114 @@ +package org.mineacademy.fo.boss; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * The convenient builder for {@link SpawnerData}. + * + * @author Rubix327 + */ +@SuppressWarnings("unused") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class SpawnerDataBuilder { + private @Nullable SimpleBoss boss; + private final @NotNull EntityType type; + private String itemName = "%name% Spawner"; + private int delay = 10; + private int minSpawnDelay = 200; + private int maxSpawnDelay = 800; + private int spawnCount = 4; + private int maxNearbyEntities = 16; + private int requiredPlayerRange = 16; + private int spawnRange = 4; + + /** + * Create new SpawnerDataBuilder with the specified EntityType. + * @param type the type + * @return the new SpawnerDataBuilder + */ + public static SpawnerDataBuilder of(EntityType type){ + return new SpawnerDataBuilder(type); + } + + /** + * Set the SimpleBoss related to this SpawnerData. + * Used for saving boss' data automatically on any change. + */ + public SpawnerDataBuilder setBoss(SimpleBoss boss){ + this.boss = boss; + return this; + } + + /** + * Set the name of the item in {@link SimpleBoss#getSpawner()} + */ + public SpawnerDataBuilder setItemName(String name){ + this.itemName = name; + return this; + } + + /** + * See {@link CreatureSpawner#setDelay(int)} + */ + public SpawnerDataBuilder setDelay(int delay) { + this.delay = delay; + return this; + } + + /** + * See {@link CreatureSpawner#setMinSpawnDelay(int)} + */ + public SpawnerDataBuilder setMinSpawnDelay(int minSpawnDelay) { + this.minSpawnDelay = minSpawnDelay; + return this; + } + + /** + * See {@link CreatureSpawner#setMaxSpawnDelay(int)} + */ + public SpawnerDataBuilder setMaxSpawnDelay(int maxSpawnDelay) { + this.maxSpawnDelay = maxSpawnDelay; + return this; + } + + /** + * See {@link CreatureSpawner#setSpawnCount(int)} + */ + public SpawnerDataBuilder setSpawnCount(int spawnCount) { + this.spawnCount = spawnCount; + return this; + } + + /** + * See {@link CreatureSpawner#setMaxNearbyEntities(int)} + */ + public SpawnerDataBuilder setMaxNearbyEntities(int maxNearbyEntities) { + this.maxNearbyEntities = maxNearbyEntities; + return this; + } + + /** + * See {@link CreatureSpawner#setRequiredPlayerRange(int)} + */ + public SpawnerDataBuilder setRequiredPlayerRange(int requiredPlayerRange) { + this.requiredPlayerRange = requiredPlayerRange; + return this; + } + + /** + * See {@link CreatureSpawner#setSpawnRange(int)} + */ + public SpawnerDataBuilder setSpawnRange(int spawnRange) { + this.spawnRange = spawnRange; + return this; + } + + public SpawnerData build() { + return new SpawnerData(type, itemName, delay, minSpawnDelay, maxSpawnDelay, spawnCount, maxNearbyEntities, requiredPlayerRange, spawnRange); + } +} \ No newline at end of file diff --git a/src/main/java/org/mineacademy/fo/model/ItemStackSerializer.java b/src/main/java/org/mineacademy/fo/model/ItemStackSerializer.java new file mode 100644 index 000000000..50b42ce78 --- /dev/null +++ b/src/main/java/org/mineacademy/fo/model/ItemStackSerializer.java @@ -0,0 +1,210 @@ +package org.mineacademy.fo.model; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.bukkit.Material; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeModifier; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.Contract; +import org.mineacademy.fo.annotation.AutoSerialize; +import org.mineacademy.fo.boss.SimpleBossEquipment; +import org.mineacademy.fo.boss.SimpleDropTable; +import org.mineacademy.fo.constants.FoConstants; +import org.mineacademy.fo.remain.CompMaterial; +import org.mineacademy.fo.remain.CompMetadata; +import org.mineacademy.fo.remain.nbt.NBTCompound; +import org.mineacademy.fo.remain.nbt.NBTItem; + +import java.util.*; + +/** + * This class represents an ItemStack wrapper for beautiful serialization to a file. + * @author Rubix327 + */ +@Getter +@AutoSerialize +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ItemStackSerializer implements ConfigSerializable{ + + private Material type = Material.AIR; + private int amount = 0; + private String displayName; + private String localizedName; + private List lore; + private Integer customModelData; + private Boolean unbreakable; + private Map enchants; + private Set flags; + private List attributes; + /** + * This only stores metadata from Foundation. + */ + private Map metadata; + /** + * Additional parameter - chance of dropping the item for events like EntityDeathEvent.
+ * Left on null if you don't want to use it.
+ * Used in Foundation in {@link SimpleBossEquipment} and {@link SimpleDropTable}.

+ * This parameter is not saved on the item after converting it into an ItemStack! + */ + private Float dropChance; + + public ItemStackSerializer(ItemStack item){ + this(item, null); + } + + public ItemStackSerializer(ItemStack item, Float dropChance){ + this.dropChance = dropChance; + + // Basic parameters + this.type = item.getType(); + this.amount = item.getAmount(); + + // Item Meta + ItemMeta meta = item.getItemMeta(); + if (item.hasItemMeta() && meta != null){ + if (meta.hasDisplayName()){ + this.displayName = meta.getDisplayName(); + } + if (meta.hasLocalizedName()){ + this.localizedName = meta.getLocalizedName(); + } + if (meta.hasLore()){ + this.lore = meta.getLore(); + } + if (meta.hasCustomModelData()){ + this.customModelData = meta.getCustomModelData(); + } + if (meta.getEnchants().size() > 0){ + this.enchants = meta.getEnchants(); + } + if (meta.getItemFlags().size() > 0){ + this.flags = meta.getItemFlags(); + } + this.unbreakable = meta.isUnbreakable() ? true : null; + if (meta.getAttributeModifiers() != null && meta.getAttributeModifiers().size() > 0){ + if (attributes == null){ + attributes = new ArrayList<>(); + } + for (Map.Entry attr : meta.getAttributeModifiers().entries()){ + attributes.add(serializeAttributeModifier(attr.getKey(), attr.getValue())); + } + } + } + + // Metadata by Foundation + if (!CompMaterial.isAir(item.getType())){ + final String compoundTag = FoConstants.NBT.TAG; + final NBTItem nbt = new NBTItem(item); + NBTCompound compound = nbt.getCompound(compoundTag); + if (compound != null){ + Set keys = compound.getKeys(); + if (keys.size() != 0 && this.metadata == null){ + this.metadata = new HashMap<>(); + } + for (String key : keys){ + this.metadata.put(key, compound.getString(key)); + } + } + } + } + + public ItemStack toItemStack(){ + // Basic parameters + ItemStack item = new ItemStack(this.type, this.amount); + + // Item meta + ItemMeta meta = item.getItemMeta(); + if (meta != null){ + meta.setDisplayName(this.displayName); + meta.setLocalizedName(this.localizedName); + meta.setLore(this.lore); + meta.setCustomModelData(this.customModelData); + meta.setUnbreakable(this.unbreakable != null && this.unbreakable); + // Enchantments + if (this.enchants != null){ + for (Map.Entry ench : this.enchants.entrySet()){ + meta.addEnchant(ench.getKey(), ench.getValue(), true); + } + } + // ItemFlags + if (this.flags != null){ + for (ItemFlag flag : this.flags){ + meta.addItemFlags(flag); + } + } + // Attributes + if (this.attributes != null){ + for (String line : attributes){ + meta.addAttributeModifier(deserializeAttribute(line), deserializeModifier(line)); + } + } + // Metadata by Foundation + if (this.metadata != null){ + for (Map.Entry data : this.metadata.entrySet()){ + CompMetadata.setMetadata(item, data.getKey(), data.getValue()); + } + } + } + item.setItemMeta(meta); + return item; + } + + public static String serializeAttributeModifier(Attribute attribute, AttributeModifier mod){ + return attribute + " " + mod.getAmount() + " " + + mod.getOperation() + (mod.getSlot() == null ? "" : mod.getSlot().toString()); + } + + public static Attribute deserializeAttribute(String line){ + int index = line.indexOf(" "); + return Attribute.valueOf(line.substring(0, index)); + } + + public static AttributeModifier deserializeModifier(String line){ + String[] arr = line.split(" "); + UUID uuid = UUID.randomUUID(); + double amount = Double.parseDouble(arr[1]); + AttributeModifier.Operation op = AttributeModifier.Operation.valueOf(arr[2]); + EquipmentSlot slot = arr.length == 4 ? EquipmentSlot.valueOf(arr[3]) : null; + if (slot == null){ + return new AttributeModifier(uuid, "", amount, op); + } else { + return new AttributeModifier(uuid, "", amount, op, slot); + } + } + + @Contract(pure = true) + public static ItemStackSerializer empty(){ + return new ItemStackSerializer(new ItemStack(Material.AIR), null); + } + + @Override + public boolean loadEmptyCollections() { + return false; + } + + @Override + public String toString() { + return "ItemStackSerializer{" + + "type=" + type + + ", amount=" + amount + + ", displayName='" + displayName + '\'' + + ", localizedName='" + localizedName + '\'' + + ", lore=" + lore + + ", customModelData=" + customModelData + + ", enchants=" + enchants + + ", flags=" + flags + + ", unbreakable=" + unbreakable + + ", attributes=" + attributes + + ", metadata=" + metadata + + ", dropChance=" + dropChance + + '}'; + } +} diff --git a/src/main/java/org/mineacademy/fo/remain/CompColor.java b/src/main/java/org/mineacademy/fo/remain/CompColor.java index e61a977d8..2f7d41385 100644 --- a/src/main/java/org/mineacademy/fo/remain/CompColor.java +++ b/src/main/java/org/mineacademy/fo/remain/CompColor.java @@ -249,8 +249,7 @@ public static CompColor fromName(String name) { name = name.toUpperCase(); for (final CompColor comp : values()) - if (comp.getName().equals(name) || comp.chatColor.toString().equals(name) || - comp.dye.toString().equals(name) || comp.legacyName.equals(name)) + if (comp.getName().equals(name) || comp.chatColor.toString().equals(name) || comp.legacyName.equals(name)) return comp; throw new IllegalArgumentException("Could not get CompColor from name: " + name); diff --git a/src/main/java/org/mineacademy/fo/remain/Remain.java b/src/main/java/org/mineacademy/fo/remain/Remain.java index 0b6e5d607..670a72d14 100644 --- a/src/main/java/org/mineacademy/fo/remain/Remain.java +++ b/src/main/java/org/mineacademy/fo/remain/Remain.java @@ -30,8 +30,7 @@ import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionType; -import org.bukkit.scoreboard.Objective; -import org.bukkit.scoreboard.Score; +import org.bukkit.scoreboard.*; import org.mineacademy.fo.*; import org.mineacademy.fo.MinecraftVersion.V; import org.mineacademy.fo.ReflectionUtil.ReflectionException; @@ -2007,6 +2006,80 @@ public static void removeCustomName(final Entity entity) { } } + /** + * Set the entity as a baby (+ enable the age lock) or as an adult. + * @param entity the entity + * @param isBaby true - baby, false - adult + * @return false if the operation cannot be done (the entity is not Ageable) + * @author Rubix327 + */ + public static boolean setBabyOrAdult(final Entity entity, final boolean isBaby){ + // Do nothing if the entity is not Ageable + if (!(entity instanceof Ageable)) return false; + + // If the entity should be a baby, make it a baby and enable the age lock + if (isBaby){ + ((Ageable) entity).setBaby(); + if (entity instanceof Breedable){ + try{ + ((Breedable) entity).setAgeLock(true); + } catch (NoSuchMethodError ignored){ + ((Ageable) entity).setAgeLock(true); + } + } + return true; + // If the entity should be an adult, set it clearly + } else { + ((Ageable) entity).setAdult(); + return true; + } + } + + /** + * Add a passenger to the vehicle or a living entity. + * @param carrier the vehicle + * @param passenger the passenger + * @return false if it could not be done for whatever reason + * @author Rubix327 + */ + public static boolean addPassenger(Entity carrier, Entity passenger){ + if (hasAddPassenger()){ + return carrier.addPassenger(passenger); + } else { + return carrier.setPassenger(passenger); + } + } + + /** + * Add the glowing effect to the entity with the specified color. + * @param entity the entity + * @param glowColor the color of glow + * @return false - if the operation could not be done + * (either no worlds are loaded or this operation is not supported on your server version) + * @author Rubix327 + * @since MC 1.12 + */ + public static boolean setGlowing(Entity entity, CompColor glowColor){ + try{ + entity.setGlowing(true); + if (glowColor != CompColor.WHITE){ + String name = glowColor.getName().toLowerCase() + "_tm"; + + ScoreboardManager manager = Bukkit.getScoreboardManager(); + if (manager == null) return false; + + Scoreboard board = manager.getMainScoreboard(); + Team team = board.getTeam(name); + team = team != null ? team : board.registerNewTeam(name); + team.setColor(glowColor.getChatColor()); + team.addEntry(entity.getUniqueId().toString()); + } + return true; + } catch (NoSuchMethodError ignored){ + return false; + } + } + /** * Calls NMS to find out if the entity is invisible, works for any entity, * better than Bukkit since it has extreme downwards compatibility and does not require LivingEntity