diff --git a/Fabric/src/main/java/vazkii/botania/fabric/mixin/LivingEntityFabricMixin.java b/Fabric/src/main/java/vazkii/botania/fabric/mixin/LivingEntityFabricMixin.java index 6371fc943c..77fc8277e9 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/mixin/LivingEntityFabricMixin.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/mixin/LivingEntityFabricMixin.java @@ -13,6 +13,7 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; @@ -26,6 +27,7 @@ import vazkii.botania.common.block.flower.functional.LooniumBlockEntity; import vazkii.botania.common.brew.effect.SoulCrossMobEffect; import vazkii.botania.common.item.AssemblyHaloItem; +import vazkii.botania.common.item.equipment.bauble.CharmOfTheDivaItem; import vazkii.botania.common.item.equipment.bauble.SojournersSashItem; import vazkii.botania.common.item.equipment.tool.elementium.ElementiumAxeItem; import vazkii.botania.common.item.rod.ShadedMesaRodItem; @@ -86,4 +88,11 @@ private void onSwing(InteractionHand hand, boolean bl, CallbackInfo ci) { private void onJump(CallbackInfo ci) { SojournersSashItem.onPlayerJump((LivingEntity) (Object) this); } + + @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getDamageAfterArmorAbsorb(Lnet/minecraft/world/damagesource/DamageSource;F)F", ordinal = 0), method = "actuallyHurt") + private void onActuallyHurt(DamageSource damageSource, float damageAmount, CallbackInfo ci) { + if (damageSource.getDirectEntity() instanceof Player player) { + CharmOfTheDivaItem.onEntityDamaged(player, (LivingEntity) (Object) this); + } + } } diff --git a/Fabric/src/main/java/vazkii/botania/fabric/mixin/PlayerFabricMixin.java b/Fabric/src/main/java/vazkii/botania/fabric/mixin/PlayerFabricMixin.java index 771979ee13..bd1a61450e 100644 --- a/Fabric/src/main/java/vazkii/botania/fabric/mixin/PlayerFabricMixin.java +++ b/Fabric/src/main/java/vazkii/botania/fabric/mixin/PlayerFabricMixin.java @@ -113,11 +113,6 @@ private float cushionFall(float originalDist) { return SojournersSashItem.onPlayerFall((Player) (Object) this, originalDist); } - @Inject(at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/player/Player;setLastHurtMob(Lnet/minecraft/world/entity/Entity;)V"), method = "attack") - private void onAttack(Entity target, CallbackInfo ci) { - CharmOfTheDivaItem.onEntityDamaged((Player) (Object) this, target); - } - // Multiply the damage on crit. Targets the first float LOAD after the sprint check for the crit. // Stores the entity for further handling in the common Player mixin. @ModifyVariable( diff --git a/Xplat/src/main/java/vazkii/botania/common/block/flower/functional/HeiseiDreamBlockEntity.java b/Xplat/src/main/java/vazkii/botania/common/block/flower/functional/HeiseiDreamBlockEntity.java index 77956023e1..8c5083b08c 100644 --- a/Xplat/src/main/java/vazkii/botania/common/block/flower/functional/HeiseiDreamBlockEntity.java +++ b/Xplat/src/main/java/vazkii/botania/common/block/flower/functional/HeiseiDreamBlockEntity.java @@ -11,7 +11,6 @@ import com.google.common.base.Predicates; import net.minecraft.core.BlockPos; -import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ai.goal.GoalSelector; @@ -46,56 +45,51 @@ public void tickFlower() { return; } - @SuppressWarnings("unchecked") - List mobs = (List) getLevel().getEntitiesOfClass(Entity.class, new AABB(getEffectivePos().offset(-RANGE, -RANGE, -RANGE), getEffectivePos().offset(RANGE + 1, RANGE + 1, RANGE + 1)), Predicates.instanceOf(Enemy.class)); + List mobs = getLevel().getEntitiesOfClass(Mob.class, new AABB(getEffectivePos().offset(-RANGE, -RANGE, -RANGE), getEffectivePos().offset(RANGE + 1, RANGE + 1, RANGE + 1)), Predicates.instanceOf(Enemy.class)); if (mobs.size() > 1 && getMana() >= COST) { - for (Enemy mob : mobs) { - if (mob instanceof Mob entity) { - if (brainwashEntity(entity, mobs)) { - addMana(-COST); - sync(); - break; - } + for (Mob mob : mobs) { + if (brainwashEntity(mob, mobs)) { + addMana(-COST); + sync(); + break; } } } } - public static boolean brainwashEntity(Mob entity, List mobs) { + public static boolean brainwashEntity(Mob entity, List mobs) { LivingEntity target = entity.getTarget(); boolean did = false; if (!(target instanceof Enemy)) { - Enemy newTarget; + Mob newTarget; do { newTarget = mobs.get(entity.level().random.nextInt(mobs.size())); } while (newTarget == entity); - if (newTarget instanceof Mob mob) { - entity.setTarget(null); - - // Move any HurtByTargetGoal to highest priority - GoalSelector targetSelector = ((MobAccessor) entity).getTargetSelector(); - for (WrappedGoal entry : targetSelector.getAvailableGoals()) { - if (entry.getGoal() instanceof HurtByTargetGoal goal) { - // Remove all ignorals. We can't actually resize or overwrite - // the array, but we can fill it with classes that will never pass - // the game logic's checks. - var ignoreClasses = ((HurtByTargetGoalAccessor) goal).getIgnoreDamageClasses(); - Arrays.fill(ignoreClasses, Void.TYPE); - - // Concurrent modification OK since we break out of the loop - targetSelector.removeGoal(goal); - targetSelector.addGoal(-1, goal); - break; - } + entity.setTarget(null); + + // Move any HurtByTargetGoal to highest priority + GoalSelector targetSelector = ((MobAccessor) entity).getTargetSelector(); + for (WrappedGoal entry : targetSelector.getAvailableGoals()) { + if (entry.getGoal() instanceof HurtByTargetGoal goal) { + // Remove all ignorals. We can't actually resize or overwrite + // the array, but we can fill it with classes that will never pass + // the game logic's checks. + var ignoreClasses = ((HurtByTargetGoalAccessor) goal).getIgnoreDamageClasses(); + Arrays.fill(ignoreClasses, Void.TYPE); + + // Concurrent modification OK since we break out of the loop + targetSelector.removeGoal(goal); + targetSelector.addGoal(-1, goal); + break; } - - // Now set last hurt by, which HurtByTargetGoal will pick up - entity.setLastHurtByMob(mob); - did = true; } + + // Now set last hurt by, which HurtByTargetGoal will pick up + entity.setLastHurtByMob(newTarget); + did = true; } return did; diff --git a/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/CharmOfTheDivaItem.java b/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/CharmOfTheDivaItem.java index 56897c6b9f..3e7415e21f 100644 --- a/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/CharmOfTheDivaItem.java +++ b/Xplat/src/main/java/vazkii/botania/common/item/equipment/bauble/CharmOfTheDivaItem.java @@ -8,17 +8,15 @@ */ package vazkii.botania.common.item.equipment.bauble; -import com.google.common.base.Predicates; import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.Minecraft; import net.minecraft.client.model.HumanoidModel; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.nbt.Tag; import net.minecraft.sounds.SoundSource; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.*; import net.minecraft.world.entity.monster.Creeper; import net.minecraft.world.entity.monster.Enemy; import net.minecraft.world.entity.player.Player; @@ -40,43 +38,101 @@ import vazkii.botania.network.clientbound.BotaniaEffectPacket; import vazkii.botania.xplat.XplatAbstractions; +import java.util.Arrays; import java.util.List; +import java.util.function.Predicate; public class CharmOfTheDivaItem extends BaubleItem { + public static final int MANA_COST = 250; + public static final int CHARM_RANGE = 20; + private static final String TAG_MOBS_TO_CHARM = "mobsToCharm"; public CharmOfTheDivaItem(Properties props) { super(props); Proxy.INSTANCE.runOnClient(() -> () -> AccessoryRenderRegistry.register(this, new Renderer())); } + @Override + public void onWornTick(ItemStack stack, LivingEntity entity) { + if (entity.level().isClientSide()) { + return; + } + var tag = stack.getTag(); + if (tag != null && tag.contains(TAG_MOBS_TO_CHARM, Tag.TAG_INT_ARRAY)) { + charmMobs(stack, (Player) entity, tag.getIntArray(TAG_MOBS_TO_CHARM)); + stack.removeTagKey(TAG_MOBS_TO_CHARM); + } + } + + @Override + public void onEquipped(ItemStack stack, LivingEntity entity) { + if (!entity.level().isClientSide()) { + stack.removeTagKey(TAG_MOBS_TO_CHARM); + } + } + + @Override + public void onUnequipped(ItemStack stack, LivingEntity entity) { + if (!entity.level().isClientSide()) { + stack.removeTagKey(TAG_MOBS_TO_CHARM); + } + } + + private static Predicate getCharmTargetPredicate(Player player, Mob mobToCharm) { + return mob -> mob != mobToCharm && mob.isAlive() && mob.canBeSeenAsEnemy() && !mob.isPassengerOfSameVehicle(mobToCharm) + && (!(mob instanceof TamableAnimal tamable) || !tamable.isOwnedBy(player)) + && (mob instanceof Enemy || mob instanceof NeutralMob neutralMob && (neutralMob.isAngryAt(player) + || mob.getTarget() instanceof TamableAnimal targetTamable && targetTamable.isOwnedBy(player))); + } + + private static void charmMobs(ItemStack amulet, Player player, int[] mobsToCharmIds) { + for (int mobId : mobsToCharmIds) { + if (!ManaItemHandler.instance().requestManaExact(amulet, player, MANA_COST, false)) { + return; + } + var mobEntity = player.level().getEntity(mobId); + if (mobEntity instanceof Mob target && target.isAlive() + && (mobEntity instanceof Enemy || mobEntity instanceof NeutralMob) + // don't encourage being a bad pet owner + && (!(mobEntity instanceof TamableAnimal tamable) || !tamable.isOwnedBy(player)) + // check that target is still nearby, since it was marked some time earlier + && player.position().closerThan(target.position(), CHARM_RANGE)) { + List potentialTargets = player.level().getEntitiesOfClass(Mob.class, + AABB.ofSize(target.position(), 2 * CHARM_RANGE, 2 * CHARM_RANGE, 2 * CHARM_RANGE), + getCharmTargetPredicate(player, target)); + if (!potentialTargets.isEmpty() && HeiseiDreamBlockEntity.brainwashEntity(target, potentialTargets)) { + target.heal(target.getMaxHealth()); + ((EntityAccessor) target).callUnsetRemoved(); + if (target instanceof Creeper) { + ((CreeperAccessor) target).setCurrentFuseTime(2); + } + + ManaItemHandler.instance().requestManaExact(amulet, player, MANA_COST, true); + player.level().playSound(null, player.getX(), player.getY(), player.getZ(), BotaniaSounds.divaCharm, SoundSource.PLAYERS, 1F, 1F); + XplatAbstractions.INSTANCE.sendToTracking(target, new BotaniaEffectPacket(EffectType.DIVA_EFFECT, target.getX(), target.getY(), target.getZ(), target.getId())); + } + } + } + } + public static void onEntityDamaged(Player player, Entity entity) { if (entity instanceof Mob target && !entity.level().isClientSide - && entity.canChangeDimensions() && Math.random() < 0.6F) { ItemStack amulet = EquipmentHandler.findOrEmpty(BotaniaItems.divaCharm, player); if (!amulet.isEmpty()) { - final int cost = 250; - if (ManaItemHandler.instance().requestManaExact(amulet, player, cost, false)) { - final int range = 20; - - @SuppressWarnings("unchecked") - List mobs = (List) (List) player.level().getEntitiesOfClass(Entity.class, new AABB(target.getX() - range, target.getY() - range, target.getZ() - range, target.getX() + range, target.getY() + range, target.getZ() + range), Predicates.instanceOf(Enemy.class)); - if (mobs.size() > 1) { - if (HeiseiDreamBlockEntity.brainwashEntity(target, mobs)) { - target.heal(target.getMaxHealth()); - ((EntityAccessor) target).callUnsetRemoved(); - if (target instanceof Creeper) { - ((CreeperAccessor) target).setCurrentFuseTime(2); - } - - ManaItemHandler.instance().requestManaExact(amulet, player, cost, true); - player.level().playSound(null, player.getX(), player.getY(), player.getZ(), BotaniaSounds.divaCharm, SoundSource.PLAYERS, 1F, 1F); - XplatAbstractions.INSTANCE.sendToTracking(target, new BotaniaEffectPacket(EffectType.DIVA_EFFECT, target.getX(), target.getY(), target.getZ(), target.getId())); - } - } + // only mark potential target, then charm later, outside the damage logic + var tag = amulet.getOrCreateTag(); + int[] entityIds; + if (tag.contains(TAG_MOBS_TO_CHARM, Tag.TAG_INT_ARRAY)) { + int[] oldIds = tag.getIntArray(TAG_MOBS_TO_CHARM); + entityIds = Arrays.copyOf(oldIds, oldIds.length + 1); + entityIds[entityIds.length - 1] = entity.getId(); + } else { + entityIds = new int[] { entity.getId() }; } + tag.putIntArray(TAG_MOBS_TO_CHARM, entityIds); } } }