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:
+ *
+ * - Set the name, health, damage, drop tables, equipment, etc. in {@link #setup()} method
+ * - Set the rest of the characteristics in {@link #onSpawn(LivingEntity)}
+ * - Spawn entity using {@link #spawn(Location)}
+ * - Easily add extra behavior on spawn, attack, get damage, death, etc.
+ * - Add timed tasks by overriding {@link #runTask(LivingEntity)}, {@link #getTaskPeriod()}, {@link #getTaskDelay()}
+ * - Save and load to the file using YamlConfig
+ *
+ *
+ * @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 extends LivingEntity> 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 extends LivingEntity>) 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 extends SimpleBossSkill> 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