diff --git a/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java b/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java index 06ea024a8..e4945103f 100644 --- a/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java +++ b/core/src/main/java/com/nisovin/magicspells/listeners/MagicSpellListener.java @@ -6,12 +6,17 @@ import org.bukkit.entity.LivingEntity; import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityRemoveEvent; +import org.bukkit.event.entity.EntityDismountEvent; +import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.event.entity.EntityDamageByEntityEvent; import org.bukkit.event.inventory.InventoryPickupItemEvent; import com.nisovin.magicspells.Perm; import com.nisovin.magicspells.Spell; +import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.EntityData; import com.nisovin.magicspells.events.SpellTargetEvent; import com.nisovin.magicspells.zones.NoMagicZoneManager; import com.nisovin.magicspells.spelleffects.effecttypes.*; @@ -59,6 +64,26 @@ public void onEntityDamage(EntityDamageEvent event) { event.setCancelled(true); } + @EventHandler + public void onEntityRemove(EntityRemoveEvent event) { + Util.forEachPassenger(event.getEntity(), passenger -> { + PersistentDataContainer container = passenger.getPersistentDataContainer(); + if (!container.has(EntityData.MS_PASSENGER)) return; + + if (passenger.isPersistent()) { + container.remove(EntityData.MS_PASSENGER); + return; + } + + passenger.remove(); + }); + } + + @EventHandler + public void onEntityDismount(EntityDismountEvent event) { + event.getEntity().getPersistentDataContainer().remove(EntityData.MS_PASSENGER); + } + private boolean isMSEntity(Entity entity) { return entity.getScoreboardTags().contains(ArmorStandEffect.ENTITY_TAG) || entity.getScoreboardTags().contains(EntityEffect.ENTITY_TAG); } diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java index e984dd9cf..b2862b7cd 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/ArmorStandEffect.java @@ -2,7 +2,6 @@ import org.bukkit.Location; import org.bukkit.entity.ArmorStand; -import org.bukkit.entity.EntityType; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.EquipmentSlot; import org.bukkit.configuration.ConfigurationSection; @@ -11,9 +10,11 @@ import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.EntityData; +import com.nisovin.magicspells.util.config.ConfigData; import com.nisovin.magicspells.spelleffects.SpellEffect; import com.nisovin.magicspells.util.magicitems.MagicItem; import com.nisovin.magicspells.util.magicitems.MagicItems; +import com.nisovin.magicspells.util.config.ConfigDataUtil; @Name("armorstand") public class ArmorStandEffect extends SpellEffect { @@ -22,14 +23,12 @@ public class ArmorStandEffect extends SpellEffect { private EntityData entityData; - private boolean gravity; - - private String customName; - private boolean customNameVisible; + private ConfigData gravity; + private ConfigData disableSlots; private ItemStack headItem; - private ItemStack mainhandItem; - private ItemStack offhandItem; + private ItemStack offHandItem; + private ItemStack mainHandItem; @Override protected void loadFromConfig(ConfigurationSection config) { @@ -37,41 +36,38 @@ protected void loadFromConfig(ConfigurationSection config) { if (section == null) return; entityData = new EntityData(section); - entityData.setEntityType(data -> EntityType.ARMOR_STAND); - - gravity = section.getBoolean("gravity", false); - - customName = section.getString("custom-name", ""); - customNameVisible = section.getBoolean("custom-name-visible", false); - String strMagicItem = section.getString("head", ""); - MagicItem magicItem = MagicItems.getMagicItemFromString(strMagicItem); - if (magicItem != null) headItem = magicItem.getItemStack(); + gravity = ConfigDataUtil.getBoolean(section, "gravity", false); + disableSlots = ConfigDataUtil.getBoolean(section, "disable-slots", true); - strMagicItem = section.getString("mainhand", ""); - magicItem = MagicItems.getMagicItemFromString(strMagicItem); - if (magicItem != null) mainhandItem = magicItem.getItemStack(); + MagicItem item = MagicItems.getMagicItemFromString(section.getString("head")); + if (item != null) headItem = item.getItemStack(); - strMagicItem = section.getString("offhand", ""); - magicItem = MagicItems.getMagicItemFromString(strMagicItem); - if (magicItem != null) offhandItem = magicItem.getItemStack(); + item = MagicItems.getMagicItemFromString(section.getString("offhand")); + if (item != null) offHandItem = item.getItemStack(); + item = MagicItems.getMagicItemFromString(section.getString("mainhand")); + if (item != null) mainHandItem = item.getItemStack(); } @Override protected ArmorStand playArmorStandEffectLocation(Location location, SpellData data) { - return (ArmorStand) entityData.spawn(location, data, entity -> { - ArmorStand armorStand = (ArmorStand) entity; - - armorStand.addScoreboardTag(ENTITY_TAG); - armorStand.setGravity(gravity); - armorStand.setSilent(true); - armorStand.customName(Util.getMiniMessage(customName, data)); - armorStand.setCustomNameVisible(customNameVisible); - - armorStand.setItem(EquipmentSlot.HEAD, headItem); - armorStand.setItem(EquipmentSlot.HAND, mainhandItem); - armorStand.setItem(EquipmentSlot.OFF_HAND, offhandItem); + boolean gravity = this.gravity.get(data); + boolean disableSlots = this.disableSlots.get(data); + + return entityData.spawn(location, data, ArmorStand.class, stand -> { + stand.setSilent(true); + stand.addScoreboardTag(ENTITY_TAG); + + stand.setGravity(gravity); + if (disableSlots) stand.setDisabledSlots(EquipmentSlot.values()); + + stand.setItem(EquipmentSlot.HEAD, headItem); + stand.setItem(EquipmentSlot.HAND, mainHandItem); + stand.setItem(EquipmentSlot.OFF_HAND, offHandItem); + }, stand -> { + stand.setPersistent(false); + Util.forEachPassenger(stand, e -> e.setPersistent(false)); }); } diff --git a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java index cd5dcb816..339135c84 100644 --- a/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java +++ b/core/src/main/java/com/nisovin/magicspells/spelleffects/effecttypes/EntityEffect.java @@ -5,10 +5,10 @@ import org.bukkit.Location; import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; import org.bukkit.configuration.ConfigurationSection; import com.nisovin.magicspells.util.Name; +import com.nisovin.magicspells.util.Util; import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.util.SpellData; import com.nisovin.magicspells.util.EntityData; @@ -27,9 +27,7 @@ public class EntityEffect extends SpellEffect { private ConfigData duration; - private ConfigData silent; private ConfigData gravity; - private ConfigData enableAI; @Override protected void loadFromConfig(ConfigurationSection config) { @@ -40,9 +38,7 @@ protected void loadFromConfig(ConfigurationSection config) { duration = ConfigDataUtil.getInteger(section, "duration", 0); - silent = ConfigDataUtil.getBoolean(section, "silent", false); gravity = ConfigDataUtil.getBoolean(section, "gravity", false); - enableAI = ConfigDataUtil.getBoolean(section, "ai", true); } @Override @@ -50,10 +46,9 @@ protected Entity playEntityEffectLocation(Location location, SpellData data) { return entityData.spawn(location, data, entity -> { entity.addScoreboardTag(ENTITY_TAG); entity.setGravity(gravity.get(data)); - entity.setSilent(silent.get(data)); + }, entity -> { entity.setPersistent(false); - - if (entity instanceof LivingEntity livingEntity) livingEntity.setAI(enableAI.get(data)); + Util.forEachPassenger(entity, e -> e.setPersistent(false)); }); } @@ -75,7 +70,7 @@ public Runnable playEffectLocation(Location location, SpellData data) { @Override public void turnOff() { - for (Entity entity : entities) entity.remove(); + entities.forEach(Entity::remove); entities.clear(); } diff --git a/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java index 691bc8ab4..e3c71dcae 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/MultiSpell.java @@ -89,17 +89,18 @@ else if (action.isSpell()) { } } else { if (customSpellCastChance.get(data)) { - double total = 0; - for (ActionChance actionChance : actions) total += actionChance.chance; + double total = actions.stream().mapToDouble(ActionChance::chance).sum(); + if (total <= 0) return new CastResult(PostCastAction.ALREADY_HANDLED, data); + + double selected = random.nextDouble(total); - double index = random.nextDouble(total); Action action = null; - double subChance = 0; + double current = 0; for (ActionChance actionChance : actions) { - subChance += actionChance.chance; - + current += actionChance.chance; + if (selected >= current) continue; action = actionChance.action; - if (subChance > index) break; + break; } if (action != null && action.isSpell()) action.getSpell().subcast(data); @@ -141,17 +142,19 @@ else if (action.isSpell()) { } } else { if (customSpellCastChance.get(subData)) { - double total = 0; - for (ActionChance actionChance : actions) total += actionChance.chance; + double total = actions.stream().mapToDouble(ActionChance::chance).sum(); + if (total <= 0) return false; + + double selected = random.nextDouble(total); - double index = random.nextDouble(total); Action action = null; - double subChance = 0; + double current = 0; for (ActionChance actionChance : actions) { - subChance += actionChance.chance; + current += actionChance.chance; + if (selected >= current) continue; action = actionChance.action; - if (subChance > index) break; + break; } if (action != null && action.isSpell()) action.getSpell().getSpell().castFromConsole(sender, args); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java index 55aabf8f9..ba8361d9c 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/MinionSpell.java @@ -22,6 +22,7 @@ import org.bukkit.attribute.AttributeModifier; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.configuration.ConfigurationSection; import io.papermc.paper.entity.TeleportFlag; @@ -38,26 +39,32 @@ public class MinionSpell extends BuffSpell { - private final Map minions; - private final Map players; - private final Map targets; + private static final DeprecationNotice BABY_DEPRECATION_NOTICE = new DeprecationNotice( + "The 'baby' option of '.buff.MinionSpell' overrides the one from 'minion' Entity Data option.", + "Use the 'baby' option in the 'minion' Entity Data to avoid the override.", + "https://github.com/TheComputerGeek2/MagicSpells/wiki/Deprecations#buffminionspell-baby" + ); - private final int[] chances; + private final Map minions = new HashMap<>(); + private final Map players = new HashMap<>(); + private final Map mobChances = new HashMap<>(); + private final Map targets = new ConcurrentHashMap<>(); + + private EntityData entityData; private ValidTargetList minionTargetList; - private EntityType[] creatureTypes; - private final ConfigData powerAffectsHealth; - private final ConfigData gravity; - private final ConfigData baby; private boolean preventCombust; + private final ConfigData baby; + private final ConfigData gravity; + private final ConfigData powerAffectsHealth; - private final ConfigData powerHealthFactor; - private final ConfigData maxHealth; - private final ConfigData health; private double followRange; private double followSpeed; private double maxDistance; + private final ConfigData health; + private final ConfigData maxHealth; + private final ConfigData powerHealthFactor; private final ConfigData spawnOffset; @@ -92,29 +99,27 @@ public class MinionSpell extends BuffSpell { public MinionSpell(MagicConfig config, String spellName) { super(config, spellName); - minions = new HashMap<>(); - players = new HashMap<>(); - targets = new ConcurrentHashMap<>(); + ConfigurationSection minionSection = getConfigSection("minion"); + if (minionSection != null) entityData = new EntityData(minionSection); // Formatted as - List c = getConfigStringList("mob-chances", new ArrayList<>()); - if (c.isEmpty()) c.add("Zombie 100"); - - creatureTypes = new EntityType[c.size()]; - chances = new int[c.size()]; - for (int i = 0; i < c.size(); i++) { - String[] data = c.get(i).split(" "); - EntityType creatureType = MobUtil.getEntityType(data[0]); - int chance = 0; - if (creatureType != null) { - try { - chance = Integer.parseInt(data[1]); - } catch (NumberFormatException e) { - // No op - } + List mobChanceList = getConfigStringList("mob-chances", new ArrayList<>()); + if (mobChanceList.isEmpty()) mobChanceList.add("Zombie 100"); + + for (int i = 0; i < mobChanceList.size(); i++) { + String[] splits = mobChanceList.get(i).split(" "); + + EntityType type = MobUtil.getEntityType(splits[0]); + if (type == null) { + MagicSpells.error("MinionSpell '" + internalName + "' has an invalid Entity Type specified in 'mob-chances' at index " + i + ": " + splits[0]); + continue; + } + + try { + mobChances.put(type, Integer.parseInt(splits[1])); + } catch (NumberFormatException ignored) { + MagicSpells.error("MinionSpell '" + internalName + "' has an invalid chance value specified in 'mob-chances' at index " + i + ": " + splits[1]); } - creatureTypes[i] = creatureType; - chances[i] = chance; } // Potion effects @@ -196,22 +201,28 @@ public MinionSpell(MagicConfig config, String spellName) { leggingsDropChance = getConfigFloat("leggings-drop-chance", 0) / 100F; bootsDropChance = getConfigFloat("boots-drop-chance", 0) / 100F; + minionName = getConfigString("minion-name", ""); spawnSpellName = getConfigString("spell-on-spawn", ""); - attackSpellName = getConfigString("spell-on-attack", ""); deathSpellName = getConfigString("spell-on-death", ""); + attackSpellName = getConfigString("spell-on-attack", ""); spawnOffset = getConfigDataVector("spawn-offset", new Vector(1, 0, 0)); - followRange = getConfigDouble("follow-range", 1.5) * -1; + + health = getConfigDataDouble("health", 20); followSpeed = getConfigDouble("follow-speed", 1); maxDistance = getConfigDouble("max-distance", 30); - powerAffectsHealth = getConfigDataBoolean("power-affects-health", false); - powerHealthFactor = getConfigDataDouble("power-health-factor", 1); maxHealth = getConfigDataDouble("max-health", 20); - health = getConfigDataDouble("health", 20); - minionName = getConfigString("minion-name", ""); - gravity = getConfigDataBoolean("gravity", true); + followRange = getConfigDouble("follow-range", 1.5) * -1; + powerHealthFactor = getConfigDataDouble("power-health-factor", 1); + baby = getConfigDataBoolean("baby", false); + gravity = getConfigDataBoolean("gravity", true); preventCombust = getConfigBoolean("prevent-sun-burn", true); + powerAffectsHealth = getConfigDataBoolean("power-affects-health", false); + + MagicSpells.getDeprecationManager().addDeprecation(this, BABY_DEPRECATION_NOTICE, + entityData != null && (!baby.isConstant() || baby.get()) + ); } @Override @@ -234,44 +245,73 @@ public void initialize() { public boolean castBuff(SpellData data) { if (!(data.target() instanceof Player target)) return false; // Selecting the mob - EntityType creatureType = null; - int num = random.nextInt(100); - int n = 0; - for (int i = 0; i < creatureTypes.length; i++) { - if (num < chances[i] + n) { - creatureType = creatureTypes[i]; + EntityType entityType = null; + if (entityData == null) { + int total = mobChances.values().stream().mapToInt(Integer::intValue).sum(); + int selected = random.nextInt(total); + + int current = 0; + for (Map.Entry entry : mobChances.entrySet()) { + current += entry.getValue(); + if (selected >= current) continue; + + entityType = entry.getKey(); break; } - - n += chances[i]; } + else entityType = entityData.getEntityType().get(data); - if (creatureType == null) return false; + if (entityType == null) { + MagicSpells.error("MinionSpell '" + internalName + "' is missing entity type!"); + return false; + } + Class entityClass = entityType.getEntityClass(); + if (entityClass == null || !Mob.class.isAssignableFrom(entityClass)) { + MagicSpells.error("MinionSpell '" + internalName + "' can only summon mobs!"); + return false; + } + //noinspection unchecked + Class mobClass = (Class) entityClass; // Spawn location Location loc = target.getLocation().clone(); loc.setPitch(0); - Vector spawnOffset = this.spawnOffset.get(data); loc.add(0, spawnOffset.getY(), 0); Util.applyRelativeOffset(loc, spawnOffset.setY(0)); // Spawn creature - LivingEntity minion = (LivingEntity) target.getWorld().spawnEntity(loc, creatureType); - if (!(minion instanceof Mob)) { - minion.remove(); - MagicSpells.error("MinionSpell '" + internalName + "' can only summon mobs!"); - return false; - } + Mob minion; + if (entityData == null) minion = target.getWorld().spawn(loc, mobClass, mob -> { + prepareMob(mob, target, data); - if (minion instanceof Ageable ageable) { + if (!(mob instanceof Ageable ageable)) return; if (baby.get(data)) ageable.setBaby(); else ageable.setAdult(); + }); + else minion = entityData.spawn(loc, data, mobClass, mob -> prepareMob(mob, target, data), mob -> { + mob.setPersistent(false); + Util.forEachPassenger(mob, e -> e.setPersistent(false)); + }); + + if (spawnSpell != null) { + SpellData castData = data.builder().caster(target).target(minion).location(minion.getLocation()).recipient(null).build(); + spawnSpell.subcast(castData); } + minions.put(target.getUniqueId(), minion); + players.put(minion, target.getUniqueId()); + return true; + } + + private void prepareMob(Mob minion, Player target, SpellData data) { minion.setGravity(gravity.get(data)); - minion.customName(Util.getMiniMessage(MagicSpells.doReplacements(minionName, target, data, "%c", target.getName()))); - minion.setCustomNameVisible(true); + + String customName = MagicSpells.doReplacements(minionName, target, data, "%c", target.getName()); + if (!customName.isEmpty()) { + minion.customName(Util.getMiniMessage(customName)); + minion.setCustomNameVisible(true); + } double powerHealthFactor = this.powerHealthFactor.get(data); double maxHealth = this.maxHealth.get(data); @@ -284,15 +324,8 @@ public boolean castBuff(SpellData data) { minion.setHealth(health); } - if (spawnSpell != null) { - SpellData castData = data.builder().caster(target).target(minion).location(minion.getLocation()).recipient(null).build(); - spawnSpell.subcast(castData); - } - - // Apply potion effects if (potionEffects != null) minion.addPotionEffects(potionEffects); - // Apply attributes if (attributes != null) { attributes.asMap().forEach((attribute, modifiers) -> { AttributeInstance attributeInstance = minion.getAttribute(attribute); @@ -302,8 +335,7 @@ public boolean castBuff(SpellData data) { }); } - // Equip the minion - final EntityEquipment eq = minion.getEquipment(); + EntityEquipment eq = minion.getEquipment(); if (mainHandItem != null) eq.setItemInMainHand(mainHandItem.clone()); if (offHandItem != null) eq.setItemInOffHand(offHandItem.clone()); if (helmet != null) eq.setHelmet(helmet.clone()); @@ -311,17 +343,12 @@ public boolean castBuff(SpellData data) { if (leggings != null) eq.setLeggings(leggings.clone()); if (boots != null) eq.setBoots(boots.clone()); - // Equipment drop chance eq.setItemInMainHandDropChance(mainHandItemDropChance); eq.setItemInOffHandDropChance(offHandItemDropChance); eq.setHelmetDropChance(helmetDropChance); eq.setChestplateDropChance(chestplateDropChance); eq.setLeggingsDropChance(leggingsDropChance); eq.setBootsDropChance(bootsDropChance); - - minions.put(target.getUniqueId(), minion); - players.put(minion, target.getUniqueId()); - return true; } @Override @@ -330,7 +357,7 @@ public boolean isActive(LivingEntity entity) { } public boolean isMinion(LivingEntity entity) { - return minions.containsValue(entity); + return minions.values().stream().anyMatch(mob -> mob.getUniqueId().equals(entity.getUniqueId())); } @Override @@ -356,7 +383,7 @@ public void onEntityTarget(EntityTargetEvent e) { Player pl = Bukkit.getPlayer(players.get(minion)); if (pl == null) return; - if (targets.get(pl.getUniqueId()) == null || !targets.containsKey(pl.getUniqueId()) || !targets.get(pl.getUniqueId()).isValid()) { + if (!targets.containsKey(pl.getUniqueId()) || !targets.get(pl.getUniqueId()).isValid()) { e.setCancelled(true); return; } @@ -453,7 +480,7 @@ else if (e.getDamager() instanceof Projectile projectile) { // Check if the entity can be targeted by the minion if (!minionTargetList.canTarget(pl, entity)) return; - targets.put(pl.getUniqueId(),entity); + targets.put(pl.getUniqueId(), entity); MobUtil.setTarget(minions.get(pl.getUniqueId()), entity); addUseAndChargeCost(pl); @@ -479,7 +506,7 @@ else if (e.getDamager() instanceof Projectile projectile) { Location loc = pl.getLocation().clone(); loc.add(loc.getDirection().setY(0).normalize().multiply(followRange)); - ((Mob) minions.get(pl.getUniqueId())).getPathfinder().moveTo(loc, followSpeed); + minions.get(pl.getUniqueId()).getPathfinder().moveTo(loc, followSpeed); } } } @@ -527,10 +554,9 @@ public void onPlayerMove(PlayerMoveEvent e) { if (e.getFrom().getBlock().equals(e.getTo().getBlock())) return; Player pl = e.getPlayer(); if (!isActive(pl)) return; - LivingEntity minion = minions.get(pl.getUniqueId()); + Mob minion = minions.get(pl.getUniqueId()); if ((pl.getWorld().equals(minion.getWorld()) && pl.getLocation().distanceSquared(minion.getLocation()) > maxDistance * maxDistance) || targets.get(pl.getUniqueId()) == null || !targets.containsKey(pl.getUniqueId())) { - // The minion has a target, but he is far away from his owner, remove his current target if (targets.get(pl.getUniqueId()) != null) { targets.remove(pl.getUniqueId()); @@ -540,7 +566,7 @@ public void onPlayerMove(PlayerMoveEvent e) { // The distance between minion and his owner is greater that the defined max distance or the minion has no targets, he will follow his owner Location loc = pl.getLocation().clone(); loc.add(loc.getDirection().setY(0).normalize().multiply(followRange)); - ((Mob) minions.get(pl.getUniqueId())).getPathfinder().moveTo(loc, followSpeed); + minion.getPathfinder().moveTo(loc, followSpeed); } } @@ -567,7 +593,7 @@ public void onEntityUnload(ChunkUnloadEvent event) { } } - public Map getMinions() { + public Map getMinions() { return minions; } @@ -595,14 +621,6 @@ public void setMinionTargetList(ValidTargetList minionTargetList) { this.minionTargetList = minionTargetList; } - public EntityType[] getCreatureTypes() { - return creatureTypes; - } - - public void setCreatureTypes(EntityType[] creatureTypes) { - this.creatureTypes = creatureTypes; - } - public boolean shouldPreventCombust() { return preventCombust; } diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java index 736a58188..bb70a70a5 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/ext/DisguiseSpell.java @@ -7,7 +7,6 @@ import org.bukkit.DyeColor; import org.bukkit.entity.Villager; import org.bukkit.entity.LivingEntity; -import org.bukkit.inventory.ItemStack; import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.util.DependsOn; @@ -151,7 +150,7 @@ public boolean castBuff(SpellData data) { creeperWatcher.setPowered(entityData.getPowered().get(data)); if (watcher instanceof DroppedItemWatcher droppedItemWatcher) - droppedItemWatcher.setItemStack(new ItemStack(entityData.getDroppedItemStack().get(data))); + droppedItemWatcher.setItemStack(entityData.getDroppedItemStack().get(data)); if (watcher instanceof EndermanWatcher endermanWatcher) endermanWatcher.setItemInMainHand(entityData.getCarriedBlockData().get(data).getMaterial()); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityDataApplySpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityDataApplySpell.java new file mode 100644 index 000000000..dd7b16cc0 --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityDataApplySpell.java @@ -0,0 +1,37 @@ +package com.nisovin.magicspells.spells.targeted; + +import org.bukkit.entity.LivingEntity; + +import com.nisovin.magicspells.util.*; +import com.nisovin.magicspells.spells.TargetedSpell; +import com.nisovin.magicspells.spells.TargetedEntitySpell; + +public class EntityDataApplySpell extends TargetedSpell implements TargetedEntitySpell { + + private final EntityData entityData; + + public EntityDataApplySpell(MagicConfig config, String spellName) { + super(config, spellName); + + entityData = new EntityData(getConfigSection("entity")); + } + + @Override + public CastResult cast(SpellData data) { + TargetInfo info = getTargetedEntity(data); + if (info.noTarget()) return noTarget(info); + + return castAtEntity(info.spellData()); + } + + @Override + public CastResult castAtEntity(SpellData data) { + if (!data.hasTarget()) return noTarget(data); + + entityData.apply(data.target(), data); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + +} diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityRemoveSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityRemoveSpell.java new file mode 100644 index 000000000..f834c85cf --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntityRemoveSpell.java @@ -0,0 +1,34 @@ +package com.nisovin.magicspells.spells.targeted; + +import org.bukkit.entity.Player; +import org.bukkit.entity.LivingEntity; + +import com.nisovin.magicspells.util.*; +import com.nisovin.magicspells.spells.TargetedSpell; +import com.nisovin.magicspells.spells.TargetedEntitySpell; + +public class EntityRemoveSpell extends TargetedSpell implements TargetedEntitySpell { + + public EntityRemoveSpell(MagicConfig config, String spellName) { + super(config, spellName); + } + + @Override + public CastResult cast(SpellData data) { + TargetInfo info = getTargetedEntity(data); + if (info.noTarget()) return noTarget(info); + + return castAtEntity(info.spellData()); + } + + @Override + public CastResult castAtEntity(SpellData data) { + if (!data.hasTarget() || data.target() instanceof Player) return noTarget(data); + + data.target().remove(); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + +} diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java index fb8587562..ef439b783 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SpawnEntitySpell.java @@ -33,7 +33,6 @@ import net.kyori.adventure.text.Component; -import com.destroystokyo.paper.entity.ai.MobGoals; import com.destroystokyo.paper.event.entity.EntityRemoveFromWorldEvent; import com.nisovin.magicspells.util.*; @@ -185,11 +184,11 @@ public SpawnEntitySpell(MagicConfig config, String spellName) { setOwner = getConfigDataBoolean("set-owner", true); removeAI = getConfigDataBoolean("remove-ai", false); invulnerable = getConfigDataBoolean("invulnerable", false); + cancelAttack = getConfigDataBoolean("cancel-attack", true); useCasterName = getConfigDataBoolean("use-caster-name", false); centerLocation = getConfigDataBoolean("center-location", false); addLookAtPlayerAI = getConfigDataBoolean("add-look-at-player-ai", false); allowSpawnInMidair = getConfigDataBoolean("allow-spawn-in-midair", false); - cancelAttack = getConfigDataBoolean("cancel-attack", true); attackSpellName = getConfigString("attack-spell", ""); spellOnSpawnName = getConfigString("spell-on-spawn", ""); @@ -439,7 +438,11 @@ private CastResult spawnMob(Location source, SpellData data) { } SpellData finalData = data; - Entity entity = entityData.spawn(loc, data, mob -> prepMob(mob, finalData)); + Entity entity = entityData.spawn(loc, data, mob -> prepMob(mob, finalData), mob -> { + if (!removeMob) return; + mob.setPersistent(false); + Util.forEachPassenger(mob, e -> e.setPersistent(false)); + }); if (entity == null) return noTarget(data); UUID uuid = entity.getUniqueId(); @@ -483,8 +486,6 @@ private CastResult spawnMob(Location source, SpellData data) { } private void prepMob(Entity entity, SpellData data) { - if (removeMob) entity.setPersistent(false); - entity.setGravity(gravity.get(data)); entity.setInvulnerable(invulnerable.get(data)); @@ -545,10 +546,8 @@ private void prepMob(Entity entity, SpellData data) { if (removeAI.get(data)) { if (addLookAtPlayerAI.get(data) && livingEntity instanceof Mob mob) { - MobGoals mobGoals = Bukkit.getMobGoals(); - - mobGoals.removeAllGoals(mob); - mobGoals.addGoal(mob, 1, new LookAtEntityTypeGoal(mob, data)); + Bukkit.getMobGoals().removeAllGoals(mob); + Bukkit.getMobGoals().addGoal(mob, 1, new LookAtEntityTypeGoal(mob, data)); } else livingEntity.setAI(false); } diff --git a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java index 9e67e4452..042724fd3 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/EntityData.java +++ b/core/src/main/java/com/nisovin/magicspells/util/EntityData.java @@ -4,10 +4,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.ApiStatus; -import java.util.Map; -import java.util.List; -import java.util.ArrayList; -import java.util.Collection; +import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.BiConsumer; @@ -18,8 +15,13 @@ import com.google.common.collect.Multimap; import com.google.common.collect.MultimapBuilder; +import net.kyori.adventure.util.TriState; import net.kyori.adventure.text.Component; +import com.destroystokyo.paper.entity.ai.Goal; +import com.destroystokyo.paper.entity.ai.GoalKey; +import com.destroystokyo.paper.entity.ai.GoalType; + import org.bukkit.*; import org.bukkit.entity.*; import org.bukkit.util.Vector; @@ -34,14 +36,18 @@ import org.bukkit.inventory.EntityEquipment; import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeModifier; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.entity.minecart.CommandMinecart; import org.bukkit.configuration.ConfigurationSection; +import io.papermc.paper.entity.Frictional; import io.papermc.paper.registry.RegistryKey; import io.papermc.paper.entity.CollarColorable; import io.papermc.paper.registry.RegistryAccess; import io.papermc.paper.threadedregions.scheduler.ScheduledTask; import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.ai.CustomGoal; import com.nisovin.magicspells.util.config.ConfigData; import com.nisovin.magicspells.util.config.FunctionData; import com.nisovin.magicspells.util.magicitems.MagicItem; @@ -51,6 +57,8 @@ public class EntityData { + public static final NamespacedKey MS_PASSENGER = new NamespacedKey(MagicSpells.getInstance(), "entity_passenger"); + private final Multimap> options = MultimapBuilder.enumKeys(EntityType.class).arrayListValues().build(); private final List delayedEntityData = new ArrayList<>(); @@ -85,7 +93,7 @@ public class EntityData { private final ConfigData horseStyle; // Item - private final ConfigData dropItemMaterial; + private final ConfigData dropItem; // Llama private final ConfigData llamaColor; @@ -129,18 +137,47 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptBoolean(transformers, config, "silent", Entity.class, Entity::setSilent); addOptBoolean(transformers, config, "glowing", Entity.class, Entity::setGlowing); addOptBoolean(transformers, config, "gravity", Entity.class, Entity::setGravity); + addOptBoolean(transformers, config, "no-physics", Entity.class, Entity::setNoPhysics); + addOptBoolean(transformers, config, "persistent", Entity.class, Entity::setPersistent); + addOptBoolean(transformers, config, "visual-fire", Entity.class, Entity::setVisualFire); + addOptBoolean(transformers, config, "invulnerable", Entity.class, Entity::setInvulnerable); addOptBoolean(transformers, config, "visible-by-default", Entity.class, Entity::setVisibleByDefault); - addOptVector(transformers, config, "velocity", Entity.class, Entity::setVelocity); + addOptBoolean(transformers, config, "custom-name-visible", Entity.class, Entity::setCustomNameVisible); - if (config.isList("scoreboard-tags")) { - List tagStrings = config.getStringList("scoreboard-tags"); - if (!tagStrings.isEmpty()) { - List> tags = new ArrayList<>(); - for (String tagString : tagStrings) tags.add(ConfigDataUtil.getString(tagString)); + addOptInteger(transformers, config, "fire-ticks", Entity.class, Entity::setFireTicks); + addOptInteger(transformers, config, "freeze-ticks", Entity.class, Entity::setFreezeTicks); - transformers.put(Entity.class, (Entity entity, SpellData data) -> { - for (ConfigData tag : tags) entity.addScoreboardTag(tag.get(data)); - }); + addOptVector(transformers, config, "velocity", Entity.class, Entity::setVelocity); + + for (Object object : config.getList("scoreboard-tags", new ArrayList<>())) { + switch (object) { + case String string -> { + ConfigData tag = ConfigDataUtil.getString(string); + transformers.put(Entity.class, (Entity entity, SpellData data) -> entity.addScoreboardTag(tag.get(data))); + } + case Map map -> { + ConfigurationSection section = ConfigReaderUtil.mapToSection(map); + + ConfigData operation = ConfigDataUtil.getEnum(section, "operation", EntityTagOperation.class, EntityTagOperation.ADD); + ConfigData tagData = ConfigDataUtil.getString(section, "tag", null); + + transformers.put(Entity.class, (Entity entity, SpellData data) -> { + switch (operation.get(data)) { + case ADD -> { + String tag = tagData.get(data); + if (tag == null) break; + entity.addScoreboardTag(tag); + } + case REMOVE -> { + String tag = tagData.get(data); + if (tag == null) break; + entity.removeScoreboardTag(tag); + } + case CLEAR -> entity.getScoreboardTags().clear(); + } + }); + } + default -> {} } } @@ -153,10 +190,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptInteger(transformers, config, "age", Ageable.class, Ageable::setAge); // Attributable - List attributeModifierStrings = config.getList("attribute-modifiers"); - if (attributeModifierStrings != null && !attributeModifierStrings.isEmpty()) { - Multimap attributeModifiers = AttributeHandler.getAttributeModifiers(attributeModifierStrings, null); - + List attributeModifierStrings = config.getList("attribute-modifiers", new ArrayList<>()); + Multimap attributeModifiers = AttributeHandler.getAttributeModifiers(attributeModifierStrings, null); + if (!attributeModifiers.isEmpty()) { transformers.put(Attributable.class, (Attributable entity, SpellData data) -> { attributeModifiers.asMap().forEach((attribute, modifiers) -> { AttributeInstance attributeInstance = entity.getAttribute(attribute); @@ -170,8 +206,15 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // Damageable addOptDouble(transformers, config, "health", Damageable.class, Damageable::setHealth); + // Nameable + addOptComponent(transformers, config, "custom-name", Nameable.class, Nameable::customName); + // LivingEntity addOptBoolean(transformers, config, "ai", LivingEntity.class, LivingEntity::setAI); + addOptBoolean(transformers, config, "can-pickup-items", LivingEntity.class, LivingEntity::setCanPickupItems); + + addOptDouble(transformers, config, "max-health", LivingEntity.class, Util::setMaxHealth); + addOptEquipment(transformers, config, "equipment.main-hand", EquipmentSlot.HAND); addOptEquipment(transformers, config, "equipment.off-hand", EquipmentSlot.OFF_HAND); addOptEquipment(transformers, config, "equipment.helmet", EquipmentSlot.HEAD); @@ -180,17 +223,16 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptEquipment(transformers, config, "equipment.boots", EquipmentSlot.FEET); addOptEquipment(transformers, config, "equipment.body", Mob.class, EquipmentSlot.BODY); - List potionEffectData = config.getList("potion-effects"); - if (potionEffectData != null && !potionEffectData.isEmpty()) { - List> effects = Util.getPotionEffects(potionEffectData, null); - if (effects != null) { - transformers.put(LivingEntity.class, (LivingEntity entity, SpellData data) -> { - effects.forEach(effect -> entity.addPotionEffect(effect.get(data))); - }); - } + List> potionEffects = Util.getPotionEffects(config.getList("potion-effects", new ArrayList<>()), null); + if (potionEffects != null) { + transformers.put(LivingEntity.class, (LivingEntity entity, SpellData data) -> { + potionEffects.forEach(effect -> entity.addPotionEffect(effect.get(data))); + }); } // Mob + addOptBoolean(transformers, config, "aware", Mob.class, Mob::setAware); + addOptEquipmentDropChance(transformers, config, "equipment.main-hand-drop-chance", EquipmentSlot.HAND); addOptEquipmentDropChance(transformers, config, "equipment.off-hand-drop-chance", EquipmentSlot.OFF_HAND); addOptEquipmentDropChance(transformers, config, "equipment.helmet-drop-chance", EquipmentSlot.HEAD); @@ -201,6 +243,12 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // Tameable tamed = addBoolean(transformers, config, "tamed", false, Tameable.class, Tameable::setTamed, forceOptional); + if (config.getBoolean("tamed-owner")) { + transformers.put(Tameable.class, (Tameable tameable, SpellData data) -> { + if (!(data.recipient() instanceof AnimalTamer tamer)) return; + tameable.setOwner(tamer); + }); + } // AbstractHorse saddled = addBoolean(transformers, config, "saddled", false, AbstractHorse.class, (horse, saddled) -> { @@ -213,6 +261,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addBoolean(transformers, config, "visible", true, ArmorStand.class, ArmorStand::setVisible, forceOptional); addBoolean(transformers, config, "has-arms", true, ArmorStand.class, ArmorStand::setArms, forceOptional); addBoolean(transformers, config, "has-base-plate", true, ArmorStand.class, ArmorStand::setBasePlate, forceOptional); + addBoolean(transformers, config, "disable-slots", false, ArmorStand.class, (stand, disabled) -> { + if (disabled) stand.setDisabledSlots(EquipmentSlot.values()); + }, forceOptional); addEulerAngle(transformers, config, "head-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setHeadPose, forceOptional); addEulerAngle(transformers, config, "body-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setBodyPose, forceOptional); @@ -221,6 +272,33 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addEulerAngle(transformers, config, "left-leg-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setLeftLegPose, forceOptional); addEulerAngle(transformers, config, "right-leg-angle", EulerAngle.ZERO, ArmorStand.class, ArmorStand::setRightLegPose, forceOptional); + for (String slotName : config.getStringList("disable-slots")) { + ConfigData slotData = ConfigDataUtil.getEnum(slotName, EquipmentSlot.class, null); + + transformers.put(ArmorStand.class, (ArmorStand stand, SpellData data) -> { + EquipmentSlot slot = slotData.get(data); + if (slot == null) return; + + stand.addDisabledSlots(slot); + }); + } + + for (Object object : config.getList("equipment-locks", new ArrayList<>())) { + if (!(object instanceof Map map)) continue; + ConfigurationSection section = ConfigReaderUtil.mapToSection(map); + + ConfigData slotData = ConfigDataUtil.getEnum(section, "slot", EquipmentSlot.class, null); + ConfigData lockData = ConfigDataUtil.getEnum(section, "lock", ArmorStand.LockType.class, null); + + transformers.put(ArmorStand.class, (ArmorStand stand, SpellData data) -> { + EquipmentSlot slot = slotData.get(data); + ArmorStand.LockType lock = lockData.get(data); + if (slot == null || lock == null) return; + + stand.addEquipmentLock(slot, lock); + }); + } + // Axolotl fallback( key -> addOptEnum(transformers, config, key, Axolotl.class, Axolotl.Variant.class, Axolotl::setVariant), @@ -239,6 +317,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "collar-color", "color" ); + // CommandMinecart + addOptString(transformers, config, "command", CommandMinecart.class, CommandMinecart::setCommand); + // ChestedHorse chested = addBoolean(transformers, config, "chested", false, ChestedHorse.class, ChestedHorse::setCarryingChest, forceOptional); @@ -257,18 +338,37 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "falling-block", "material" ); + addOptBoolean(transformers, config, "cancel-drop", FallingBlock.class, FallingBlock::setCancelDrop); + addOptBoolean(transformers, config, "hurt-entities", FallingBlock.class, FallingBlock::setHurtEntities); + + addOptFloat(transformers, config, "damage-per-block", FallingBlock.class, FallingBlock::setDamagePerBlock); + + addOptInteger(transformers, config, "max-damage", FallingBlock.class, FallingBlock::setMaxDamage); + // Fox fallback( key -> addOptEnum(transformers, config, key, Fox.class, Fox.Type.class, Fox::setFoxType), "fox-type", "type" ); + // Frictional + addOptEnum(transformers, config, "friction-state", Frictional.class, TriState.class, Frictional::setFrictionState); + // Frog fallback( key -> addOptRegistryEntry(transformers, config, key, Frog.class, Registry.FROG_VARIANT, Frog::setVariant), "frog-variant", "type" ); + // Goat + addOptBoolean(transformers, config, "left-horn", Goat.class, Goat::setLeftHorn); + addOptBoolean(transformers, config, "right-horn", Goat.class, Goat::setRightHorn); + addOptBoolean(transformers, config, "screaming", Goat.class, Goat::setScreaming); + + // Hoglin + addOptBoolean(transformers, config, "immune-to-zombification", Hoglin.class, Hoglin::setImmuneToZombification); + addOptBoolean(transformers, config, "able-to-be-hunted", Hoglin.class, Hoglin::setIsAbleToBeHunted); + // Horse horseColor = fallback( key -> addOptEnum(transformers, config, key, Horse.class, Horse.Color.class, Horse::setColor), @@ -280,9 +380,14 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { ); // Item - dropItemMaterial = addOptMaterial(transformers, config, "material", Item.class, (item, material) -> item.setItemStack(new ItemStack(material))); - addOptBoolean(transformers, config, "will-age", Item.class, Item::setWillAge); + dropItem = fallback( + key -> addOptItemStack(transformers, config, key, Item.class, Item::setItemStack), + "material", "item" + ); + addOptInteger(transformers, config, "pickup-delay", Item.class, Item::setPickupDelay); + + addOptBoolean(transformers, config, "will-age", Item.class, Item::setWillAge); addOptBoolean(transformers, config, "can-mob-pickup", Item.class, Item::setCanMobPickup); addOptBoolean(transformers, config, "can-player-pickup", Item.class, Item::setCanPlayerPickup); @@ -301,6 +406,15 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "llama-decor", "material" ); + // Minecart + addOptDouble(transformers, config, "max-speed", Minecart.class, Minecart::setMaxSpeed); + + addOptBoolean(transformers, config, "slow-when-empty", Minecart.class, Minecart::setSlowWhenEmpty); + + addOptInteger(transformers, config, "display-block-offset", Minecart.class, Minecart::setDisplayBlockOffset); + + addOptBlockData(transformers, config, "display-block", Minecart.class, Minecart::setDisplayBlockData); + // Mushroom Cow fallback( key -> addOptEnum(transformers, config, key, MushroomCow.class, MushroomCow.Variant.class, MushroomCow::setVariant), @@ -321,6 +435,13 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addInteger(transformers, config, "size", 0, Phantom.class, Phantom::setSize, forceOptional); addOptBoolean(transformers, config, "should-burn-in-day", Phantom.class, Phantom::setShouldBurnInDay); + // Piglin + addOptBoolean(transformers, config, "able-to-hunt", Piglin.class, Piglin::setIsAbleToHunt); + addOptInteger(transformers, config, "dancing", Piglin.class, Piglin::setDancing); + + // Piglin Abstract + addOptBoolean(transformers, config, "immune-to-zombification", PiglinAbstract.class, PiglinAbstract::setImmuneToZombification); + // Puffer Fish size = addInteger(transformers, config, "size", 0, PufferFish.class, PufferFish::setPuffState, forceOptional); @@ -330,6 +451,11 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "rabbit-type", "type" ); + // Raider + addOptBoolean(transformers, config, "patrol-leader", Raider.class, Raider::setPatrolLeader); + addOptBoolean(transformers, config, "can-join-raid", Raider.class, Raider::setCanJoinRaid); + addOptBoolean(transformers, config, "celebrating", Raider.class, Raider::setCelebrating); + // Sheep sheared = addBoolean(transformers, config, "sheared", false, Sheep.class, Sheep::setSheared, forceOptional); color = fallback( @@ -366,6 +492,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { "tropical-fish.pattern", "type" ); + // Salmon + addOptEnum(transformers, config, "salmon-variant", Salmon.class, Salmon.Variant.class, Salmon::setVariant); + // Villager profession = fallback( key -> addOptRegistryEntry(transformers, config, key, Villager.class, Registry.VILLAGER_PROFESSION, Villager::setProfession), @@ -373,6 +502,9 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { ); addOptRegistryEntry(transformers, config, "villager-type", Villager.class, Registry.VILLAGER_TYPE, Villager::setVillagerType); + // Vindicator + addOptBoolean(transformers, config, "johnny", Vindicator.class, Vindicator::setJohnny); + // Wolf addBoolean(transformers, config, "angry", false, Wolf.class, Wolf::setAngry, forceOptional); addOptRegistryEntry(transformers, config, "wolf-variant", Wolf.class, RegistryKey.WOLF_VARIANT, Wolf::setVariant); @@ -380,6 +512,10 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { // Zombie addOptBoolean(transformers, config, "should-burn-in-day", Zombie.class, Zombie::setShouldBurnInDay); + // Zombie Villager + addOptRegistryEntry(transformers, config, "villager-profession", ZombieVillager.class, Registry.VILLAGER_PROFESSION, ZombieVillager::setVillagerProfession); + addOptRegistryEntry(transformers, config, "villager-type", ZombieVillager.class, Registry.VILLAGER_TYPE, ZombieVillager::setVillagerType); + // Display ConfigData leftRotation = getQuaternion(config, "transformation.left-rotation"); ConfigData rightRotation = getQuaternion(config, "transformation.right-rotation"); @@ -470,15 +606,100 @@ public EntityData(ConfigurationSection config, boolean forceOptional) { addOptBoolean(transformers, config, "default-background", TextDisplay.class, TextDisplay::setDefaultBackground); addOptEnum(transformers, config, "alignment", TextDisplay.class, TextDisplay.TextAlignment.class, TextDisplay::setAlignment); + // Passengers + for (Object object : config.getList("passengers", new ArrayList<>())) { + if (!(object instanceof Map map)) continue; + EntityData passengerData = new EntityData(ConfigReaderUtil.mapToSection(map)); + + transformers.put(Entity.class, (Entity entity, SpellData data) -> { + passengerData.spawn(entity.getLocation(), data, passenger -> { + entity.addPassenger(passenger); + passenger.getPersistentDataContainer().set(MS_PASSENGER, PersistentDataType.BOOLEAN, true); + }); + }); + } + + // Mob Goals + ConfigurationSection mobGoals = config.getConfigurationSection("mob-goals"); + if (mobGoals != null) { + if (mobGoals.getBoolean("remove-all")) + transformers.put(Mob.class, (Mob mob, SpellData data) -> Bukkit.getMobGoals().removeAllGoals(mob)); + + for (String string : mobGoals.getStringList("remove-types")) { + ConfigData typeData = ConfigDataUtil.getEnum(string, GoalType.class, null); + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + GoalType type = typeData.get(data); + if (type == null) return; + + Bukkit.getMobGoals().removeAllGoals(mob, type); + }); + } + + for (String string : mobGoals.getStringList("remove")) { + ConfigData keyData = ConfigDataUtil.getNamespacedKey(string, null); + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + NamespacedKey key = keyData.get(data); + if (key == null) return; + + Bukkit.getMobGoals().removeGoal(mob, GoalKey.of(Mob.class, key)); + }); + } + + for (String string : mobGoals.getStringList("remove-vanilla")) { + ConfigData stringData = ConfigDataUtil.getString(string); + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + String value = stringData.get(data); + if (value == null) return; + + GoalKey key = MagicSpells.getCustomGoalsManager().getVanillaGoal(value); + if (key == null) return; + + // We have to loop through because casting to parameter types is tricky. + // It loops through on each MobGoals#removeGoal call anyway. + for (Goal<@NotNull Mob> goal : Bukkit.getMobGoals().getAllGoals(mob)) { + if (!goal.getKey().equals(key)) continue; + Bukkit.getMobGoals().removeGoal(mob, goal); + } + }); + } + + for (Object object : mobGoals.getList("add", new ArrayList<>())) { + if (!(object instanceof Map map)) continue; + ConfigurationSection section = ConfigReaderUtil.mapToSection(map); + + ConfigData priorityData = ConfigDataUtil.getInteger(section, "priority", 0); + ConfigData goalNameData = ConfigDataUtil.getString(section, "goal", ""); + + ConfigurationSection goalSection = section.getConfigurationSection("data"); + if (goalSection == null) continue; + + transformers.put(Mob.class, (Mob mob, SpellData data) -> { + int priority = priorityData.get(data); + String goalName = goalNameData.get(data); + + CustomGoal goal = MagicSpells.getCustomGoalsManager().getGoal(goalName, mob, data); + if (goal == null) return; + + boolean success = goal.initialize(goalSection); + if (success) Bukkit.getMobGoals().addGoal(mob, priority, goal); + }); + } + } + + // Apply transformers for (EntityType entityType : EntityType.values()) { Class entityClass = entityType.getEntityClass(); if (entityClass == null) continue; - for (Class transformerType : transformers.keys()) + for (Class transformerType : transformers.keySet()) if (transformerType.isAssignableFrom(entityClass)) options.putAll(entityType, transformers.get(transformerType)); } + // Delayed Entity Data List delayedDataEntries = config.getList("delayed-entity-data"); if (delayedDataEntries == null || delayedDataEntries.isEmpty()) return; @@ -505,12 +726,42 @@ public Entity spawn(@NotNull Location location) { } @Nullable - public Entity spawn(@NotNull Location location, @Nullable Consumer consumer) { - return spawn(location, SpellData.NULL, consumer); + public Entity spawn(@NotNull Location location, @Nullable Consumer postConsumer) { + return spawn(location, SpellData.NULL, postConsumer); } @Nullable - public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer consumer) { + public Entity spawn(@NotNull Location location, @Nullable Consumer preConsumer, @Nullable Consumer postConsumer) { + return spawn(location, SpellData.NULL, preConsumer, postConsumer); + } + + @Nullable + public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer postConsumer) { + return spawn(location, data, (Consumer) null, postConsumer); + } + + @Nullable + public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullable Consumer preConsumer, @Nullable Consumer postConsumer) { + EntityType type = this.entityType.get(data); + if (type == null || !type.isSpawnable()) return null; + + Class entityClass = type.getEntityClass(); + if (entityClass == null) return null; + + return spawn(location, data, entityClass, entity -> { + if (preConsumer != null) preConsumer.accept(entity); + }, entity -> { + if (postConsumer != null) postConsumer.accept(entity); + }); + } + + @NotNull + public T spawn(@NotNull Location location, @NotNull SpellData data, @NotNull Class entityClass, @Nullable Consumer postConsumer) { + return spawn(location, data, entityClass, null, postConsumer); + } + + @NotNull + public T spawn(@NotNull Location location, @NotNull SpellData data, @NotNull Class entityClass, @Nullable Consumer preConsumer, @Nullable Consumer postConsumer) { Location spawnLocation = location.clone(); Vector relativeOffset = this.relativeOffset.get(data); @@ -520,17 +771,12 @@ public Entity spawn(@NotNull Location location, @NotNull SpellData data, @Nullab spawnLocation.setYaw(yaw.get(data).apply(spawnLocation.getYaw())); spawnLocation.setPitch(pitch.get(data).apply(spawnLocation.getPitch())); - EntityType type = this.entityType.get(data); - if (type == null || (!type.isSpawnable() && type != EntityType.FALLING_BLOCK && type != EntityType.ITEM)) - return null; - - Class entityClass = type.getEntityClass(); - if (entityClass == null) return null; - return spawnLocation.getWorld().spawn(spawnLocation, entityClass, entity -> { + if (preConsumer != null) preConsumer.accept(entity); + apply(entity, data); - if (consumer != null) consumer.accept(entity); + if (postConsumer != null) postConsumer.accept(entity); }); } @@ -661,6 +907,11 @@ private void addOptDouble(Multimap, Transformer> transformers, C transformers.put(type, new TransformerImpl<>(supplier, setter, true)); } + private void addOptString(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { + ConfigData supplier = ConfigDataUtil.getString(config, name, null); + transformers.put(type, new TransformerImpl<>(supplier, setter, true)); + } + private > ConfigData addOptEnum(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, Class enumType, BiConsumer setter) { ConfigData supplier = ConfigDataUtil.getEnum(config, name, enumType, null); transformers.put(type, new TransformerImpl<>(supplier, setter, true)); @@ -697,17 +948,20 @@ private ConfigData addOptBlockData(Multimap, Transformer return supplier; } - private void addOptItemStack(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { + private ConfigData addOptItemStack(Multimap, Transformer> transformers, ConfigurationSection config, String name, Class type, BiConsumer setter) { ConfigData supplier = ConfigDataUtil.getString(config, name, null); if (supplier.isConstant()) { ItemStack item = getItemStack(supplier.get()); - if (item == null) return; + if (item == null) return data -> null; transformers.put(type, new TransformerImpl<>(data -> item, setter, true)); - return; + return data -> item; } - transformers.put(type, new TransformerImpl<>(data -> getItemStack(supplier.get(data)), setter, true)); + ConfigData itemSupplier = data -> getItemStack(supplier.get(data)); + transformers.put(type, new TransformerImpl<>(itemSupplier, setter, true)); + + return itemSupplier; } private ItemStack getItemStack(String string) { @@ -1003,13 +1257,17 @@ public ConfigData getRelativeOffset() { return relativeOffset; } + /** + * @deprecated Use {@link EntityData#spawn(Location, SpellData, Class, Consumer)} + */ + @Deprecated(forRemoval = true) public void setEntityType(ConfigData entityType) { this.entityType = entityType; } @ApiStatus.Internal - public ConfigData getDroppedItemStack() { - return dropItemMaterial; + public ConfigData getDroppedItemStack() { + return dropItem; } @ApiStatus.Internal @@ -1097,6 +1355,12 @@ public ConfigData getProfession() { return profession; } + private enum EntityTagOperation { + ADD, + REMOVE, + CLEAR, + } + private record DelayedEntityData(EntityData data, ConfigData delay, ConfigData interval, ConfigData iterations) { } diff --git a/core/src/main/java/com/nisovin/magicspells/util/Util.java b/core/src/main/java/com/nisovin/magicspells/util/Util.java index 758fc3238..affe054b8 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/Util.java +++ b/core/src/main/java/com/nisovin/magicspells/util/Util.java @@ -582,6 +582,13 @@ public static void forEachPlayerOnline(Consumer consumer) { Bukkit.getOnlinePlayers().forEach(consumer); } + public static void forEachPassenger(Entity entity, Consumer consumer) { + for (Entity passenger : entity.getPassengers()) { + forEachPassenger(passenger, consumer); + consumer.accept(passenger); + } + } + public static > C getMaterialList(List strings, Supplier supplier) { C ret = supplier.get(); strings.forEach(string -> ret.add(getMaterial(string))); diff --git a/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java b/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java index c54060e75..ceb2f7abc 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java +++ b/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java @@ -35,7 +35,7 @@ public PathToGoal(Mob mob, SpellData data) { @Override public boolean initialize(@Nullable ConfigurationSection config) { if (config == null) return false; - speed = ConfigDataUtil.getDouble(config, "speed", 0.2); + speed = ConfigDataUtil.getDouble(config, "speed", 1); position = ConfigDataUtil.getVector(config, "position", new Vector()); distanceAllowed = ConfigDataUtil.getDouble(config, "distance-allowed", 1); return true; diff --git a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java index f976b3968..bfbb57bfe 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java +++ b/core/src/main/java/com/nisovin/magicspells/util/config/ConfigDataUtil.java @@ -509,7 +509,13 @@ public static > ConfigData getEnum(@NotNull ConfigurationSe @NotNull String path, @NotNull Class type, @Nullable T def) { - String value = config.getString(path); + return getEnum(config.getString(path), type, def); + } + + @NotNull + public static > ConfigData getEnum(@Nullable String value, + @NotNull Class type, + @Nullable T def) { if (value == null) return data -> def; try { @@ -781,7 +787,10 @@ public static ConfigData getNamedTextColor(@NotNull Configuratio public static ConfigData getNamespacedKey(@NotNull ConfigurationSection config, @NotNull String path, @Nullable NamespacedKey def) { String value = config.getString(path); if (value == null) return data -> def; + return getNamespacedKey(value, def); + } + public static ConfigData getNamespacedKey(@NotNull String value, @Nullable NamespacedKey def) { NamespacedKey val = NamespacedKey.fromString(value.toLowerCase()); if (val != null) return data -> val;