Protect owned pets from lethal damage on Forge
This commit is contained in:
@@ -16,6 +16,7 @@ out
|
||||
# gradle
|
||||
build
|
||||
.gradle
|
||||
.gradle-user-home
|
||||
|
||||
# other
|
||||
eclipse
|
||||
|
||||
@@ -12,4 +12,6 @@ Mobs cannot execute players who are currently being carried. Press V near a knoc
|
||||
|
||||
Fire, lava, fall, and explosion deaths bypass the knocked state and kill normally.
|
||||
|
||||
Animations and other stuff like config files and custom items for reviving will come in future updates.
|
||||
Owned pets also survive lethal combat damage at 1 HP. They cannot take ordinary damage while knocked, and recover with four health after 20 seconds. Hold H within two blocks to help a knocked pet recover sooner. Their owner receives a chat notice. The pet's recovery timer is saved with the entity, so it resumes after a world reload. Tamed animals and modded followers that implement Minecraft's `OwnableEntity` interface are protected automatically.
|
||||
|
||||
For modded followers without vanilla ownership data, add their entity type IDs to `pets.protectedEntityTypes` in the Forge config file (`config/knockedback-common.toml`). Every mob of a listed type is protected, so list only the follower types you want to save. Pet protection, recovery time, recovery health, and player help are configurable in the same section.
|
||||
|
||||
@@ -5,6 +5,7 @@ All notable changes since version 1.0.0 are documented in this file.
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- Owned pets and configured modded followers now enter a saved knocked state on lethal combat damage; players can help them recover with H.
|
||||
- Added the help-up progress bar when a friendly NPC is reviving the player.
|
||||
- Added achievements for being rescued by an NPC, owned pet, friendly villager, or iron golem.
|
||||
- Added separate configuration for mob execution time, executor movement, and knocked-player movement.
|
||||
|
||||
@@ -37,26 +37,27 @@ public class HelpUpHandler {
|
||||
return helpUpAttempts.values().stream().anyMatch(attempt -> attempt.helperId.equals(helperId));
|
||||
}
|
||||
|
||||
public static void startHelpUp(ServerPlayer helper) {
|
||||
if (!Config.COMMON.helpUpEnabled.get()) return;
|
||||
public static boolean startHelpUp(ServerPlayer helper) {
|
||||
if (!Config.COMMON.helpUpEnabled.get()) return false;
|
||||
if (KnockedManager.isKnocked(helper)) {
|
||||
helper.sendSystemMessage(Component.translatable("message.knockedback.cannot_help_while_knocked"));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (PlayerExecutionHandler.isExecuting(helper.getUUID())) return;
|
||||
if (isHelping(helper.getUUID())) return;
|
||||
if (PlayerExecutionHandler.isExecuting(helper.getUUID())) return false;
|
||||
if (isHelping(helper.getUUID()) || PetKnockdownHandler.isHelping(helper.getUUID())) return false;
|
||||
Player target = findNearestKnocked(helper);
|
||||
if (target == null) return;
|
||||
if (target == null) return false;
|
||||
UUID knockedId = target.getUUID();
|
||||
if (CarryManager.isBeingCarried(knockedId)) {
|
||||
helper.sendSystemMessage(Component.translatable("message.knockedback.cannot_help_carried_player"));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (helpUpAttempts.containsKey(knockedId)) return;
|
||||
if (NpcHelpUpHandler.isBeingHelped(knockedId)) return;
|
||||
if (helpUpAttempts.containsKey(knockedId)) return false;
|
||||
if (NpcHelpUpHandler.isBeingHelped(knockedId)) return false;
|
||||
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()));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void cancelHelpUp(UUID knockedId) {
|
||||
|
||||
@@ -26,9 +26,12 @@ public class HelpUpKnockedPacket {
|
||||
ServerPlayer helper = ctx.get().getSender();
|
||||
if (helper == null) return;
|
||||
if (msg.helping) {
|
||||
HelpUpHandler.startHelpUp(helper);
|
||||
if (!HelpUpHandler.startHelpUp(helper)) {
|
||||
PetKnockdownHandler.startHelpUp(helper);
|
||||
}
|
||||
} else {
|
||||
HelpUpHandler.cancelHelpUpByHelper(helper.getUUID());
|
||||
PetKnockdownHandler.cancelHelpUpByHelper(helper.getUUID());
|
||||
}
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
|
||||
@@ -121,7 +121,8 @@ public final class NpcHelpUpHandler {
|
||||
}
|
||||
|
||||
private static boolean isValidHelper(Mob mob, ServerPlayer player) {
|
||||
if (mob == null || !mob.isAlive() || mob.level() != player.level()) return false;
|
||||
if (mob == null || !mob.isAlive() || mob.level() != player.level()
|
||||
|| PetKnockdownHandler.isKnocked(mob)) 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);
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
package net.fretux.knockedback;
|
||||
|
||||
import net.fretux.knockedback.config.Config;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.Mob;
|
||||
import net.minecraft.world.entity.OwnableEntity;
|
||||
import net.minecraft.world.entity.TamableAnimal;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingDeathEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingHurtEvent;
|
||||
import net.minecraftforge.event.entity.player.AttackEntityEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
import net.minecraftforge.registries.ForgeRegistries;
|
||||
import net.minecraftforge.server.ServerLifecycleHooks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = KnockedBack.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||
public final class PetKnockdownHandler {
|
||||
private static final String TIMER_KEY = "knockedback.pet_remaining";
|
||||
private static final double HELP_RANGE = 2.0D;
|
||||
private static final Map<UUID, HelpAttempt> HELP_ATTEMPTS = new HashMap<>();
|
||||
|
||||
private PetKnockdownHandler() {}
|
||||
|
||||
private static final class HelpAttempt {
|
||||
final UUID helperId;
|
||||
int remaining;
|
||||
|
||||
HelpAttempt(UUID helperId) {
|
||||
this.helperId = helperId;
|
||||
this.remaining = Config.COMMON.helpUpTime.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isKnocked(LivingEntity entity) {
|
||||
return entity instanceof Mob && entity.getPersistentData().getInt(TIMER_KEY) > 0;
|
||||
}
|
||||
|
||||
private static boolean isProtectedPet(LivingEntity entity) {
|
||||
if (!Config.COMMON.petKnockdownEnabled.get() || !(entity instanceof Mob)) return false;
|
||||
if (entity instanceof TamableAnimal pet && pet.isTame() && pet.getOwnerUUID() != null) return true;
|
||||
if (entity instanceof OwnableEntity owned && owned.getOwnerUUID() != null) return true;
|
||||
var id = ForgeRegistries.ENTITY_TYPES.getKey(entity.getType());
|
||||
return id != null && Config.COMMON.protectedEntityTypes.get().contains(id.toString());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onDeath(LivingDeathEvent event) {
|
||||
LivingEntity entity = event.getEntity();
|
||||
if (entity.level().isClientSide || event.getSource().is(KnockedBackDamageTags.BYPASS_KNOCKDOWN)
|
||||
|| !isProtectedPet(entity)) return;
|
||||
entity.setHealth(1.0F);
|
||||
entity.getPersistentData().putInt(TIMER_KEY, Config.COMMON.petKnockdownDuration.get());
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setTarget(null);
|
||||
mob.getNavigation().stop();
|
||||
}
|
||||
notifyOwner(entity, "message.knockedback.pet_knocked");
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onHurt(LivingHurtEvent event) {
|
||||
LivingEntity entity = event.getEntity();
|
||||
if (entity.level().isClientSide || !isKnocked(entity)) return;
|
||||
if (event.getSource().is(KnockedBackDamageTags.BYPASS_KNOCKDOWN)) {
|
||||
clearKnockdown(entity);
|
||||
return;
|
||||
}
|
||||
entity.getPersistentData().putInt(TIMER_KEY, Config.COMMON.petKnockdownDuration.get());
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onLivingTick(LivingEvent.LivingTickEvent event) {
|
||||
LivingEntity entity = event.getEntity();
|
||||
if (entity.level().isClientSide || !isKnocked(entity)) return;
|
||||
if (entity instanceof Mob mob) {
|
||||
mob.setTarget(null);
|
||||
mob.getNavigation().stop();
|
||||
mob.setDeltaMovement(0, mob.getDeltaMovement().y(), 0);
|
||||
}
|
||||
if (HELP_ATTEMPTS.containsKey(entity.getUUID())) return;
|
||||
CompoundTag data = entity.getPersistentData();
|
||||
int remaining = data.getInt(TIMER_KEY) - 1;
|
||||
if (remaining > 0) {
|
||||
data.putInt(TIMER_KEY, remaining);
|
||||
} else {
|
||||
clearKnockdown(entity);
|
||||
entity.setHealth(Math.min(entity.getMaxHealth(), Config.COMMON.petRecoveryHealth.get().floatValue()));
|
||||
notifyOwner(entity, "message.knockedback.pet_recovered");
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearKnockdown(LivingEntity entity) {
|
||||
entity.getPersistentData().remove(TIMER_KEY);
|
||||
cancelHelpUp(entity.getUUID());
|
||||
}
|
||||
|
||||
private static void notifyOwner(LivingEntity entity, String messageKey) {
|
||||
if (!(entity instanceof OwnableEntity owned) || owned.getOwnerUUID() == null) return;
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
if (server == null) return;
|
||||
ServerPlayer owner = server.getPlayerList().getPlayer(owned.getOwnerUUID());
|
||||
if (owner != null) owner.sendSystemMessage(Component.translatable(messageKey, entity.getDisplayName()));
|
||||
}
|
||||
|
||||
public static boolean isHelping(UUID helperId) {
|
||||
return HELP_ATTEMPTS.values().stream().anyMatch(attempt -> attempt.helperId.equals(helperId));
|
||||
}
|
||||
|
||||
public static void startHelpUp(ServerPlayer helper) {
|
||||
if (!Config.COMMON.helpUpEnabled.get() || !Config.COMMON.petHelpUpEnabled.get()
|
||||
|| KnockedManager.isKnocked(helper) || HelpUpHandler.isHelping(helper.getUUID())
|
||||
|| PlayerExecutionHandler.isExecuting(helper.getUUID()) || isHelping(helper.getUUID())) return;
|
||||
AABB box = helper.getBoundingBox().inflate(HELP_RANGE);
|
||||
Mob pet = helper.level().getEntitiesOfClass(Mob.class, box).stream()
|
||||
.filter(entity -> isKnocked(entity) && !HELP_ATTEMPTS.containsKey(entity.getUUID())
|
||||
&& helper.distanceToSqr(entity) <= HELP_RANGE * HELP_RANGE)
|
||||
.min(Comparator.comparingDouble(helper::distanceToSqr)).orElse(null);
|
||||
if (pet == null) return;
|
||||
HELP_ATTEMPTS.put(pet.getUUID(), new HelpAttempt(helper.getUUID()));
|
||||
helper.sendSystemMessage(Component.translatable("message.knockedback.help_up_started", pet.getDisplayName()));
|
||||
}
|
||||
|
||||
public static void cancelHelpUpByHelper(UUID helperId) {
|
||||
for (Map.Entry<UUID, HelpAttempt> entry : new ArrayList<>(HELP_ATTEMPTS.entrySet())) {
|
||||
if (entry.getValue().helperId.equals(helperId)) cancelHelpUp(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private static void cancelHelpUp(UUID petId) {
|
||||
HelpAttempt attempt = HELP_ATTEMPTS.remove(petId);
|
||||
if (attempt == null) return;
|
||||
ServerPlayer helper = findPlayer(attempt.helperId);
|
||||
if (helper != null) clearProgress(helper);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onServerTick(TickEvent.ServerTickEvent event) {
|
||||
if (event.phase != TickEvent.Phase.END || !event.side.isServer()) return;
|
||||
for (Map.Entry<UUID, HelpAttempt> entry : new ArrayList<>(HELP_ATTEMPTS.entrySet())) {
|
||||
UUID petId = entry.getKey();
|
||||
HelpAttempt attempt = entry.getValue();
|
||||
Mob pet = findMob(petId);
|
||||
ServerPlayer helper = findPlayer(attempt.helperId);
|
||||
if (pet == null || helper == null || !isKnocked(pet) || KnockedManager.isKnocked(helper)
|
||||
|| pet.level() != helper.level() || helper.distanceToSqr(pet) > HELP_RANGE * HELP_RANGE) {
|
||||
cancelHelpUp(petId);
|
||||
continue;
|
||||
}
|
||||
attempt.remaining--;
|
||||
NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> helper),
|
||||
new ExecutionProgressPacket(attempt.remaining, Config.COMMON.helpUpTime.get(),
|
||||
ExecutionProgressPacket.Action.HELP_UP, pet.getDisplayName().getString(), null));
|
||||
if (attempt.remaining <= 0) {
|
||||
HELP_ATTEMPTS.remove(petId);
|
||||
pet.getPersistentData().remove(TIMER_KEY);
|
||||
pet.setHealth(Math.min(pet.getMaxHealth(), pet.getHealth() + Config.COMMON.helpUpHealAmount.get().floatValue()));
|
||||
clearProgress(helper);
|
||||
KnockedbackAdvancements.recordHelpUp(helper);
|
||||
helper.sendSystemMessage(Component.translatable("message.knockedback.you_helped_up", pet.getDisplayName()));
|
||||
notifyOwner(pet, "message.knockedback.pet_recovered");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.side.isServer() && event.phase == TickEvent.Phase.END
|
||||
&& event.player instanceof ServerPlayer helper && isHelping(helper.getUUID())) {
|
||||
helper.setDeltaMovement(0, 0, 0);
|
||||
helper.setSprinting(false);
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onAttack(AttackEntityEvent event) {
|
||||
if (event.getEntity() instanceof ServerPlayer helper && isHelping(helper.getUUID())) event.setCanceled(true);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onHelperHurt(LivingHurtEvent event) {
|
||||
if (event.getEntity() instanceof ServerPlayer helper) cancelHelpUpByHelper(helper.getUUID());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onLogout(PlayerEvent.PlayerLoggedOutEvent event) {
|
||||
cancelHelpUpByHelper(event.getEntity().getUUID());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRespawn(PlayerEvent.PlayerRespawnEvent event) {
|
||||
cancelHelpUpByHelper(event.getEntity().getUUID());
|
||||
}
|
||||
|
||||
private static Mob findMob(UUID id) {
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
if (server == null) return null;
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
Entity entity = level.getEntity(id);
|
||||
if (entity instanceof Mob mob) return mob;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ServerPlayer findPlayer(UUID id) {
|
||||
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
|
||||
return server == null ? null : server.getPlayerList().getPlayer(id);
|
||||
}
|
||||
|
||||
private static void clearProgress(ServerPlayer player) {
|
||||
NetworkHandler.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player),
|
||||
new ExecutionProgressPacket(0, 1, ExecutionProgressPacket.Action.NONE, null));
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,11 @@ public class Config {
|
||||
public final ForgeConfigSpec.DoubleValue npcHelpUpSearchRange;
|
||||
public final ForgeConfigSpec.DoubleValue npcThreatRange;
|
||||
public final ForgeConfigSpec.ConfigValue<List<? extends String>> npcHelpUpAllowlist;
|
||||
public final ForgeConfigSpec.BooleanValue petKnockdownEnabled;
|
||||
public final ForgeConfigSpec.IntValue petKnockdownDuration;
|
||||
public final ForgeConfigSpec.DoubleValue petRecoveryHealth;
|
||||
public final ForgeConfigSpec.BooleanValue petHelpUpEnabled;
|
||||
public final ForgeConfigSpec.ConfigValue<List<? extends String>> protectedEntityTypes;
|
||||
|
||||
public Common(ForgeConfigSpec.Builder builder) {
|
||||
builder.push("general");
|
||||
@@ -124,6 +129,30 @@ public class Config {
|
||||
.comment("If true, Totems of Undying prevent entering the knocked state. Default: true.")
|
||||
.define("totemPreventsKnockdown", true);
|
||||
|
||||
builder.pop().push("pets");
|
||||
|
||||
petKnockdownEnabled = builder
|
||||
.comment("If true, owned pets and listed follower types survive lethal combat damage. Default: true.")
|
||||
.define("petKnockdownEnabled", true);
|
||||
|
||||
petKnockdownDuration = builder
|
||||
.comment("How long (in ticks) a pet remains knocked before recovering. Default: 400 (20 seconds).")
|
||||
.defineInRange("petKnockdownDuration", 400, 20, 1200);
|
||||
|
||||
petRecoveryHealth = builder
|
||||
.comment("Health restored to a pet after automatic recovery. Default: 4.0 (2 hearts).")
|
||||
.defineInRange("petRecoveryHealth", 4.0D, 1.0D, 100.0D);
|
||||
|
||||
petHelpUpEnabled = builder
|
||||
.comment("If true, a player can hold the help-up key near a knocked pet to rescue it early. Default: true.")
|
||||
.define("petHelpUpEnabled", true);
|
||||
|
||||
protectedEntityTypes = builder
|
||||
.comment("Additional entity type ids to protect, for modded followers without vanilla ownership data.",
|
||||
"Every mob of a listed type is protected, so list only intended follower types.",
|
||||
"Example: [\"modid:follower\"]")
|
||||
.defineListAllowEmpty("protectedEntityTypes", List.of(), o -> o instanceof String);
|
||||
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"message.knockedback.been_dropped": "Du wurdest von %1$s abgesetzt",
|
||||
"message.knockedback.you_executed": "Du hast %1$s hingerichtet!",
|
||||
"message.knockedback.you_were_executed": "Du wurdest von %1$s hingerichtet!",
|
||||
"message.knockedback.pet_knocked": "%s wurde niedergeschlagen. Halte H in der N\u00e4he gedr\u00fcckt, um zu helfen.",
|
||||
"message.knockedback.pet_recovered": "%s hat sich erholt.",
|
||||
"overlay.knockedback.execution_progress": "Hinrichten",
|
||||
"overlay.knockedback.help_up_progress": "Aufhelfen",
|
||||
"overlay.knockedback.executing_target": "%s hinrichten...",
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"message.knockedback.you_were_helped_up": "You were helped up by %s!",
|
||||
"message.knockedback.you_executed": "You executed %s!",
|
||||
"message.knockedback.you_were_executed": "You were executed by %s!",
|
||||
"message.knockedback.pet_knocked": "%s was knocked down. Hold H nearby to help them recover.",
|
||||
"message.knockedback.pet_recovered": "%s recovered from knockdown.",
|
||||
"overlay.knockedback.execution_progress": "Executing",
|
||||
"overlay.knockedback.help_up_progress": "Helping up",
|
||||
"overlay.knockedback.executing_target": "Executing %s...",
|
||||
|
||||
Reference in New Issue
Block a user