diff --git a/changelog.md b/changelog.md index 2e933ac..6bd3e33 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,11 @@ All notable changes since version 1.0.0 are documented in this file. ## Unreleased +### Added +- Added separate configuration for mob execution time, executor movement, and knocked-player movement. +- Added configurable NPC help-up behavior for owned pets, friendly villagers, golems, and allowlisted modded NPCs. +- NPC helpers only begin a rescue when they have no hostile target and no living threats remain nearby. + ### Fixed - Improved multiplayer target selection for execute, carry, and help-up interactions so the nearest valid knocked player is selected instead of an arbitrary player. - Added same-dimension and range validation for ongoing player execution and help-up interactions. diff --git a/src/main/java/net/fretux/knockedback/HelpUpHandler.java b/src/main/java/net/fretux/knockedback/HelpUpHandler.java index 5a8502e..20f26d0 100644 --- a/src/main/java/net/fretux/knockedback/HelpUpHandler.java +++ b/src/main/java/net/fretux/knockedback/HelpUpHandler.java @@ -53,6 +53,7 @@ public class HelpUpHandler { return; } if (helpUpAttempts.containsKey(knockedId)) return; + if (NpcHelpUpHandler.isBeingHelped(knockedId)) return; helpUpAttempts.put(knockedId, new HelpUpAttempt(helper.getUUID())); helper.sendSystemMessage(Component.translatable("message.knockedback.help_up_started", target.getDisplayName())); target.sendSystemMessage(Component.translatable("message.knockedback.being_helped", helper.getDisplayName())); diff --git a/src/main/java/net/fretux/knockedback/KnockedManager.java b/src/main/java/net/fretux/knockedback/KnockedManager.java index c6c5005..0543201 100644 --- a/src/main/java/net/fretux/knockedback/KnockedManager.java +++ b/src/main/java/net/fretux/knockedback/KnockedManager.java @@ -56,7 +56,7 @@ public class KnockedManager { applyKnockedEffects(sp); boolean grounded = sp.onGround() || sp.isInWater(); sp.setNoGravity(false); - if (!grounded) { + if (!grounded && !Config.COMMON.knockedMovementEnabled.get()) { sp.setDeltaMovement(0, sp.getDeltaMovement().y() - 0.08, 0); } } @@ -75,6 +75,7 @@ public class KnockedManager { MobKillHandler.clearKillAttempt(entity.getUUID()); PlayerExecutionHandler.cancelExecution(entity.getUUID()); HelpUpHandler.cancelHelpUp(entity.getUUID()); + NpcHelpUpHandler.cancelHelpUp(entity.getUUID()); entity.setPose(Pose.STANDING); if (entity instanceof ServerPlayer sp) { NetworkHandler.CHANNEL.send( @@ -125,7 +126,8 @@ public class KnockedManager { } if (MobKillHandler.isBeingMobExecuted(playerId) || PlayerExecutionHandler.isBeingPlayerExecuted(playerId) - || HelpUpHandler.isBeingHelped(playerId)) { + || HelpUpHandler.isBeingHelped(playerId) + || NpcHelpUpHandler.isBeingHelped(playerId)) { ServerPlayer p = NetworkHandlerHelper.getPlayerByUuid(server, playerId); if (p != null) { applyKnockedEffects(p); @@ -216,6 +218,7 @@ public class KnockedManager { PlayerExecutionHandler.cancelExecution(playerId); PlayerExecutionHandler.cancelExecutionByExecutor(playerId); HelpUpHandler.cancelHelpUp(playerId); + NpcHelpUpHandler.cancelHelpUp(playerId); HelpUpHandler.cancelHelpUpByHelper(playerId); CarryManager.stopCarry(playerId); CarryManager.stopCarrying(playerId); diff --git a/src/main/java/net/fretux/knockedback/MobKillHandler.java b/src/main/java/net/fretux/knockedback/MobKillHandler.java index 2296ba9..52a00a4 100644 --- a/src/main/java/net/fretux/knockedback/MobKillHandler.java +++ b/src/main/java/net/fretux/knockedback/MobKillHandler.java @@ -22,7 +22,7 @@ import static net.fretux.knockedback.KnockedManager.setGripped; public class MobKillHandler { public static int getExecutionTime() { - return Config.COMMON.executionTime.get(); + return Config.COMMON.mobExecutionTime.get(); } private static final Map killAttempts = new HashMap<>(); @@ -110,7 +110,8 @@ public class MobKillHandler { continue; } if (PlayerExecutionHandler.isBeingPlayerExecuted(knockedId) - || HelpUpHandler.isBeingHelped(knockedId)) { + || HelpUpHandler.isBeingHelped(knockedId) + || NpcHelpUpHandler.isBeingHelped(knockedId)) { setGripped(knocked, false); continue; } @@ -144,7 +145,9 @@ public class MobKillHandler { executeKnockedPlayer(knocked, mob); continue; } else { - mob.getNavigation().stop(); + if (!Config.COMMON.executorMovementEnabled.get()) { + mob.getNavigation().stop(); + } mob.setTarget(null); } } diff --git a/src/main/java/net/fretux/knockedback/NpcHelpUpHandler.java b/src/main/java/net/fretux/knockedback/NpcHelpUpHandler.java new file mode 100644 index 0000000..9a04902 --- /dev/null +++ b/src/main/java/net/fretux/knockedback/NpcHelpUpHandler.java @@ -0,0 +1,138 @@ +package net.fretux.knockedback; + +import net.fretux.knockedback.config.Config; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.TamableAnimal; +import net.minecraft.world.entity.animal.IronGolem; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.phys.AABB; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.registries.ForgeRegistries; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.UUID; + +@Mod.EventBusSubscriber(modid = KnockedBack.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE) +public final class NpcHelpUpHandler { + private static final double ASSIST_DISTANCE = 2.25D; + private static final Map ATTEMPTS = new HashMap<>(); + + private NpcHelpUpHandler() {} + + public static boolean isBeingHelped(UUID playerId) { + return ATTEMPTS.containsKey(playerId); + } + + public static void cancelHelpUp(UUID playerId) { + AssistAttempt attempt = ATTEMPTS.remove(playerId); + if (attempt != null) { + Mob helper = findMob(attempt.helperId); + if (helper != null) helper.getNavigation().stop(); + } + } + + @SubscribeEvent + public static void onServerTick(TickEvent.ServerTickEvent event) { + if (event.phase != TickEvent.Phase.END || !event.side.isServer()) return; + if (!Config.COMMON.npcHelpUpEnabled.get()) { + for (UUID playerId : new HashSet<>(ATTEMPTS.keySet())) cancelHelpUp(playerId); + return; + } + + for (UUID playerId : KnockedManager.getKnockedUuids()) { + ServerPlayer player = NetworkHandlerHelper.getPlayerByUuid( + net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(), playerId); + if (player == null || !player.isAlive() || CarryManager.isBeingCarried(playerId) + || HelpUpHandler.isBeingHelped(playerId) + || PlayerExecutionHandler.isBeingPlayerExecuted(playerId) + || MobKillHandler.isBeingMobExecuted(playerId)) { + cancelHelpUp(playerId); + continue; + } + + AssistAttempt attempt = ATTEMPTS.get(playerId); + Mob helper = attempt == null ? findHelper(player) : findMob(attempt.helperId); + if (!isValidHelper(helper, player) || hasThreats(player, helper)) { + cancelHelpUp(playerId); + continue; + } + if (attempt == null) { + attempt = new AssistAttempt(helper.getUUID(), Config.COMMON.npcHelpUpTime.get()); + ATTEMPTS.put(playerId, attempt); + } + + if (helper.distanceToSqr(player) > ASSIST_DISTANCE * ASSIST_DISTANCE) { + helper.getNavigation().moveTo(player, 1.1D); + attempt.timeLeft = Config.COMMON.npcHelpUpTime.get(); + } else { + helper.getNavigation().stop(); + helper.getLookControl().setLookAt(player, 30.0F, 30.0F); + if (--attempt.timeLeft <= 0) complete(playerId, player, helper); + } + } + + ATTEMPTS.keySet().removeIf(id -> !KnockedManager.getKnockedUuids().contains(id)); + } + + private static void complete(UUID playerId, ServerPlayer player, Mob helper) { + ATTEMPTS.remove(playerId); + helper.getNavigation().stop(); + KnockedManager.removeKnockedState(player); + player.heal(Config.COMMON.npcHelpUpHealAmount.get().floatValue()); + } + + private static Mob findHelper(ServerPlayer player) { + double range = Config.COMMON.npcHelpUpSearchRange.get(); + return ((ServerLevel) player.level()).getEntitiesOfClass(Mob.class, + player.getBoundingBox().inflate(range), mob -> isValidHelper(mob, player)) + .stream().filter(mob -> !hasThreats(player, mob)) + .min(java.util.Comparator.comparingDouble(player::distanceToSqr)).orElse(null); + } + + private static boolean isValidHelper(Mob mob, ServerPlayer player) { + if (mob == null || !mob.isAlive() || mob.level() != player.level()) return false; + ResourceLocation id = ForgeRegistries.ENTITY_TYPES.getKey(mob.getType()); + if (id == null || !Config.COMMON.npcHelpUpAllowlist.get().contains(id.toString())) return false; + if (mob instanceof TamableAnimal pet) return pet.isTame() && pet.isOwnedBy(player); + if (mob instanceof Villager villager) return villager.getPlayerReputation(player) > 0; + if (mob instanceof IronGolem golem) return golem.isPlayerCreated() || golem.getTarget() != player; + return !(mob instanceof Monster) && mob.getTarget() != player; + } + + private static boolean hasThreats(ServerPlayer player, Mob helper) { + if (helper.getTarget() != null && helper.getTarget().isAlive()) return true; + double range = Config.COMMON.npcThreatRange.get(); + AABB area = player.getBoundingBox().minmax(helper.getBoundingBox()).inflate(range); + return player.level().getEntitiesOfClass(Mob.class, area).stream().anyMatch(mob -> + mob.isAlive() && mob != helper && (mob instanceof Monster + || mob.getTarget() == player || mob.getTarget() == helper)); + } + + private static Mob findMob(UUID id) { + var server = net.minecraftforge.server.ServerLifecycleHooks.getCurrentServer(); + if (server == null) return null; + for (ServerLevel level : server.getAllLevels()) { + var entity = level.getEntity(id); + if (entity instanceof Mob mob) return mob; + } + return null; + } + + private static final class AssistAttempt { + private final UUID helperId; + private int timeLeft; + + private AssistAttempt(UUID helperId, int timeLeft) { + this.helperId = helperId; + this.timeLeft = timeLeft; + } + } +} diff --git a/src/main/java/net/fretux/knockedback/PlayerExecutionHandler.java b/src/main/java/net/fretux/knockedback/PlayerExecutionHandler.java index d3878bd..ace4659 100644 --- a/src/main/java/net/fretux/knockedback/PlayerExecutionHandler.java +++ b/src/main/java/net/fretux/knockedback/PlayerExecutionHandler.java @@ -40,7 +40,8 @@ public class PlayerExecutionHandler { @SubscribeEvent public static void onPlayerTick(TickEvent.PlayerTickEvent event) { - if (event.side.isServer() && event.phase == TickEvent.Phase.END && event.player instanceof ServerPlayer sp && isExecuting(sp.getUUID())) { + if (event.side.isServer() && event.phase == TickEvent.Phase.END && event.player instanceof ServerPlayer sp + && isExecuting(sp.getUUID()) && !Config.COMMON.executorMovementEnabled.get()) { sp.setDeltaMovement(0, 0, 0); sp.setSprinting(false); } diff --git a/src/main/java/net/fretux/knockedback/config/Config.java b/src/main/java/net/fretux/knockedback/config/Config.java index 3236027..745eefd 100644 --- a/src/main/java/net/fretux/knockedback/config/Config.java +++ b/src/main/java/net/fretux/knockedback/config/Config.java @@ -17,6 +17,9 @@ public class Config { public static class Common { public final ForgeConfigSpec.IntValue knockedDuration; public final ForgeConfigSpec.IntValue executionTime; + public final ForgeConfigSpec.IntValue mobExecutionTime; + public final ForgeConfigSpec.BooleanValue knockedMovementEnabled; + public final ForgeConfigSpec.BooleanValue executorMovementEnabled; public final ForgeConfigSpec.IntValue helpUpTime; public final ForgeConfigSpec.DoubleValue helpUpHealAmount; public final ForgeConfigSpec.BooleanValue helpUpEnabled; @@ -25,6 +28,12 @@ public class Config { public final ForgeConfigSpec.ConfigValue> mobExecutionAllowlist; public final ForgeConfigSpec.ConfigValue> knockedPotionEffects; public final ForgeConfigSpec.BooleanValue removeOtherPotionEffectsWhileKnocked; + public final ForgeConfigSpec.BooleanValue npcHelpUpEnabled; + public final ForgeConfigSpec.IntValue npcHelpUpTime; + public final ForgeConfigSpec.DoubleValue npcHelpUpHealAmount; + public final ForgeConfigSpec.DoubleValue npcHelpUpSearchRange; + public final ForgeConfigSpec.DoubleValue npcThreatRange; + public final ForgeConfigSpec.ConfigValue> npcHelpUpAllowlist; public Common(ForgeConfigSpec.Builder builder) { builder.push("general"); @@ -34,9 +43,21 @@ public class Config { .defineInRange("knockedDuration", 400, 100, 1200); executionTime = builder - .comment("How long (in ticks) it takes to execute a knocked player. Default: 60 (3 seconds).") + .comment("How long (in ticks) it takes another player to execute a knocked player. Default: 60 (3 seconds).") .defineInRange("executionTime", 60, 20, 200); + mobExecutionTime = builder + .comment("How long (in ticks) it takes a mob to execute a knocked player. Default: 60 (3 seconds).") + .defineInRange("mobExecutionTime", 60, 0, 1200); + + knockedMovementEnabled = builder + .comment("If true, knocked players retain horizontal movement and can jump. Default: false.") + .define("knockedMovementEnabled", false); + + executorMovementEnabled = builder + .comment("If true, players and mobs may move while executing a knocked player. Default: false.") + .define("executorMovementEnabled", false); + helpUpEnabled = builder .comment("If true, players can help knocked players recover by holding the help-up key. Default: true.") .define("helpUpEnabled", true); @@ -49,6 +70,34 @@ public class Config { .comment("How much health a helped-up player receives after recovery. Default: 4.0 (2 hearts).") .defineInRange("helpUpHealAmount", 4.0D, 1.0D, 20.0D); + npcHelpUpEnabled = builder + .comment("If true, friendly configured NPCs can help knocked players recover. Default: true.") + .define("npcHelpUpEnabled", true); + + npcHelpUpTime = builder + .comment("How long (in ticks) an NPC must remain beside a player to help them up. Default: 60 (3 seconds).") + .defineInRange("npcHelpUpTime", 60, 1, 1200); + + npcHelpUpHealAmount = builder + .comment("How much health an NPC-assisted player receives. Default: 4.0 (2 hearts).") + .defineInRange("npcHelpUpHealAmount", 4.0D, 1.0D, 20.0D); + + npcHelpUpSearchRange = builder + .comment("How far friendly NPCs can notice a knocked player. Default: 16 blocks.") + .defineInRange("npcHelpUpSearchRange", 16.0D, 2.0D, 64.0D); + + npcThreatRange = builder + .comment("Radius which must be free of living threats before an NPC will help. Default: 12 blocks.") + .defineInRange("npcThreatRange", 12.0D, 1.0D, 64.0D); + + npcHelpUpAllowlist = builder + .comment("Entity ids which may help knocked players.", + "Tamed animals must belong to the player; villagers require positive reputation.", + "Other listed mobs, including modded guards, must not be hostile to the player.") + .defineListAllowEmpty("npcHelpUpAllowlist", + List.of("minecraft:wolf", "minecraft:cat", "minecraft:parrot", "minecraft:villager", "minecraft:iron_golem"), + o -> o instanceof String); + mobExecutionEnabled = builder .comment("If true, mobs can execute knocked players. Default: true.") .define("mobExecutionEnabled", true); diff --git a/src/main/java/net/fretux/knockedback/effects/KnockedEffect.java b/src/main/java/net/fretux/knockedback/effects/KnockedEffect.java index da66521..b86bbd3 100644 --- a/src/main/java/net/fretux/knockedback/effects/KnockedEffect.java +++ b/src/main/java/net/fretux/knockedback/effects/KnockedEffect.java @@ -2,6 +2,7 @@ package net.fretux.knockedback.effects; import net.fretux.knockedback.KnockedManager; import net.fretux.knockedback.CarryManager; +import net.fretux.knockedback.config.Config; import net.minecraft.world.entity.Pose; import net.minecraft.world.entity.player.Player; import net.minecraftforge.event.entity.living.LivingEvent; @@ -21,7 +22,9 @@ public class KnockedEffect { player.setSprinting(false); player.hurtMarked = true; player.setNoGravity(false); - player.setDeltaMovement(0, player.getDeltaMovement().y(), 0); + if (!Config.COMMON.knockedMovementEnabled.get()) { + player.setDeltaMovement(0, player.getDeltaMovement().y(), 0); + } if (CarryManager.isBeingCarried(player.getUUID())) { if (player.getForcedPose() == Pose.SWIMMING) { player.setForcedPose(null); @@ -60,7 +63,8 @@ public class KnockedEffect { @SubscribeEvent public static void onLivingJump(LivingEvent.LivingJumpEvent event) { - if (event.getEntity() instanceof Player player && KnockedManager.isKnocked(player)) { + if (event.getEntity() instanceof Player player && KnockedManager.isKnocked(player) + && !Config.COMMON.knockedMovementEnabled.get()) { event.getEntity().setDeltaMovement(0, 0, 0); } }