Skip to content

Commit

Permalink
Move Diva Charm effect outside of damage logic and support angry neut…
Browse files Browse the repository at this point in the history
…ral mobs

Fixes VazkiiMods#4561 by temporarily storing mob IDs on the charm and applying the brainwashing during the next worn tick.
Fabric damage hook for the charm was also moved to LivingEntity::actuallyHurt for better parity with Forge.
  • Loading branch information
TheRealWormbo committed May 26, 2024
1 parent dfc8c94 commit c412b86
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,56 +45,51 @@ public void tickFlower() {
return;
}

@SuppressWarnings("unchecked")
List<Enemy> 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<Mob> 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<Enemy> mobs) {
public static boolean brainwashEntity(Mob entity, List<Mob> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Mob> 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<Mob> 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<Enemy> mobs = (List<Enemy>) (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);
}
}
}
Expand Down

0 comments on commit c412b86

Please sign in to comment.