diff --git a/src/main/java/cn/variZoo/Command.java b/src/main/java/cn/variZoo/Command.java new file mode 100644 index 0000000..fe22477 --- /dev/null +++ b/src/main/java/cn/variZoo/Command.java @@ -0,0 +1,68 @@ +package cn.variZoo; + +import cn.variZoo.utils.Message; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +public class Command implements CommandExecutor, TabCompleter { + private final List subcommands; + private final Main plugin; + + public Command(Main main) { + this.plugin = main; + subcommands = new ArrayList<>(List.of("reload", "help")); + } + + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull org.bukkit.command.Command command, @NotNull String label, @NotNull String[] args) { + + if (args.length == 0 || !subcommands.contains(args[0].toLowerCase())) { + Message.showHelp(sender); + return true; + } + + if (!sender.hasPermission("varizoo." + args[0])) { + Message.sendMsg(sender, "你没有权限执行此命令!"); + return true; + } + + switch (args[0].toLowerCase()) { + case "reload": + long startTime = System.currentTimeMillis(); + Message.sendMsg(sender, "插件重启中..."); + plugin.reload(); + long elapsedTime = System.currentTimeMillis() - startTime; + Message.sendMsg(sender, "VariZoo重启完成,耗时 " + elapsedTime + " ms"); + break; + case "help": + Message.showHelp(sender); + break; + } + return true; + } + + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull org.bukkit.command.Command command, @NotNull String alias, @NotNull String[] args) { + List ret = new ArrayList<>(); + if (args.length == 1) { + for (String subcmd : subcommands) { + if (!sender.hasPermission("varizoo." + subcmd)) continue; + ret.add(subcmd); + } + return StringUtil.copyPartialMatches(args[0].toLowerCase(), ret, new ArrayList<>()); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/cn/variZoo/Configuration.java b/src/main/java/cn/variZoo/Configuration.java new file mode 100644 index 0000000..90bc977 --- /dev/null +++ b/src/main/java/cn/variZoo/Configuration.java @@ -0,0 +1,146 @@ +package cn.variZoo; + +import cn.variZoo.utils.configuration.Comment; +import cn.variZoo.utils.configuration.ConfigurationFile; +import cn.variZoo.utils.configuration.ConfigurationPart; + +import java.util.ArrayList; +import java.util.List; + +public class Configuration extends ConfigurationFile { + + @Comment("版本号") + public static int version = 3; + + @Comment("总开关") + public static boolean enabled = true; + + @Comment("动物生成相关") + public static AnimalSpawn animalSpawn = new AnimalSpawn(); + @Comment("生育相关") + public static Breed breed = new Breed(); + @Comment("其他相关") + public static Other other = new Other(); + + public static class AnimalSpawn extends ConfigurationPart { + + @Comment("基础配置") + public static Basic basic = new Basic(); + @Comment("在基础上二次突变") + public static Mutant mutant = new Mutant(); + @Comment("黑名单") + public static BlackList blackList = new BlackList(); + + public static class Basic extends ConfigurationPart { + + @Comment({"动物生成时附带体型大小的概率", + "设置为0或者负数禁用, 最大为100"}) + public double apply = 50.0; + + @Comment({"体型变化值", + "degree可填写范围或者多个数字"}) + public String degree = "0.86-1.16"; + + } + + public static class Mutant extends ConfigurationPart { + + @Comment({"变异的概率", "设置为0或者负数禁用, 最大为100"}) + public double apply = 3.0; + @Comment({"MULTIPLY: 简单相乘", "MORE: 自适应, 大的更大, 小的更小"}) + public String mode = "MORE"; + @Comment({"变化值", + "degree可填写范围或者多个数字"}) + public String degree = "0.77, 1.3"; + @Comment("触发变异时产生的粒子特效") + public static Particle particle = new Particle(); + + public static class Particle extends ConfigurationPart { + + @Comment("类型, 留空禁用") + public String type = "GLOW"; + + @Comment("数量") + public int count = 20; + } + + } + + public static class BlackList extends ConfigurationPart { + + @Comment({"不受影响的动物", + "https://hub.spigotmc.org/javadocs/spigot/org/bukkit/entity/EntityType.html"}) + public List animal = new ArrayList<>(); + + @Comment("不受影响的世界") + public List world = new ArrayList<>(); + + @Comment({"不受影响的生成原因", + "https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/entity/CreatureSpawnEvent.SpawnReason.html"}) + public List spawnReason = List.of( "SPAWNER"); + + } + + } + + public static class Breed extends ConfigurationPart { + + @Comment("动物生育宝宝是否遗传体型") + public static Inheritance inheritance = new Inheritance(); + @Comment("多胞胎功能") + public static Multiple multiple = new Multiple(); + @Comment({"黑名单", "给予玩家varizoo.skip.breed权限可以单独关闭遗传功能, 防止一些生物牧场被破坏"}) + public static BlackList blackList = new BlackList(); + + public static class Inheritance extends ConfigurationPart { + + @Comment({"父母对孩子体型的影响", "可使用复杂的公式, 留空时禁用", "可用变量:{father}、{mother}父母的体型, {degree}对应下方比例"}) + public String finalScale = "({father} * 1.1 + {mother} * 1.2) / {degree}"; + + @Comment("比例, 配合上方的final-scale使用") + public String degree = "2.0-2.6"; + + @Comment("开启该选项会让宝宝不受animal-spawn的二次体型变化影响") + public boolean skipAnimalSpawn = false; + + @Comment({"当饲养出宝宝时的提示","仅支持minimessage颜色格式, 留空时禁用", "可用变量: {scale}体型, {baby}宝宝名称, {player}玩家名称"}) + public String actionbar = "新生命诞生啦! 是体型为 {scale} 的{baby}宝宝~"; + } + + public static class Multiple extends ConfigurationPart { + + @Comment({"多胞胎的概率", "设置为0或负数时禁止, 最大为100", "注意不可过高, 每次生育都会触发多胞胎判定, 过高会一直生孩子导致卡服"}) + public double apply = 9.0; + + @Comment({"每次生孩子的间隔", "单位为tick, 20ticks = 1s"}) + public int delay = 3; + + @Comment({"启动多胞胎时, 为了限制生育, 每次生育都会扣除以下血量", "可使用复杂的公式, 留空时禁用", "可用变量:{health}当前血量, {max_health}最大血量"}) + public String hurt = "{health} * 0.05"; + } + + public static class BlackList extends ConfigurationPart { + + @Comment("不受影响的动物") + public List animal = List.of("BEE"); + + @Comment("不受影响的世界") + public List world = new ArrayList<>(); + } + } + + public static class Other extends ConfigurationPart { + + @Comment("体型对最大生命的影响, 会等比例变化生命值") + public boolean effectHealth = true; + + @Comment("动物转变时是否保留体型, 比如猪被雷劈变成猪灵, 依旧会继承体型") + public boolean transform = true; + + @Comment("蝾螈、鱼等动物装进桶后放出会失去原有的体型, 该功能可以保留体型, 会修正鱼桶和发射器") + public boolean bucketFishFix = true; + + @Comment({"使动物的掉落物数量乘以以下值", "可使用复杂的公式, 留空时禁用", "可用变量:{scale}生物体型"}) + public String increaseDrops = "sqrt({scale})"; + } +} diff --git a/src/main/java/cn/variZoo/Main.java b/src/main/java/cn/variZoo/Main.java new file mode 100644 index 0000000..ed83134 --- /dev/null +++ b/src/main/java/cn/variZoo/Main.java @@ -0,0 +1,82 @@ +package cn.variZoo; + +import cn.variZoo.managers.ConfigManager; +import cn.variZoo.managers.ListenerManager; +import cn.variZoo.utils.*; +import org.bukkit.attribute.Attribute; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; + +public final class Main extends JavaPlugin { + public ConfigManager configManager; + public ListenerManager listenerManager; + private boolean folia; + + @Override + public void onEnable() { + long startTime = System.currentTimeMillis(); + + new XLogger(this); + + boolean enabled = false; + for (Attribute attribute : Attribute.values()) { + if (attribute.name().toLowerCase().contains("scale")) { + new EntityUtil(this,attribute); + enabled = true; + } + } + + if (!enabled) { + XLogger.err("This server is not supported. The plugin will be disabled!"); + this.getServer().getPluginManager().disablePlugin(this); + return; + } + + XLogger.info(this.getServer().getName() + this.getServer().getVersion()); + XLogger.info("██╗ ██╗ █████╗ ██████╗ ██╗███████╗ ██████╗ ██████╗ "); + XLogger.info("██║ ██║██╔══██╗██╔══██╗██║╚══███╔╝██╔═══██╗██╔═══██╗"); + XLogger.info("██║ ██║███████║██████╔╝██║ ███╔╝ ██║ ██║██║ ██║"); + XLogger.info("╚██╗ ██╔╝██╔══██║██╔══██╗██║ ███╔╝ ██║ ██║██║ ██║"); + XLogger.info(" ╚████╔╝ ██║ ██║██║ ██║██║███████╗╚██████╔╝╚██████╔╝"); + XLogger.info(" ╚═══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ ╚═════╝ ╚═════╝ "); + XLogger.info("https://github.com/Noogear/VariZoo"); + XLogger.info("Version: " + this.getDescription().getVersion()); + + try { + Class.forName("io.papermc.paper.threadedregions.RegionizedServer"); + folia = true; + } catch (ClassNotFoundException e) { + folia = false; + } + new Message(); + configManager = new ConfigManager(this); + listenerManager = new ListenerManager(this); + + PluginCommand mainCommand = getCommand("varizoo"); + if (mainCommand != null) { + mainCommand.setExecutor(new Command(this)); + } else { + XLogger.err("Failed to load command."); + } + + new Scheduler(this); + + long elapsedTime = System.currentTimeMillis() - startTime; + XLogger.info("Plugin loaded successfully in " + elapsedTime + " ms"); + } + + @Override + public void onDisable() { + Scheduler.cancelAll(); + } + + public boolean isFolia() { + return folia; + } + + public void reload() { + Scheduler.cancelAll(); + configManager.load(); + listenerManager.reload(); + } +} diff --git a/src/main/java/cn/variZoo/listeners/AnimalBreed.java b/src/main/java/cn/variZoo/listeners/AnimalBreed.java new file mode 100644 index 0000000..fec3f2e --- /dev/null +++ b/src/main/java/cn/variZoo/listeners/AnimalBreed.java @@ -0,0 +1,149 @@ +package cn.variZoo.listeners; + +import cn.variZoo.Configuration; +import cn.variZoo.utils.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Animals; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityBreedEvent; +import redempt.crunch.CompiledExpression; +import redempt.crunch.Crunch; +import redempt.crunch.functional.EvaluationEnvironment; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +public class AnimalBreed implements Listener { + + private Degree breedInheritanceDegree; + private String breedActionbar; + private boolean breedActionbarEnabled; + private boolean multipleHurtEnabled; + private Set blacklistEntity; + private Set blacklistWorld; + private CompiledExpression breedFinalScaleExpression; + private CompiledExpression breedHurtExpression; + + public AnimalBreed() { + + + try { + breedInheritanceDegree = DataUtil.saveDegree(Configuration.Breed.inheritance.degree); + breedActionbar = Configuration.Breed.inheritance.actionbar + .replace("{", "<") + .replace("}", ">"); + breedActionbarEnabled = !breedActionbar.isEmpty(); + multipleHurtEnabled = !Configuration.Breed.multiple.hurt.isEmpty(); + blacklistEntity = EntityUtil.entityToSet(Configuration.Breed.blackList.animal); + blacklistWorld = new HashSet<>(Configuration.Breed.blackList.world); + } catch (Exception e) { + XLogger.err(e.getMessage()); + } + + try { + String expression = Configuration.Breed.inheritance.finalScale.replace(" ", "").replaceAll("\\{([^}]*)}", "$1"); + EvaluationEnvironment env = new EvaluationEnvironment(); + env.setVariableNames("father", "mother", "degree"); + breedFinalScaleExpression = Crunch.compileExpression(expression, env); + breedFinalScaleExpression.evaluate(1, 1, 1); + } catch (Exception e) { + XLogger.err(e.getMessage()); + } + + try { + String expression = Configuration.Breed.multiple.hurt.replace(" ", "").replaceAll("\\{([^}]*)}", "$1"); + EvaluationEnvironment env = new EvaluationEnvironment(); + env.setVariableNames("max_health", "health"); + breedHurtExpression = Crunch.compileExpression(expression, env); + breedHurtExpression.evaluate(1, 1); + } catch (Exception e) { + XLogger.err(e.getMessage()); + } + } + + @EventHandler(ignoreCancelled = true) + public void onBreed(EntityBreedEvent event) { + if (!(event.getEntity() instanceof Animals entity)) return; + + if (isInvalidBreed(entity, event.getBreeder())) return; + + Animals mother = (Animals) event.getMother(); + Animals father = (Animals) event.getFather(); + + double degree = breedInheritanceDegree.getRandom(); + + double birthScale = breedFinalScaleExpression.evaluate( + father.getAttribute(EntityUtil.getScaleAttribute()).getValue(), + mother.getAttribute(EntityUtil.getScaleAttribute()).getValue(), + degree + ); + + AttributeInstance babyScale = entity.getAttribute(EntityUtil.getScaleAttribute()); + + if (babyScale != null) { + babyScale.setBaseValue(birthScale * babyScale.getValue()); + } + + if (Configuration.Breed.inheritance.skipAnimalSpawn) { + EntityUtil.setInvalid(entity); + } + + if (Configuration.other.effectHealth) { + AttributeInstance babyHealth = entity.getAttribute(Attribute.GENERIC_MAX_HEALTH); + if (babyHealth == null) return; + double finalHealth = birthScale * babyHealth.getValue(); + babyHealth.setBaseValue(finalHealth); + entity.setHealth(finalHealth); + } + + double multiple = Configuration.Breed.multiple.apply; + if (multiple > 0) { + Scheduler.runTaskLater(() -> { + if (ThreadLocalRandom.current().nextInt(100) > multiple) return; + mother.setLoveModeTicks(100); + father.setLoveModeTicks(100); + }, Configuration.Breed.multiple.delay); + } + + if (multipleHurtEnabled) { + father.damage(breedHurtExpression.evaluate(father.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue(), father.getHealth())); + mother.damage(breedHurtExpression.evaluate(mother.getAttribute(Attribute.GENERIC_MAX_HEALTH).getValue(), mother.getHealth())); + } + + if (breedActionbarEnabled) { + if (event.getBreeder() instanceof Player p) { + Scheduler.runTaskLater(() -> { + if (babyScale != null) { + Component actionbar = MiniMessage.miniMessage().deserialize(breedActionbar, + Placeholder.parsed("scale", String.format("%.2f", babyScale.getValue())), + Placeholder.parsed("baby", EntityUtil.getI18nName(entity)), + Placeholder.parsed("player", p.getName()) + ); + p.sendActionBar(actionbar); + } + }, 1); + } + } + } + + + private boolean isInvalidBreed(Animals e, LivingEntity p) { + if (blacklistWorld.contains(e.getWorld().getName())) return true; + if (blacklistEntity.contains(e.getType())) return true; + if (p instanceof Player player) { + if (player.hasPermission("varizoo.skip.breed")) { + return !player.isOp(); + } + } + return false; + } +} diff --git a/src/main/java/cn/variZoo/listeners/AnimalSpawn.java b/src/main/java/cn/variZoo/listeners/AnimalSpawn.java new file mode 100644 index 0000000..1e4e269 --- /dev/null +++ b/src/main/java/cn/variZoo/listeners/AnimalSpawn.java @@ -0,0 +1,97 @@ +package cn.variZoo.listeners; + +import cn.variZoo.Configuration; +import cn.variZoo.utils.*; +import org.bukkit.Particle; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Animals; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.CreatureSpawnEvent; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +public class AnimalSpawn implements Listener { + private final Set blackListEntity; + private final Set blackListSpawnReason; + private Degree animalBasicDegree; + private Degree animalMutantDegree; + private boolean mutantParticleEnabled; + private Particle mutantparticle; + private Set blackListWorld; + + public AnimalSpawn() { + + try { + animalBasicDegree = DataUtil.saveDegree(Configuration.AnimalSpawn.basic.degree); + animalMutantDegree = DataUtil.saveDegree(Configuration.AnimalSpawn.mutant.degree); + mutantparticle = Particle.valueOf(Configuration.AnimalSpawn.Mutant.particle.type.toUpperCase(Locale.ROOT)); + mutantParticleEnabled = !(Configuration.AnimalSpawn.Mutant.particle.type.isEmpty() && Configuration.AnimalSpawn.Mutant.particle.count < 1); + blackListWorld = new HashSet<>(Configuration.AnimalSpawn.blackList.world); + } catch (Exception e) { + XLogger.err(e.getMessage()); + } + + blackListEntity = EntityUtil.entityToSet(Configuration.AnimalSpawn.blackList.animal); + blackListSpawnReason = EntityUtil.spawnReasonToSet(Configuration.AnimalSpawn.blackList.spawnReason); + + + } + + @EventHandler(ignoreCancelled = true) + public void onSpawn(CreatureSpawnEvent event) { + if (!(event.getEntity() instanceof Animals entity)) return; + + if (isInvalidSpawn(entity, event.getSpawnReason())) return; + + if (EntityUtil.isInvalid(entity)) return; + + if (ThreadLocalRandom.current().nextInt(100) > Configuration.AnimalSpawn.basic.apply) return; + + AttributeInstance scale = entity.getAttribute(EntityUtil.getScaleAttribute()); + if (scale == null) return; + + double randomScale = animalBasicDegree.getRandom(); + + if (ThreadLocalRandom.current().nextInt(100) < Configuration.AnimalSpawn.mutant.apply) { + double randomMutant = animalMutantDegree.getRandom(); + if (Objects.equals(Configuration.AnimalSpawn.mutant.mode, "MORE")) { + if ((randomScale >= 1 && randomMutant < 1) || (randomScale < 1 && randomMutant >= 1)) { + randomMutant = 1 / randomMutant; + } + } + randomScale = randomScale * randomMutant; + if (mutantParticleEnabled) { + Scheduler.runTaskLater(() -> { + entity.getWorld().spawnParticle(mutantparticle, entity.getLocation(), Configuration.AnimalSpawn.Mutant.particle.count); + }, 1); + } + } + + double finalScale = Math.max(.00625, Math.min(16, randomScale * scale.getValue())); + scale.setBaseValue(finalScale); + + if (Configuration.other.effectHealth) { + AttributeInstance maxHealth = entity.getAttribute(Attribute.GENERIC_MAX_HEALTH); + if (maxHealth == null) return; + maxHealth.setBaseValue(Math.max(1, randomScale * maxHealth.getValue())); + entity.setHealth(maxHealth.getValue()); + } + + } + + private boolean isInvalidSpawn(Entity e, CreatureSpawnEvent.SpawnReason reason) { + if (blackListWorld.contains(e.getWorld().getName())) return true; + if (blackListEntity.contains(e.getType())) return true; + return blackListSpawnReason.contains(reason); + } + + +} diff --git a/src/main/java/cn/variZoo/listeners/AnimalTransform.java b/src/main/java/cn/variZoo/listeners/AnimalTransform.java new file mode 100644 index 0000000..4b17a2c --- /dev/null +++ b/src/main/java/cn/variZoo/listeners/AnimalTransform.java @@ -0,0 +1,37 @@ +package cn.variZoo.listeners; + +import cn.variZoo.Configuration; +import cn.variZoo.utils.EntityUtil; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.entity.Animals; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityTransformEvent; + +public class AnimalTransform implements Listener { + + @EventHandler(ignoreCancelled = true) + public void onTransform(EntityTransformEvent event) { + if (!(event.getEntity() instanceof Animals from)) return; + + AttributeInstance fromScale = from.getAttribute(EntityUtil.getScaleAttribute()); + if (fromScale == null) return; + + if (!(event.getTransformedEntity() instanceof LivingEntity to)) return; + AttributeInstance toScale = to.getAttribute(EntityUtil.getScaleAttribute()); + if (toScale == null || toScale.getValue() != 1.0) return; + + toScale.setBaseValue(fromScale.getValue()); + + if (Configuration.other.effectHealth) { + AttributeInstance maxHealth = to.getAttribute(Attribute.GENERIC_MAX_HEALTH); + if (maxHealth == null) return; + maxHealth.setBaseValue(Math.max(1, toScale.getValue() * maxHealth.getValue())); + to.setHealth(maxHealth.getValue()); + } + + } + +} diff --git a/src/main/java/cn/variZoo/listeners/BucketFishFix.java b/src/main/java/cn/variZoo/listeners/BucketFishFix.java new file mode 100644 index 0000000..f407adf --- /dev/null +++ b/src/main/java/cn/variZoo/listeners/BucketFishFix.java @@ -0,0 +1,131 @@ +package cn.variZoo.listeners; + +import cn.variZoo.Configuration; +import cn.variZoo.Main; +import cn.variZoo.utils.EntityUtil; +import io.papermc.paper.entity.Bucketable; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.attribute.Attribute; +import org.bukkit.attribute.AttributeInstance; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockDispenseEvent; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.event.player.PlayerBucketEmptyEvent; +import org.bukkit.event.player.PlayerBucketEntityEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.WeakHashMap; + +import static org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.SPAWNER_EGG; + +public class BucketFishFix implements Listener { + private final Main plugin; + private final NamespacedKey scaleKey; + private final Set fishBucket; + private final WeakHashMap fishScale; + + public BucketFishFix(Main main) { + this.plugin = main; + scaleKey = new NamespacedKey(plugin, "scale"); + this.fishBucket = new HashSet<>(List.of( + Material.AXOLOTL_BUCKET, + Material.COD_BUCKET, + Material.SALMON_BUCKET, + Material.PUFFERFISH_BUCKET, + Material.TROPICAL_FISH_BUCKET, + Material.TADPOLE_BUCKET + )); + fishScale = new WeakHashMap<>(); + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onFishSpawn(CreatureSpawnEvent event) { + if (event.getEntity() instanceof Bucketable entity) { + if (event.getSpawnReason() == SPAWNER_EGG) { + Location loc = entity.getLocation(); + loc.setYaw(0); + double sca = getFishScale(loc); + if (sca != 0) { + entity.setFromBucket(true); + EntityUtil.setInvalid(entity); + AttributeInstance scale = ((LivingEntity) entity).getAttribute(EntityUtil.getScaleAttribute()); + if (scale == null) return; + scale.setBaseValue(sca); + if (Configuration.other.effectHealth) { + AttributeInstance maxHealth = ((LivingEntity) entity).getAttribute(Attribute.GENERIC_MAX_HEALTH); + if (maxHealth == null) return; + maxHealth.setBaseValue(maxHealth.getBaseValue() * sca); + } + } + } + } + } + + @EventHandler(ignoreCancelled = true) + public void onBucketFish(PlayerBucketEntityEvent event) { + if (event.getEntity() instanceof LivingEntity entity) { + AttributeInstance scale = entity.getAttribute(EntityUtil.getScaleAttribute()); + if (scale == null) return; + ItemStack bucket = event.getEntityBucket(); + ItemMeta meta = bucket.getItemMeta(); + meta.getPersistentDataContainer().set(scaleKey, PersistentDataType.DOUBLE, scale.getValue()); + bucket.setItemMeta(meta); + } + } + + @EventHandler(ignoreCancelled = true) + public void onBucketEmpty(PlayerBucketEmptyEvent event) { + if (!fishBucket.contains(event.getBucket())) return; + ItemStack bucket = event.getPlayer().getInventory().getItem(event.getHand()); + PersistentDataContainer pdc = bucket.getItemMeta().getPersistentDataContainer(); + double scale = pdc.getOrDefault(scaleKey, PersistentDataType.DOUBLE, 1.0); + if (scale == 1.0) return; + Block block = event.getBlock(); + Location loc = block.getLocation().add(0.5, 0, 0.5); + if (block.getBlockData() instanceof Waterlogged) { + loc.add(0, 1, 0); + } + fishScale.put(loc, scale); + } + + @EventHandler(ignoreCancelled = true) + public void onDispenser(BlockDispenseEvent event) { + if (event.getBlock().getType() != Material.DISPENSER) return; + if (!fishBucket.contains(event.getItem().getType())) return; + ItemStack bucket = event.getItem(); + PersistentDataContainer pdc = bucket.getItemMeta().getPersistentDataContainer(); + double scale = pdc.getOrDefault(scaleKey, PersistentDataType.DOUBLE, 1.0); + if (scale == 1.0) return; + BlockFace facing = ((Directional) event.getBlock().getBlockData()).getFacing(); + Block block = event.getBlock().getRelative(facing); + Location loc = block.getLocation().add(0.5, 0, 0.5); + if (block.getBlockData() instanceof Waterlogged) { + loc.add(0, 1, 0); + } + fishScale.put(loc, scale); + } + + private double getFishScale(Location loc) { + if (fishScale.containsKey(loc)) { + Double scale = fishScale.get(loc); + fishScale.remove(loc); + return scale; + } + return 0; + } +} diff --git a/src/main/java/cn/variZoo/listeners/IncreaseDrops.java b/src/main/java/cn/variZoo/listeners/IncreaseDrops.java new file mode 100644 index 0000000..c05fb62 --- /dev/null +++ b/src/main/java/cn/variZoo/listeners/IncreaseDrops.java @@ -0,0 +1,46 @@ +package cn.variZoo.listeners; + +import cn.variZoo.Configuration; +import cn.variZoo.utils.EntityUtil; +import cn.variZoo.utils.XLogger; +import org.bukkit.entity.Animals; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.inventory.ItemStack; +import redempt.crunch.CompiledExpression; +import redempt.crunch.Crunch; +import redempt.crunch.functional.EvaluationEnvironment; + +import java.util.List; + +public class IncreaseDrops implements Listener { + + private CompiledExpression increaseDropsExpression; + + public IncreaseDrops() { + try { + String expression = Configuration.Breed.multiple.hurt.replace(" ", "").replaceAll("\\{([^}]*)}", "$1"); + EvaluationEnvironment env = new EvaluationEnvironment(); + env.setVariableNames("scale"); + increaseDropsExpression = Crunch.compileExpression(expression, env); + increaseDropsExpression.evaluate(1); + } catch (Exception e) { + XLogger.err(e.getMessage()); + } + } + + @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST) + public void onEvent(EntityDeathEvent event) { + if (!(event.getEntity() instanceof Animals entity)) return; + double scale = entity.getAttribute(EntityUtil.getScaleAttribute()).getValue(); + if (scale == 1) return; + double increase = increaseDropsExpression.evaluate(scale); + List drops = event.getDrops(); + for (ItemStack drop : drops) { + drop.setAmount((int) Math.round(drop.getAmount() * increase)); + } + } + +} diff --git a/src/main/java/cn/variZoo/managers/ConfigManager.java b/src/main/java/cn/variZoo/managers/ConfigManager.java new file mode 100644 index 0000000..66e4849 --- /dev/null +++ b/src/main/java/cn/variZoo/managers/ConfigManager.java @@ -0,0 +1,31 @@ +package cn.variZoo.managers; + +import cn.variZoo.Configuration; +import cn.variZoo.Main; +import cn.variZoo.utils.configuration.ConfigurationManager; + +import java.io.File; + +public class ConfigManager { + private final File configFile; + private final Main plugin; + + public ConfigManager(Main main) { + this.plugin = main; + configFile = new File(plugin.getDataFolder(), "config.yml"); + load(); + + } + + public void load() { + + try { + ConfigurationManager.load(Configuration.class, configFile, "version"); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + +} diff --git a/src/main/java/cn/variZoo/managers/ListenerManager.java b/src/main/java/cn/variZoo/managers/ListenerManager.java new file mode 100644 index 0000000..e26abba --- /dev/null +++ b/src/main/java/cn/variZoo/managers/ListenerManager.java @@ -0,0 +1,49 @@ +package cn.variZoo.managers; + +import cn.variZoo.Configuration; +import cn.variZoo.Main; +import cn.variZoo.listeners.*; +import org.bukkit.event.HandlerList; +import org.bukkit.plugin.PluginManager; + +public class ListenerManager { + private final Main plugin; + private final PluginManager pluginManager; + + public ListenerManager(Main main) { + this.plugin = main; + pluginManager = plugin.getServer().getPluginManager(); + load(); + } + + private void load() { + if (!Configuration.enabled) return; + + if (Configuration.AnimalSpawn.basic.apply > 0) { + pluginManager.registerEvents(new AnimalSpawn(), plugin); + } + + if (!Configuration.Breed.inheritance.finalScale.isEmpty()) { + pluginManager.registerEvents(new AnimalBreed(), plugin); + } + + if (Configuration.other.bucketFishFix) { + pluginManager.registerEvents(new BucketFishFix(plugin), plugin); + } + + if (!Configuration.other.increaseDrops.isEmpty()) { + pluginManager.registerEvents(new IncreaseDrops(), plugin); + } + + if (Configuration.other.transform) { + pluginManager.registerEvents(new AnimalTransform(), plugin); + } + } + + public void reload() { + HandlerList.unregisterAll(plugin); + load(); + } + + +} diff --git a/src/main/java/cn/variZoo/utils/DataUtil.java b/src/main/java/cn/variZoo/utils/DataUtil.java new file mode 100644 index 0000000..5d215d0 --- /dev/null +++ b/src/main/java/cn/variZoo/utils/DataUtil.java @@ -0,0 +1,29 @@ +package cn.variZoo.utils; + +import java.util.Arrays; + +public class DataUtil { + public static DataUtil instance; + + public DataUtil() { + instance = this; + } + + public static Degree saveDegree(String value) { + double start = 0; + double end = 0; + double[] fixed = new double[0]; + if (value.contains("-")) { + String[] range = value.replaceAll(" ", "").split("-"); + if (range.length >= 2) { + start = Double.parseDouble(range[0]); + end = Double.parseDouble(range[range.length - 1]); + } + } else { + fixed = Arrays.stream(value.replaceAll(" ", "").split(",")).mapToDouble(Double::parseDouble).toArray(); + } + return new Degree(start, end, fixed); + } + + +} diff --git a/src/main/java/cn/variZoo/utils/Degree.java b/src/main/java/cn/variZoo/utils/Degree.java new file mode 100644 index 0000000..c7d4b20 --- /dev/null +++ b/src/main/java/cn/variZoo/utils/Degree.java @@ -0,0 +1,24 @@ +package cn.variZoo.utils; + +import java.util.concurrent.ThreadLocalRandom; + +public class Degree { + + private final double start; + private final double end; + private final double[] fixed; + + public Degree(double start, double end, double[] fixed) { + this.start = start; + this.end = end; + this.fixed = fixed; + } + + public double getRandom() { + if (fixed.length == 0) { + return ThreadLocalRandom.current().nextDouble(start, end); + } + return fixed[ThreadLocalRandom.current().nextInt(fixed.length)]; + } + +} diff --git a/src/main/java/cn/variZoo/utils/EntityUtil.java b/src/main/java/cn/variZoo/utils/EntityUtil.java new file mode 100644 index 0000000..6651124 --- /dev/null +++ b/src/main/java/cn/variZoo/utils/EntityUtil.java @@ -0,0 +1,76 @@ +package cn.variZoo.utils; + +import cn.variZoo.Main; +import org.bukkit.NamespacedKey; +import org.bukkit.attribute.Attribute; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.event.entity.CreatureSpawnEvent; +import org.bukkit.persistence.PersistentDataType; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class EntityUtil { + + public static EntityUtil instance; + private final Main plugin; + private final Attribute scaleAttribute; + private final NamespacedKey invalidKey; + + public EntityUtil(Main main, Attribute scaleAttribute) { + instance = this; + this.plugin = main; + this.scaleAttribute = scaleAttribute; + this.invalidKey = new NamespacedKey(plugin, "invalid"); + } + + public static Attribute getScaleAttribute() { + return instance.scaleAttribute; + } + + public static void setInvalid(Entity entity) { + entity.getPersistentDataContainer().set(instance.invalidKey, PersistentDataType.BOOLEAN, true); + } + + public static boolean isInvalid(Entity entity) { + return entity.getPersistentDataContainer().has(instance.invalidKey); + } + + public static String getI18nName(Entity entity) { + return entity.customName() == null ? "" : entity.getCustomName(); + } + + public static Set entityToSet(List entities) { + return entities.stream() + .map(String::toUpperCase) + .map(s -> { + try { + return EntityType.valueOf(s); + } catch (IllegalArgumentException e) { + XLogger.warn(s + " is not a valid entity type."); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + public static Set spawnReasonToSet(List spawnReason) { + return spawnReason.stream() + .map(String::toUpperCase) + .map(s -> { + try { + return CreatureSpawnEvent.SpawnReason.valueOf(s); + } catch (IllegalArgumentException e) { + XLogger.warn(s + " is not a valid spawn reason."); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + +} diff --git a/src/main/java/cn/variZoo/utils/Message.java b/src/main/java/cn/variZoo/utils/Message.java new file mode 100644 index 0000000..325562c --- /dev/null +++ b/src/main/java/cn/variZoo/utils/Message.java @@ -0,0 +1,35 @@ +package cn.variZoo.utils; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class Message { + + public static Message instance; + private String breedActionbar; + + public Message() { + instance = this; + } + + + public static void sendMsg(CommandSender sender, String message) { + if (sender instanceof Player p) { + p.sendMessage(message); + } else { + String[] parts = message.split("\\n"); + for (String part : parts) { + XLogger.info(part); + } + } + } + + public static void showHelp(CommandSender sender) { + String message = "VariZoo help\n" + + "/varizoo reload 重启插件"; + sendMsg(sender, message); + + } + + +} diff --git a/src/main/java/cn/variZoo/utils/Scheduler.java b/src/main/java/cn/variZoo/utils/Scheduler.java new file mode 100644 index 0000000..a2dc587 --- /dev/null +++ b/src/main/java/cn/variZoo/utils/Scheduler.java @@ -0,0 +1,90 @@ +package cn.variZoo.utils; + +import cn.variZoo.Main; + +import java.util.concurrent.TimeUnit; + +public class Scheduler { + public static Scheduler instance; + private final boolean isFolia; + private final Main plugin; + + public Scheduler(Main main) { + this.plugin = main; + instance = this; + isFolia = main.isFolia(); + } + + public static void cancelAll() { + if (instance.isFolia) { + instance.plugin.getServer().getGlobalRegionScheduler().cancelTasks(instance.plugin); + instance.plugin.getServer().getGlobalRegionScheduler().cancelTasks(instance.plugin); + } else { + instance.plugin.getServer().getScheduler().cancelTasks(instance.plugin); + } + } + + /** + * Run a task later + * + * @param task The task to run + * @param delay The delay in ticks (20 ticks = 1 second) + */ + public static void runTaskLater(Runnable task, long delay) { + if (delay <= 0) { + runTask(task); + return; + } + if (instance.isFolia) { + instance.plugin.getServer().getGlobalRegionScheduler().runDelayed(instance.plugin, (plugin) -> task.run(), delay); + } else { + instance.plugin.getServer().getScheduler().runTaskLater(instance.plugin, task, delay); + } + } + + /** + * Run a task + * + * @param task The task to run + */ + public static void runTask(Runnable task) { + if (instance.isFolia) { + instance.plugin.getServer().getGlobalRegionScheduler().run(instance.plugin, (plugin) -> task.run()); + } else { + instance.plugin.getServer().getScheduler().runTask(instance.plugin, task); + } + } + + + /** + * Run a task later asynchronously + * + * @param task The task to run + * @param delay The delay in milliseconds + */ + public static void runTaskLaterAsync(Runnable task, long delay) { + if (delay <= 0) { + runTaskAsync(task); + return; + } + if (instance.isFolia) { + instance.plugin.getServer().getAsyncScheduler().runDelayed(instance.plugin, (plugin) -> task.run(), delay * 50, TimeUnit.MILLISECONDS); + } else { + instance.plugin.getServer().getScheduler().runTaskLaterAsynchronously(instance.plugin, task, delay); + } + } + + /** + * Run a task asynchronously + * + * @param task The task to run + */ + public static void runTaskAsync(Runnable task) { + if (instance.isFolia) { + instance.plugin.getServer().getAsyncScheduler().runNow(instance.plugin, (plugin) -> task.run()); + } else { + instance.plugin.getServer().getScheduler().runTaskAsynchronously(instance.plugin, task); + } + } + +} diff --git a/src/main/java/cn/variZoo/utils/XLogger.java b/src/main/java/cn/variZoo/utils/XLogger.java new file mode 100644 index 0000000..33de903 --- /dev/null +++ b/src/main/java/cn/variZoo/utils/XLogger.java @@ -0,0 +1,45 @@ +package cn.variZoo.utils; + +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; + +import java.util.logging.Logger; + +public class XLogger { + public static XLogger instance; + private final Logger logger; + + public XLogger() { + instance = this; + this.logger = Logger.getLogger("VariZoo"); + } + + public XLogger(@Nullable JavaPlugin plugin) { + instance = this; + this.logger = plugin != null ? plugin.getLogger() : Logger.getLogger("VariZoo"); + } + + public static void info(String message) { + instance.logger.info(message); + } + + public static void info(String message, Object... args) { + instance.logger.info(String.format(message, args)); + } + + public static void warn(String message) { + instance.logger.warning(message); + } + + public static void warn(String message, Object... args) { + instance.logger.warning(String.format(message, args)); + } + + public static void err(String message) { + instance.logger.severe(message); + } + + public static void err(String message, Object... args) { + instance.logger.severe(String.format(message, args)); + } +} diff --git a/src/main/java/cn/variZoo/utils/configuration/Comment.java b/src/main/java/cn/variZoo/utils/configuration/Comment.java new file mode 100644 index 0000000..191d26c --- /dev/null +++ b/src/main/java/cn/variZoo/utils/configuration/Comment.java @@ -0,0 +1,12 @@ +package cn.variZoo.utils.configuration; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation for adding single line comments to configuration fields. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface Comment { + String[] value(); +} diff --git a/src/main/java/cn/variZoo/utils/configuration/ConfigurationFile.java b/src/main/java/cn/variZoo/utils/configuration/ConfigurationFile.java new file mode 100644 index 0000000..1430932 --- /dev/null +++ b/src/main/java/cn/variZoo/utils/configuration/ConfigurationFile.java @@ -0,0 +1,4 @@ +package cn.variZoo.utils.configuration; + +public class ConfigurationFile { +} diff --git a/src/main/java/cn/variZoo/utils/configuration/ConfigurationManager.java b/src/main/java/cn/variZoo/utils/configuration/ConfigurationManager.java new file mode 100644 index 0000000..2ebb1c5 --- /dev/null +++ b/src/main/java/cn/variZoo/utils/configuration/ConfigurationManager.java @@ -0,0 +1,160 @@ +package cn.variZoo.utils.configuration; + +import cn.variZoo.utils.XLogger; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.List; + +/** + * Utility class for loading and saving configuration files. + *

+ * This class uses reflection to read and write configuration files. Capable of reading and writing nested configuration parts. + */ +public class ConfigurationManager { + + /** + * Load the configuration file. + * + * @param clazz The configuration file class. The class should extend {@link ConfigurationFile}. + * @param file The file to load. + * @throws Exception If failed to load the file. + */ + public static void load(Class clazz, File file) throws Exception { + if (!file.exists()) { + save(clazz, file); + return; + } + YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file); + readConfigurationFile(yaml, clazz, null); + } + + /** + * Load the configuration file and update the version field if needed. + * + * @param clazz The configuration file class. The class should extend {@link ConfigurationFile}. + * @param file The file to load. + * @param versionFieldName The name of the version field. + * @throws Exception If failed to load the file. + */ + public static void load(Class clazz, File file, String versionFieldName) throws Exception { + Field versionField = clazz.getField(versionFieldName); + int currentVersion = versionField.getInt(null); + load(clazz, file); + if (versionField.getInt(null) != currentVersion) { + File backup = new File(file.getParentFile(), file.getName() + ".bak"); + if (backup.exists() && !backup.delete()) { + throw new Exception("Failed to delete the backup configuration file."); + } + if (!file.renameTo(backup)) { + throw new Exception("Failed to backup the configuration file."); + } + clazz.getField(versionFieldName).set(null, currentVersion); + save(clazz, file); + } + } + + /** + * Save the configuration file. + * + * @param clazz The configuration file class. The class should extend {@link ConfigurationFile}. + * @param file The file to save. + * @throws Exception If failed to save the file. + */ + public static void save(Class clazz, File file) throws Exception { + createIfNotExist(file); + YamlConfiguration yaml = new YamlConfiguration(); + writeConfigurationFile(yaml, clazz, null); + yaml.save(file); + } + + private static void writeConfigurationFile(YamlConfiguration yaml, Class clazz, String prefix) throws Exception { + for (Field field : clazz.getFields()) { + field.setAccessible(true); + String key = camelToKebab(field.getName()); + if (prefix != null && !prefix.isEmpty()) { + key = prefix + "." + key; + } + // if field is extending ConfigurationPart, recursively write the content + if (ConfigurationPart.class.isAssignableFrom(field.getType())) { + XLogger.info("%s is a ConfigurationPart.", field.getName()); + writeConfigurationPart(yaml, (ConfigurationPart) field.get(null), key); + } else { + XLogger.info("Writing %s to %s.", field.getName(), key); + yaml.set(key, field.get(null)); + } + if (field.isAnnotationPresent(Comment.class)) { + yaml.setComments(key, List.of(field.getAnnotation(Comment.class).value())); + } + } + } + + private static void writeConfigurationPart(YamlConfiguration yaml, ConfigurationPart obj, String key) throws Exception { + for (Field field : obj.getClass().getFields()) { + field.setAccessible(true); + String newKey = key + "." + camelToKebab(field.getName()); + if (ConfigurationPart.class.isAssignableFrom(field.getType())) { + writeConfigurationPart(yaml, (ConfigurationPart) field.get(obj), newKey); + } else { + yaml.set(newKey, field.get(obj)); + } + if (field.isAnnotationPresent(Comment.class)) { + yaml.setComments(newKey, List.of(field.getAnnotation(Comment.class).value())); + } + } + } + + private static void createIfNotExist(File file) throws Exception { + if (file.exists()) return; + if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) + throw new Exception("Failed to create %s directory.".formatted(file.getParentFile().getAbsolutePath())); + if (!file.createNewFile()) throw new Exception("Failed to create %s file.".formatted(file.getAbsolutePath())); + } + + private static void readConfigurationFile(YamlConfiguration yaml, Class clazz, String prefix) throws Exception { + for (Field field : clazz.getFields()) { + field.setAccessible(true); + String key = camelToKebab(field.getName()); + if (prefix != null && !prefix.isEmpty()) { + key = prefix + "." + key; + } + if (!yaml.contains(key)) { + XLogger.warn("Can't find %s from %s.", field.getName(), key); + continue; + } + if (ConfigurationPart.class.isAssignableFrom(field.getType())) { + readConfigurationPart(yaml, (ConfigurationPart) field.get(null), key); + } else { + field.set(null, yaml.get(key)); + } + } + } + + private static void readConfigurationPart(YamlConfiguration yaml, ConfigurationPart obj, String key) throws Exception { + for (Field field : obj.getClass().getFields()) { + field.setAccessible(true); + String newKey = key + "." + camelToKebab(field.getName()); + if (!yaml.contains(newKey)) { + XLogger.warn("Can't find %s from %s.", field.getName(), key); + continue; + } + if (ConfigurationPart.class.isAssignableFrom(field.getType())) { + readConfigurationPart(yaml, (ConfigurationPart) field.get(obj), newKey); + } else { + field.set(obj, yaml.get(newKey)); + } + } + } + + /** + * Converts a camelCase string to kebab-case. + * + * @param camel The camelCase string. + * @return The kebab-case string. + */ + private static String camelToKebab(String camel) { + return camel.replaceAll("([a-z])([A-Z]+)", "$1-$2").toLowerCase(); + } + +} \ No newline at end of file diff --git a/src/main/java/cn/variZoo/utils/configuration/ConfigurationPart.java b/src/main/java/cn/variZoo/utils/configuration/ConfigurationPart.java new file mode 100644 index 0000000..6cafb6f --- /dev/null +++ b/src/main/java/cn/variZoo/utils/configuration/ConfigurationPart.java @@ -0,0 +1,9 @@ +package cn.variZoo.utils.configuration; + +/** + * Marker interface for unique sections of a configuration. + *

+ * The items in the configuration part should be public fields. + */ +public abstract class ConfigurationPart { +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..2d7b8d9 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,22 @@ +name: VariZoo +version: '1.1.0' +main: cn.variZoo.Main +api-version: '1.21' +folia-supported: true + +commands: + varizoo: + description: Main command. + aliases: + - vz + usage: / +permissions: + varizoo.reload: + description: Allow the player to reload the plugin. + default: op + varizoo.help: + description: Allow the player to reload the plugin. + default: true + varizoo.skip.breed: + description: Allow the player to breed animal without scale. + default: op \ No newline at end of file