Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.items.ItemLockpick;
|
||||
import com.tiedup.remake.items.base.IKnife;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.util.GameConstants;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ButtonBlock;
|
||||
import net.minecraft.world.level.block.DoorBlock;
|
||||
import net.minecraft.world.level.block.FenceGateBlock;
|
||||
import net.minecraft.world.level.block.LeverBlock;
|
||||
import net.minecraft.world.level.block.TrapDoorBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingHurtEvent;
|
||||
import net.minecraftforge.event.entity.living.LivingEntityUseItemEvent;
|
||||
import net.minecraftforge.event.entity.player.AttackEntityEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.level.BlockEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Unified event handler for all bondage item restrictions.
|
||||
*
|
||||
* Phase 14.4+: Centralized restriction system
|
||||
* Leg Binding: Separate arm/leg restrictions
|
||||
*
|
||||
* This handler manages restrictions based on equipped bondage items:
|
||||
*
|
||||
* === LEGS BOUND (hasLegsBound) ===
|
||||
* - No sprinting
|
||||
* - No climbing ladders (can descend)
|
||||
* - No elytra flying
|
||||
* - Reduced swim speed (50%)
|
||||
*
|
||||
* === ARMS BOUND (hasArmsBound) ===
|
||||
* - No block breaking
|
||||
* - No block placing
|
||||
* - No attacking
|
||||
* - No item usage
|
||||
* - No block interaction (except allowed blocks)
|
||||
*
|
||||
* === MITTENS ===
|
||||
* - Additional hand restrictions
|
||||
* - Allowed: buttons, levers, doors, trapdoors, fence gates (when arms bound only)
|
||||
*
|
||||
* === BLINDFOLDED ===
|
||||
* - Vision effects (handled client-side)
|
||||
*
|
||||
* === GAGGED ===
|
||||
* - Chat muffling (handled in ChatEventHandler)
|
||||
*
|
||||
* @see RestraintTaskTickHandler for task tick progression (untying, tying, force feeding)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class BondageItemRestrictionHandler {
|
||||
|
||||
/** Cooldown between restriction messages (in milliseconds) */
|
||||
private static final long MESSAGE_COOLDOWN_MS = 2000; // 2 seconds
|
||||
|
||||
/** Swim speed multiplier when tied (from config) */
|
||||
|
||||
/** Per-player, per-category message cooldowns */
|
||||
private static final Map<
|
||||
UUID,
|
||||
Map<MessageCategory, Long>
|
||||
> messageCooldowns = new HashMap<>();
|
||||
|
||||
// ========================================
|
||||
// MOVEMENT RESTRICTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Movement restrictions per tick (throttled to every 5 ticks).
|
||||
* - Prevent sprinting when tied
|
||||
* - Prevent ladder climbing when tied (can descend)
|
||||
* - Reduce swim speed when tied
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.player;
|
||||
|
||||
// Throttle: only check every 5 ticks (0.25 seconds) - per-player timing
|
||||
if (player.tickCount % 5 != 0) {
|
||||
return;
|
||||
}
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Movement restrictions only apply when LEGS are bound
|
||||
if (!state.hasLegsBound()) return;
|
||||
|
||||
// === SPRINT RESTRICTION ===
|
||||
if (player.isSprinting()) {
|
||||
player.setSprinting(false);
|
||||
}
|
||||
|
||||
// === LADDER RESTRICTION ===
|
||||
// Can descend but not climb
|
||||
if (player.onClimbable()) {
|
||||
Vec3 motion = player.getDeltaMovement();
|
||||
if (motion.y > 0) {
|
||||
player.setDeltaMovement(motion.x, 0, motion.z);
|
||||
}
|
||||
}
|
||||
|
||||
// === SWIM SPEED RESTRICTION ===
|
||||
if (player.isInWater() && player.isSwimming()) {
|
||||
Vec3 motion = player.getDeltaMovement();
|
||||
player.setDeltaMovement(motion.scale(
|
||||
com.tiedup.remake.core.ModConfig.SERVER.tiedSwimSpeedMultiplier.get()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent elytra flying when tied.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onLivingTick(LivingEvent.LivingTickEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Cheap check FIRST: only proceed if player is flying
|
||||
if (!player.isFallFlying()) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null || !state.hasLegsBound()) return;
|
||||
|
||||
// Elytra restriction only applies when LEGS are bound
|
||||
player.stopFallFlying();
|
||||
sendRestrictionMessage(player, MessageCategory.NO_ELYTRA);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INTERACTION RESTRICTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Block breaking restrictions.
|
||||
* - Tied: Cannot break blocks
|
||||
* - Mittens: Cannot break blocks (hands covered)
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onBlockBreak(BlockEvent.BreakEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Block breaking requires ARMS to be free
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_BREAK_TIED);
|
||||
} else if (state.hasMittens()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_BREAK_MITTENS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block placing restrictions.
|
||||
* - Arms bound: Cannot place blocks
|
||||
* - Mittens: Cannot place blocks (hands covered)
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onBlockPlace(BlockEvent.EntityPlaceEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Block placing requires ARMS to be free
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_PLACE_TIED);
|
||||
} else if (state.hasMittens()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_PLACE_MITTENS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block interaction restrictions.
|
||||
* - Arms bound: Cannot interact with most blocks
|
||||
* - Mittens: Cannot interact (hands covered)
|
||||
* - Exception: Buttons, levers, doors (can be pressed/opened with body when arms bound, but not with mittens)
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickBlock(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
boolean hasArmsBound = state.hasArmsBound();
|
||||
boolean hasMittens = state.hasMittens();
|
||||
|
||||
if (!hasArmsBound && !hasMittens) return;
|
||||
|
||||
// Get the block being interacted with
|
||||
BlockState blockState = event.getLevel().getBlockState(event.getPos());
|
||||
|
||||
// Allow specific block interactions (can press with body/head) - only when arms bound WITHOUT mittens
|
||||
// Mittens block ALL interactions since you can't use buttons with covered hands
|
||||
if (
|
||||
hasArmsBound && !hasMittens && isAllowedTiedInteraction(blockState)
|
||||
) {
|
||||
return; // Allow this interaction
|
||||
}
|
||||
|
||||
// Block all other interactions
|
||||
event.setCanceled(true);
|
||||
if (hasArmsBound) {
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_INTERACT_TIED);
|
||||
} else {
|
||||
sendRestrictionMessage(
|
||||
player,
|
||||
MessageCategory.CANT_INTERACT_MITTENS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Item usage restrictions.
|
||||
* - Arms bound: Cannot use items (except knife/lockpick without mittens)
|
||||
* - Mittens: Cannot use items (hands covered)
|
||||
*
|
||||
* v2.5: Allow knife and lockpick usage when tied (but NOT with mittens).
|
||||
* This enables the player to cut/lockpick their binds while restrained.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickItem(
|
||||
PlayerInteractEvent.RightClickItem event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
ItemStack heldItem = event.getItemStack();
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// v2.5: Allow knife and lockpick usage when tied (but NOT with mittens)
|
||||
if (state.hasArmsBound() && !state.hasMittens()) {
|
||||
// Allow knives - they have special cutting logic
|
||||
if (heldItem.getItem() instanceof IKnife) {
|
||||
return; // Don't block
|
||||
}
|
||||
// Allow lockpicks - they open the struggle choice screen
|
||||
if (heldItem.getItem() instanceof ItemLockpick) {
|
||||
return; // Don't block
|
||||
}
|
||||
}
|
||||
|
||||
// Item usage requires ARMS to be free (except above exceptions)
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(player, MessageCategory.CANT_USE_ITEM_TIED);
|
||||
} else if (state.hasMittens()) {
|
||||
event.setCanceled(true);
|
||||
sendRestrictionMessage(
|
||||
player,
|
||||
MessageCategory.CANT_USE_ITEM_MITTENS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attack restrictions.
|
||||
* - Captive/slave attacking their kidnapper: Cannot attack, gets shocked
|
||||
* - Arms bound: Cannot attack at all
|
||||
* - Mittens only: Can punch but damage is zeroed in onLivingHurt()
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onAttack(AttackEntityEvent event) {
|
||||
Player player = event.getEntity();
|
||||
Entity target = event.getTarget();
|
||||
|
||||
// Check if player is attacking a kidnapper
|
||||
if (target instanceof EntityKidnapper kidnapper) {
|
||||
// Check if the attacker is this kidnapper's captive or job worker
|
||||
boolean isThisCaptive = false;
|
||||
boolean isThisJobWorker = false;
|
||||
|
||||
// Check if player is the current captive
|
||||
IRestrainable captive = kidnapper.getCaptive();
|
||||
if (captive != null) {
|
||||
UUID captiveUUID = captive.getKidnappedUniqueId();
|
||||
if (
|
||||
captiveUUID != null && captiveUUID.equals(player.getUUID())
|
||||
) {
|
||||
isThisCaptive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if player is the job worker
|
||||
UUID workerUUID = kidnapper.getJobWorkerUUID();
|
||||
if (workerUUID != null && workerUUID.equals(player.getUUID())) {
|
||||
isThisJobWorker = true;
|
||||
}
|
||||
|
||||
// Only block if player is THIS kidnapper's captive or job worker
|
||||
if (isThisCaptive || isThisJobWorker) {
|
||||
event.setCanceled(true);
|
||||
|
||||
// Kidnapper responds with dialogue
|
||||
kidnapper.talkTo(
|
||||
player,
|
||||
DialogueCategory.ATTACK_SLAVE
|
||||
);
|
||||
|
||||
// Shock the captive/worker
|
||||
IRestrainable playerState = KidnappedHelper.getKidnappedState(
|
||||
player
|
||||
);
|
||||
if (playerState != null) {
|
||||
playerState.shockKidnapped(
|
||||
" (You cannot attack your master!)",
|
||||
2.0f
|
||||
);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRICTION] {} tried to attack master {} - shocked (captive={}, worker={})",
|
||||
player.getName().getString(),
|
||||
kidnapper.getName().getString(),
|
||||
isThisCaptive,
|
||||
isThisJobWorker
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Other players CAN attack this kidnapper - don't block
|
||||
}
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Arms bound: cannot attack at all
|
||||
if (state.hasArmsBound()) {
|
||||
event.setCanceled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mittens only (not arms bound): allow punch animation
|
||||
// Damage will be zeroed in onLivingHurt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Damage reduction for mittens.
|
||||
* When a player with mittens (but not arms bound) attacks, damage is reduced to 0.
|
||||
* The punch animation still plays, but no damage is dealt.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onLivingHurt(LivingHurtEvent event) {
|
||||
// Check if the attacker is a player with mittens
|
||||
if (!(event.getSource().getEntity() instanceof Player player)) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
// Mittens only (not arms bound): zero damage
|
||||
if (state.hasMittens() && !state.hasArmsBound()) {
|
||||
event.setAmount(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fall damage protection for captive players on a leash.
|
||||
* When being led by a kidnapper/master, the player has no control over movement
|
||||
* and should not take fall damage from terrain the captor drags them through.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGHEST)
|
||||
public static void onCaptiveFallDamage(LivingHurtEvent event) {
|
||||
if (!(event.getEntity() instanceof Player player)) return;
|
||||
if (
|
||||
!event
|
||||
.getSource()
|
||||
.is(net.minecraft.world.damagesource.DamageTypes.FALL)
|
||||
) return;
|
||||
|
||||
IRestrainable state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state == null) return;
|
||||
|
||||
if (state.isCaptive()) {
|
||||
event.setCanceled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the Master when their pet takes damage.
|
||||
* This allows the Master to react (e.g., get up from human chair).
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPetHurt(LivingHurtEvent event) {
|
||||
if (!(event.getEntity() instanceof ServerPlayer pet)) return;
|
||||
if (event.getAmount() <= 0) return;
|
||||
|
||||
// Find if this player has a nearby Master who owns them
|
||||
var masters = pet.level().getEntitiesOfClass(
|
||||
com.tiedup.remake.entities.EntityMaster.class,
|
||||
pet.getBoundingBox().inflate(32.0),
|
||||
m -> m.isAlive() && m.hasPet()
|
||||
&& pet.getUUID().equals(m.getStateManager().getPetPlayerUUID())
|
||||
);
|
||||
|
||||
for (var master : masters) {
|
||||
master.onPetHurt(event.getSource(), event.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
// ========== BREAK SPEED REDUCTION ==========
|
||||
|
||||
/**
|
||||
* Apply break speed reduction when tied up.
|
||||
* This makes mining extremely slow (10% speed) when restrained.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onCalculateSpeed(PlayerEvent.BreakSpeed event) {
|
||||
Player player = event.getEntity();
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(player);
|
||||
|
||||
if (state != null && state.isTiedUp()) {
|
||||
event.setNewSpeed(
|
||||
event.getNewSpeed() * GameConstants.TIED_BREAK_SPEED_MULTIPLIER
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== BLOCK SELF-EATING WHEN GAGGED ==========
|
||||
|
||||
/**
|
||||
* Block gagged players from eating food themselves.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onGaggedPlayerEat(LivingEntityUseItemEvent.Start event) {
|
||||
if (!(event.getEntity() instanceof Player player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.getItem().getItem().isEdible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IBondageState state = KidnappedHelper.getKidnappedState(player);
|
||||
if (state != null && state.isGagged()) {
|
||||
event.setCanceled(true);
|
||||
player.displayClientMessage(
|
||||
Component.literal("You can't eat with a gag on.").withStyle(
|
||||
ChatFormatting.RED
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPER METHODS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if a block interaction should be allowed when tied.
|
||||
* These blocks can be activated with body/head, not hands.
|
||||
*/
|
||||
private static boolean isAllowedTiedInteraction(BlockState blockState) {
|
||||
// Buttons - can press with head/body
|
||||
if (blockState.getBlock() instanceof ButtonBlock) return true;
|
||||
|
||||
// Levers - can push with body
|
||||
if (blockState.getBlock() instanceof LeverBlock) return true;
|
||||
|
||||
// Doors - can push open with body
|
||||
if (blockState.getBlock() instanceof DoorBlock) return true;
|
||||
|
||||
// Trapdoors - debatable, but allow for now
|
||||
if (blockState.getBlock() instanceof TrapDoorBlock) return true;
|
||||
|
||||
// Fence gates - can push open
|
||||
if (blockState.getBlock() instanceof FenceGateBlock) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a restriction message to the player.
|
||||
* Uses per-category cooldown to prevent spam.
|
||||
*/
|
||||
private static void sendRestrictionMessage(
|
||||
Player player,
|
||||
MessageCategory category
|
||||
) {
|
||||
// Only send on server side
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
UUID playerId = player.getUUID();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// Get or create player's cooldown map
|
||||
Map<MessageCategory, Long> playerCooldowns =
|
||||
messageCooldowns.computeIfAbsent(playerId, k -> new HashMap<>());
|
||||
|
||||
// Check cooldown for this category
|
||||
Long lastSent = playerCooldowns.get(category);
|
||||
if (lastSent != null && (now - lastSent) < MESSAGE_COOLDOWN_MS) {
|
||||
return; // Still on cooldown
|
||||
}
|
||||
|
||||
// Update cooldown and send message
|
||||
playerCooldowns.put(category, now);
|
||||
SystemMessageManager.sendRestriction(player, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up cooldowns for a player (call when they disconnect).
|
||||
*/
|
||||
public static void clearCooldowns(UUID playerId) {
|
||||
messageCooldowns.remove(playerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityLaborGuard;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.ShulkerBoxBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraftforge.event.entity.item.ItemTossEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for labor tool protection.
|
||||
*
|
||||
* Prevents prisoners from:
|
||||
* - Dropping labor tools
|
||||
* - Storing labor tools in containers
|
||||
*
|
||||
* Note: Death handling removed - if prisoner dies, they're no longer working anyway.
|
||||
*
|
||||
* This is extracted from CampLaborEventHandler after the refactoring to remove event-driven task tracking.
|
||||
* Only the essential protection mechanisms remain.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||
public class LaborToolProtectionHandler {
|
||||
|
||||
private static final Map<UUID, Long> lastDropWarningTick = new HashMap<>();
|
||||
private static final long DROP_WARNING_COOLDOWN = 60; // 3 seconds
|
||||
|
||||
/** Remove player entry on disconnect to prevent memory leak. */
|
||||
public static void cleanupPlayer(java.util.UUID playerId) {
|
||||
lastDropWarningTick.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent dropping labor tools.
|
||||
* Forge removes the item from inventory before firing ItemTossEvent,
|
||||
* so we must restore it after canceling.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onItemToss(ItemTossEvent event) {
|
||||
ItemStack stack = event.getEntity().getItem();
|
||||
if (!isLaborTool(stack)) return;
|
||||
|
||||
// Cancel + restore (Forge removes from inventory before event fires)
|
||||
event.setCanceled(true);
|
||||
event.getPlayer().getInventory().add(stack);
|
||||
|
||||
Player player = event.getPlayer();
|
||||
if (!(player instanceof ServerPlayer serverPlayer)) return;
|
||||
if (!(player.level() instanceof ServerLevel level)) return;
|
||||
|
||||
// Cooldown
|
||||
long tick = level.getGameTime();
|
||||
Long last = lastDropWarningTick.get(player.getUUID());
|
||||
if (last != null && (tick - last) < DROP_WARNING_COOLDOWN) return;
|
||||
lastDropWarningTick.put(player.getUUID(), tick);
|
||||
|
||||
// Guard reaction
|
||||
EntityLaborGuard guard = findGuardForPlayer(level, player.getUUID());
|
||||
if (guard != null && guard.isAlive()) {
|
||||
guard.getLookControl().setLookAt(player);
|
||||
guard.guardSay(
|
||||
serverPlayer,
|
||||
"guard.labor.drop_tool",
|
||||
"Stupid! Don't drop your tools!"
|
||||
);
|
||||
} else {
|
||||
player.displayClientMessage(
|
||||
Component.literal("You cannot drop labor tools!"),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent storing labor tools in containers.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onRightClickBlock(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
ItemStack held = player.getMainHandItem();
|
||||
if (!isLaborTool(held)) return;
|
||||
|
||||
BlockState state = event.getLevel().getBlockState(event.getPos());
|
||||
if (
|
||||
!(state.getBlock() instanceof ChestBlock) &&
|
||||
!(state.getBlock() instanceof ShulkerBoxBlock)
|
||||
) return;
|
||||
|
||||
event.setCanceled(true);
|
||||
|
||||
if (
|
||||
player instanceof ServerPlayer sp &&
|
||||
player.level() instanceof ServerLevel sl
|
||||
) {
|
||||
// Reuse same cooldown
|
||||
long tick = sl.getGameTime();
|
||||
Long last = lastDropWarningTick.get(player.getUUID());
|
||||
if (last != null && (tick - last) < DROP_WARNING_COOLDOWN) return;
|
||||
lastDropWarningTick.put(player.getUUID(), tick);
|
||||
|
||||
EntityLaborGuard guard = findGuardForPlayer(sl, player.getUUID());
|
||||
if (guard != null && guard.isAlive()) {
|
||||
guard.getLookControl().setLookAt(player);
|
||||
guard.guardSay(
|
||||
sp,
|
||||
"guard.labor.hide_tool",
|
||||
"Don't try to hide your tools!"
|
||||
);
|
||||
} else {
|
||||
player.displayClientMessage(
|
||||
Component.literal("You cannot store labor tools!"),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static EntityLaborGuard findGuardForPlayer(
|
||||
ServerLevel level,
|
||||
UUID playerUUID
|
||||
) {
|
||||
PrisonerManager manager = PrisonerManager.get(level);
|
||||
LaborRecord labor = manager.getLaborRecord(playerUUID);
|
||||
UUID guardId = labor.getGuardId();
|
||||
if (guardId == null) return null;
|
||||
net.minecraft.world.entity.Entity entity = level.getEntity(guardId);
|
||||
if (entity instanceof EntityLaborGuard guard) return guard;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is a labor tool (tagged as LaborTool).
|
||||
*/
|
||||
private static boolean isLaborTool(ItemStack stack) {
|
||||
return stack.hasTag() && stack.getTag().getBoolean("LaborTool");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.items.ItemChokeCollar;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.v2.blocks.PetBedManager;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.BedBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent;
|
||||
import net.minecraftforge.eventbus.api.Event;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Event handler for pet play restrictions.
|
||||
*
|
||||
* When a player has a pet play collar (from EntityMaster):
|
||||
* - Cannot eat food from hand (must use Bowl block)
|
||||
* - Cannot sleep in normal beds (must use Pet Bed block)
|
||||
*
|
||||
* Placeholder blocks:
|
||||
* - Bowl: Cauldron (shift+right-click with food to eat)
|
||||
* - Pet Bed: White carpet (shift+right-click to sleep)
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class PetPlayRestrictionHandler {
|
||||
|
||||
/** Message cooldown in milliseconds */
|
||||
private static final long MESSAGE_COOLDOWN_MS = 3000;
|
||||
|
||||
/** Last message time per player */
|
||||
private static final java.util.Map<java.util.UUID, Long> lastMessageTime =
|
||||
new java.util.HashMap<>();
|
||||
|
||||
// ========================================
|
||||
// EATING RESTRICTION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Prevent pet play players from eating food from hand.
|
||||
* They must use a Bowl block (cauldron placeholder).
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickItem(
|
||||
PlayerInteractEvent.RightClickItem event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
ItemStack heldItem = event.getItemStack();
|
||||
|
||||
// Check if trying to eat food
|
||||
if (heldItem.isEdible()) {
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(
|
||||
net.minecraft.world.InteractionResult.FAIL
|
||||
);
|
||||
|
||||
sendThrottledMessage(
|
||||
player,
|
||||
"You cannot eat from your hand! Use a bowl."
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] Blocked {} from eating food directly",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow eating from Bowl block (cauldron placeholder).
|
||||
* Shift+right-click with food on cauldron to eat.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onRightClickBlock(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
BlockPos pos = event.getPos();
|
||||
BlockState state = player.level().getBlockState(pos);
|
||||
ItemStack heldItem = event.getItemStack();
|
||||
|
||||
// Bowl interaction (cauldron placeholder)
|
||||
if (
|
||||
state.getBlock() == Blocks.CAULDRON &&
|
||||
player.isShiftKeyDown() &&
|
||||
heldItem.isEdible()
|
||||
) {
|
||||
// Allow eating from bowl
|
||||
// Consume the food
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
net.minecraft.world.food.FoodProperties food = heldItem
|
||||
.getItem()
|
||||
.getFoodProperties();
|
||||
if (food != null) {
|
||||
serverPlayer
|
||||
.getFoodData()
|
||||
.eat(food.getNutrition(), food.getSaturationModifier());
|
||||
|
||||
// Shrink the item
|
||||
heldItem.shrink(1);
|
||||
|
||||
// Play eating sound
|
||||
player
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
pos,
|
||||
net.minecraft.sounds.SoundEvents.GENERIC_EAT,
|
||||
net.minecraft.sounds.SoundSource.PLAYERS,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] {} ate from bowl",
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
// Cancel to prevent normal cauldron interaction
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(
|
||||
net.minecraft.world.InteractionResult.SUCCESS
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pet bed interaction (carpet placeholder) - handled in sleep event
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SLEEPING RESTRICTION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Prevent pet play players from sleeping in normal beds.
|
||||
* They must use a Pet Bed block (carpet placeholder).
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.HIGH)
|
||||
public static void onSleepInBed(PlayerSleepInBedEvent event) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
// Block sleeping in beds
|
||||
BlockState state = player.level().getBlockState(event.getPos());
|
||||
if (state.getBlock() instanceof BedBlock) {
|
||||
event.setResult(Player.BedSleepingProblem.OTHER_PROBLEM);
|
||||
|
||||
sendThrottledMessage(
|
||||
player,
|
||||
"You cannot sleep in a bed! Use your pet bed."
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] Blocked {} from sleeping in bed",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow sleeping on Pet Bed block (carpet placeholder).
|
||||
* Shift+right-click on white carpet to sleep.
|
||||
*/
|
||||
@SubscribeEvent(priority = EventPriority.NORMAL)
|
||||
public static void onInteractForSleep(
|
||||
PlayerInteractEvent.RightClickBlock event
|
||||
) {
|
||||
Player player = event.getEntity();
|
||||
if (player.level().isClientSide) return;
|
||||
|
||||
// Check for pet play collar
|
||||
if (!EntityMaster.hasPetCollar(player)) return;
|
||||
|
||||
BlockPos pos = event.getPos();
|
||||
BlockState state = player.level().getBlockState(pos);
|
||||
|
||||
// Pet bed interaction (white carpet placeholder)
|
||||
if (
|
||||
state.getBlock() == Blocks.WHITE_CARPET && player.isShiftKeyDown()
|
||||
) {
|
||||
if (player instanceof ServerPlayer serverPlayer) {
|
||||
// Try to sleep
|
||||
Player.BedSleepingProblem problem = canSleepNow(serverPlayer);
|
||||
|
||||
if (problem == null) {
|
||||
// Set spawn point to pet bed location
|
||||
serverPlayer.setRespawnPosition(
|
||||
serverPlayer.level().dimension(),
|
||||
pos,
|
||||
serverPlayer.getYRot(),
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
// Start sleeping (simplified - real implementation would need proper sleep mechanics)
|
||||
serverPlayer.sendSystemMessage(
|
||||
Component.literal(
|
||||
"You curl up in your pet bed..."
|
||||
).withStyle(Style.EMPTY.withColor(0x888888))
|
||||
);
|
||||
|
||||
// Apply sleep effects (heal, skip night handled elsewhere)
|
||||
serverPlayer.heal(2.0f);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PetPlayRestrictionHandler] {} slept in pet bed",
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(
|
||||
net.minecraft.world.InteractionResult.SUCCESS
|
||||
);
|
||||
} else {
|
||||
sendThrottledMessage(
|
||||
player,
|
||||
"You can only sleep at night or during thunderstorms."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can sleep now (time/weather check).
|
||||
*/
|
||||
private static Player.BedSleepingProblem canSleepNow(ServerPlayer player) {
|
||||
// Check time of day
|
||||
if (player.level().isDay() && !player.level().isThundering()) {
|
||||
return Player.BedSleepingProblem.NOT_POSSIBLE_NOW;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CHOKE COLLAR EFFECT
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Apply choke collar effect on player tick.
|
||||
* When choking is active, rapidly reduces air supply to cause drowning damage.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
// Only process on server side, at END phase
|
||||
if (event.phase != TickEvent.Phase.END) return;
|
||||
if (event.player.level().isClientSide) return;
|
||||
if (!(event.player instanceof ServerPlayer player)) return;
|
||||
|
||||
// Check pet bed sit cancellation (movement detection)
|
||||
PetBedManager.tickPlayer(player);
|
||||
|
||||
// Check pet cage validity
|
||||
com.tiedup.remake.v2.blocks.PetCageManager.tickPlayer(player);
|
||||
|
||||
// Get player's collar
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(player);
|
||||
if (bindState == null || !bindState.hasCollar()) return;
|
||||
|
||||
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
|
||||
if (chokeCollar.isChoking(collar)) {
|
||||
// Apply ChokeEffect (short duration, re-applied each active tick)
|
||||
if (
|
||||
!player.hasEffect(
|
||||
com.tiedup.remake.core.ModEffects.CHOKE.get()
|
||||
)
|
||||
) {
|
||||
player.addEffect(
|
||||
new MobEffectInstance(
|
||||
com.tiedup.remake.core.ModEffects.CHOKE.get(),
|
||||
40,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Remove effect if choke is deactivated
|
||||
player.removeEffect(
|
||||
com.tiedup.remake.core.ModEffects.CHOKE.get()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UTILITY
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* BUG FIX: Clean up player data to prevent memory leak.
|
||||
* Called on player logout.
|
||||
*/
|
||||
public static void clearPlayer(java.util.UUID playerId) {
|
||||
lastMessageTime.remove(playerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message with cooldown to prevent spam.
|
||||
*/
|
||||
private static void sendThrottledMessage(Player player, String message) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long lastTime = lastMessageTime.get(player.getUUID());
|
||||
|
||||
if (lastTime == null || now - lastTime > MESSAGE_COOLDOWN_MS) {
|
||||
lastMessageTime.put(player.getUUID(), now);
|
||||
player.sendSystemMessage(
|
||||
Component.literal(message).withStyle(
|
||||
Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
package com.tiedup.remake.events.restriction;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.minigame.StruggleSessionManager;
|
||||
import com.tiedup.remake.network.ModNetwork;
|
||||
import com.tiedup.remake.network.personality.PacketSlaveBeingFreed;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.tasks.ForceFeedingTask;
|
||||
import com.tiedup.remake.tasks.TimedInteractTask;
|
||||
import com.tiedup.remake.tasks.UntyingPlayerTask;
|
||||
import com.tiedup.remake.tasks.UntyingTask;
|
||||
import com.tiedup.remake.util.GameConstants;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* Tick handler for restraint-related tasks (untying, tying, force feeding).
|
||||
*
|
||||
* Manages progress-based interaction tasks that span multiple ticks:
|
||||
* - Untying mechanic (empty hand right-click on tied entity)
|
||||
* - Tying mechanic (tick progression)
|
||||
* - Force feeding mechanic (food right-click on gagged entity)
|
||||
* - Auto-shock collar checks
|
||||
* - Struggle auto-stop (legacy QTE fallback)
|
||||
*
|
||||
* @see BondageItemRestrictionHandler for movement, interaction, and eating restrictions
|
||||
*/
|
||||
@Mod.EventBusSubscriber(
|
||||
modid = TiedUpMod.MOD_ID,
|
||||
bus = Mod.EventBusSubscriber.Bus.FORGE
|
||||
)
|
||||
public class RestraintTaskTickHandler {
|
||||
|
||||
// ========== PLAYER-SPECIFIC TICK ==========
|
||||
|
||||
/**
|
||||
* Handle player tick event for player-specific features.
|
||||
* - Auto-shock collar check (throttled to every N ticks)
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onPlayerTick(TickEvent.PlayerTickEvent event) {
|
||||
if (event.side.isClient() || event.phase != TickEvent.Phase.END) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = event.player;
|
||||
PlayerBindState playerState = PlayerBindState.getInstance(player);
|
||||
|
||||
// Check if struggle animation should stop
|
||||
// For continuous struggle: animation is managed by MiniGameSessionManager
|
||||
// Only auto-stop if NO active continuous session (legacy QTE fallback)
|
||||
if (playerState != null && playerState.isStruggling()) {
|
||||
// Don't auto-stop if there's an active continuous struggle session
|
||||
StruggleSessionManager mgr = StruggleSessionManager.getInstance();
|
||||
if (mgr.getContinuousStruggleSession(player.getUUID()) == null) {
|
||||
// Legacy behavior: stop after 80 ticks (no active continuous session)
|
||||
if (
|
||||
playerState.shouldStopStruggling(
|
||||
player.level().getGameTime()
|
||||
)
|
||||
) {
|
||||
playerState.setStruggling(false, 0);
|
||||
com.tiedup.remake.network.sync.SyncManager.syncStruggleState(
|
||||
player
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process untying task tick (progress-based system)
|
||||
// tick() increments/decrements progress based on whether update() was called this tick
|
||||
// sendProgressPackets() updates the UI for both players
|
||||
if (playerState != null) {
|
||||
com.tiedup.remake.tasks.UntyingTask currentUntyingTask =
|
||||
playerState.getCurrentUntyingTask();
|
||||
if (currentUntyingTask != null && !currentUntyingTask.isStopped()) {
|
||||
// AUTO-UPDATE: Check if player is still targeting the same entity
|
||||
// This allows "hold click" behavior without needing repeated interactLivingEntity calls
|
||||
if (
|
||||
currentUntyingTask instanceof
|
||||
com.tiedup.remake.tasks.UntyingPlayerTask untyingPlayerTask
|
||||
) {
|
||||
net.minecraft.world.entity.LivingEntity target =
|
||||
untyingPlayerTask.getTargetEntity();
|
||||
if (target != null && target.isAlive()) {
|
||||
// Check if player is looking at target and close enough
|
||||
double distance = player.distanceTo(target);
|
||||
boolean isLookingAtTarget = isPlayerLookingAtEntity(
|
||||
player,
|
||||
target,
|
||||
4.0
|
||||
);
|
||||
|
||||
if (
|
||||
distance <= 4.0 &&
|
||||
isLookingAtTarget &&
|
||||
player.hasLineOfSight(target)
|
||||
) {
|
||||
// Player is still targeting - auto-update the task
|
||||
currentUntyingTask.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process tick (increment if active, decrement if not)
|
||||
currentUntyingTask.tick();
|
||||
// Send progress packets to update UI
|
||||
currentUntyingTask.sendProgressPackets();
|
||||
|
||||
// Check if task stopped (completed or cancelled due to no progress)
|
||||
if (currentUntyingTask.isStopped()) {
|
||||
playerState.setCurrentUntyingTask(null);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} untying task ended (tick update)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process tying task tick (same progress-based system)
|
||||
com.tiedup.remake.tasks.TyingTask currentTyingTask =
|
||||
playerState.getCurrentTyingTask();
|
||||
if (currentTyingTask != null && !currentTyingTask.isStopped()) {
|
||||
// AUTO-UPDATE: Check if player is still targeting the same entity
|
||||
// This allows "hold click" behavior without needing repeated interactLivingEntity calls
|
||||
if (
|
||||
currentTyingTask instanceof
|
||||
com.tiedup.remake.tasks.TyingPlayerTask tyingPlayerTask
|
||||
) {
|
||||
net.minecraft.world.entity.LivingEntity target =
|
||||
tyingPlayerTask.getTargetEntity();
|
||||
boolean isSelfTying =
|
||||
target != null && target.equals(player);
|
||||
|
||||
if (isSelfTying) {
|
||||
// Self-tying: skip look-at/distance checks (player can't raycast to own hitbox)
|
||||
// Progress is driven by continuous PacketSelfBondage packets from client
|
||||
currentTyingTask.update();
|
||||
} else if (target != null && target.isAlive()) {
|
||||
// Tying another player: check distance + line of sight
|
||||
double distance = player.distanceTo(target);
|
||||
boolean isLookingAtTarget = isPlayerLookingAtEntity(
|
||||
player,
|
||||
target,
|
||||
4.0
|
||||
);
|
||||
|
||||
if (
|
||||
distance <= 4.0 &&
|
||||
isLookingAtTarget &&
|
||||
player.hasLineOfSight(target)
|
||||
) {
|
||||
currentTyingTask.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process tick (increment if active, decrement if not)
|
||||
currentTyingTask.tick();
|
||||
// Send progress packets to update UI
|
||||
currentTyingTask.sendProgressPackets();
|
||||
|
||||
// Check if task stopped (completed or cancelled due to no progress)
|
||||
if (currentTyingTask.isStopped()) {
|
||||
playerState.setCurrentTyingTask(null);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} tying task ended (tick update)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process force feeding task tick
|
||||
TimedInteractTask feedingTask = playerState.getCurrentFeedingTask();
|
||||
if (feedingTask != null && !feedingTask.isStopped()) {
|
||||
LivingEntity target = feedingTask.getTargetEntity();
|
||||
if (target != null && target.isAlive()) {
|
||||
double distance = player.distanceTo(target);
|
||||
boolean isLookingAtTarget = isPlayerLookingAtEntity(
|
||||
player,
|
||||
target,
|
||||
4.0
|
||||
);
|
||||
|
||||
if (
|
||||
distance <= 4.0 &&
|
||||
isLookingAtTarget &&
|
||||
player.hasLineOfSight(target)
|
||||
) {
|
||||
feedingTask.update();
|
||||
}
|
||||
}
|
||||
|
||||
feedingTask.tick();
|
||||
feedingTask.sendProgressPackets();
|
||||
|
||||
if (feedingTask.isStopped()) {
|
||||
playerState.setCurrentFeedingTask(null);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} feeding task ended (tick update)",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle: only check every N ticks (configurable via GameConstants) - per-player timing
|
||||
if (player.tickCount % GameConstants.SHOCK_COLLAR_CHECK_INTERVAL != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 13: Auto-shock collar logic (Player-specific feature)
|
||||
if (playerState != null) {
|
||||
playerState.checkAutoShockCollar();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UNTYING MECHANIC ==========
|
||||
|
||||
/**
|
||||
* Handle untying a tied entity (right-click with empty hand).
|
||||
*
|
||||
* Based on original PlayerKidnapActionsHandler.onUntyingTarget() (1.12.2)
|
||||
*
|
||||
* When a player right-clicks a tied entity (player or NPC) with an empty hand,
|
||||
* starts or continues an untying task to free them.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onUntyingTarget(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
// Only run on server side
|
||||
if (event.getLevel().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
Entity target = event.getTarget();
|
||||
Player helper = event.getEntity();
|
||||
|
||||
// Must be targeting a LivingEntity, using main hand, and have empty hand
|
||||
if (
|
||||
!(target instanceof LivingEntity targetEntity) ||
|
||||
event.getHand() != InteractionHand.MAIN_HAND ||
|
||||
!helper.getMainHandItem().isEmpty()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// MCA villagers require Shift+click to untie (prevents conflict with MCA menu)
|
||||
if (
|
||||
com.tiedup.remake.compat.mca.MCACompat.isMCALoaded() &&
|
||||
com.tiedup.remake.compat.mca.MCACompat.isMCAVillager(
|
||||
targetEntity
|
||||
) &&
|
||||
!helper.isShiftKeyDown()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if target is tied using IBondageState interface
|
||||
IBondageState targetState = KidnappedHelper.getKidnappedState(
|
||||
targetEntity
|
||||
);
|
||||
if (targetState == null || !targetState.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SECURITY: Distance and line-of-sight validation
|
||||
// ========================================
|
||||
double maxUntieDistance = 4.0; // Max distance to untie (blocks)
|
||||
double distance = helper.distanceTo(targetEntity);
|
||||
if (distance > maxUntieDistance) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[RESTRAINT] {} tried to untie {} from too far away ({} blocks)",
|
||||
helper.getName().getString(),
|
||||
targetEntity.getName().getString(),
|
||||
String.format("%.1f", distance)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check line-of-sight (helper must be able to see target)
|
||||
if (!helper.hasLineOfSight(targetEntity)) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[RESTRAINT] {} tried to untie {} without line of sight",
|
||||
helper.getName().getString(),
|
||||
targetEntity.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for Kidnapper fight back - block untying if Kidnapper is nearby
|
||||
if (targetEntity instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npc) {
|
||||
if (
|
||||
npc.getCaptor() instanceof EntityKidnapper kidnapper &&
|
||||
kidnapper.isAlive()
|
||||
) {
|
||||
double distanceToKidnapper = helper.distanceTo(kidnapper);
|
||||
double fightBackRange = 16.0; // Kidnapper notices within 16 blocks
|
||||
|
||||
if (distanceToKidnapper <= fightBackRange) {
|
||||
// Trigger Kidnapper fight back by setting helper as "attacker"
|
||||
// This activates KidnapperFightBackGoal which handles pursuit and attack
|
||||
kidnapper.setLastAttacker(helper);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} tried to untie {}, {} fights back!",
|
||||
helper.getName().getString(),
|
||||
npc.getName().getString(),
|
||||
kidnapper.getName().getString()
|
||||
);
|
||||
|
||||
// Block untying - send message to player
|
||||
helper.displayClientMessage(
|
||||
Component.translatable(
|
||||
"tiedup.message.kidnapper_guards_captive"
|
||||
),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Kidnapper fight back - block untying if player is captive or job worker
|
||||
if (targetEntity instanceof Player targetPlayer) {
|
||||
List<EntityKidnapper> nearbyKidnappers = helper
|
||||
.level()
|
||||
.getEntitiesOfClass(
|
||||
EntityKidnapper.class,
|
||||
helper.getBoundingBox().inflate(16.0)
|
||||
);
|
||||
|
||||
for (EntityKidnapper kidnapper : nearbyKidnappers) {
|
||||
if (!kidnapper.isAlive()) continue;
|
||||
|
||||
// Check if player is kidnapper's current captive (held by leash)
|
||||
IBondageState captive = kidnapper.getCaptive();
|
||||
boolean isCaptive =
|
||||
captive != null && captive.asLivingEntity() == targetPlayer;
|
||||
|
||||
// Check if player is kidnapper's job worker
|
||||
UUID workerUUID = kidnapper.getJobWorkerUUID();
|
||||
boolean isJobWorker =
|
||||
workerUUID != null &&
|
||||
workerUUID.equals(targetPlayer.getUUID());
|
||||
|
||||
if (isCaptive || isJobWorker) {
|
||||
// Trigger Kidnapper fight back
|
||||
kidnapper.setLastAttacker(helper);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} tried to untie {} (captive={}, worker={}), {} fights back!",
|
||||
helper.getName().getString(),
|
||||
targetPlayer.getName().getString(),
|
||||
isCaptive,
|
||||
isJobWorker,
|
||||
kidnapper.getNpcName()
|
||||
);
|
||||
|
||||
// Block untying - send message to player
|
||||
helper.displayClientMessage(
|
||||
Component.translatable(
|
||||
"tiedup.message.kidnapper_guards_captive"
|
||||
),
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if helper is tied using IBondageState interface
|
||||
IBondageState helperKidnappedState = KidnappedHelper.getKidnappedState(
|
||||
helper
|
||||
);
|
||||
if (helperKidnappedState == null || helperKidnappedState.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get PlayerBindState for task management (helper only)
|
||||
PlayerBindState helperState = PlayerBindState.getInstance(helper);
|
||||
if (helperState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block untying while force feeding
|
||||
TimedInteractTask activeFeedTask = helperState.getCurrentFeedingTask();
|
||||
if (activeFeedTask != null && !activeFeedTask.isStopped()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get untying duration (default: 10 seconds)
|
||||
int untyingSeconds = getUntyingDuration(helper);
|
||||
|
||||
// Phase 11: Check collar ownership for TiedUp NPCs
|
||||
// Non-owners take 3x longer and trigger alert to owners
|
||||
if (targetEntity instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npc) {
|
||||
if (!npc.isCollarOwner(helper)) {
|
||||
// Non-owner: triple the untying time
|
||||
untyingSeconds *= 3;
|
||||
|
||||
// Alert all collar owners
|
||||
alertCollarOwners(npc, helper);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] Non-owner {} trying to free {} ({}s)",
|
||||
helper.getName().getString(),
|
||||
npc.getNpcName(),
|
||||
untyingSeconds
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get current untying task (if any)
|
||||
UntyingTask currentTask = helperState.getCurrentUntyingTask();
|
||||
|
||||
// Check if we should start a new task or continue existing one
|
||||
if (
|
||||
currentTask == null ||
|
||||
!currentTask.isSameTarget(targetEntity) ||
|
||||
currentTask.isStopped()
|
||||
) {
|
||||
// Create new untying task (unified for Players and NPCs)
|
||||
UntyingPlayerTask newTask = new UntyingPlayerTask(
|
||||
targetState,
|
||||
targetEntity,
|
||||
untyingSeconds,
|
||||
helper.level(),
|
||||
helper
|
||||
);
|
||||
|
||||
// Start new task
|
||||
helperState.setCurrentUntyingTask(newTask);
|
||||
newTask.setUpTargetState(); // Initialize target's restraint state
|
||||
newTask.start();
|
||||
currentTask = newTask;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} started untying {} ({} seconds)",
|
||||
helper.getName().getString(),
|
||||
targetEntity.getName().getString(),
|
||||
untyingSeconds
|
||||
);
|
||||
} else {
|
||||
// Continue existing task - ensure helper is set
|
||||
if (currentTask instanceof UntyingPlayerTask playerTask) {
|
||||
playerTask.setHelper(helper);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this tick as active (progress will increase in onPlayerTick)
|
||||
// The tick() method in onPlayerTick handles progress increment/decrement
|
||||
currentTask.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player is looking at a specific entity (raycast).
|
||||
*
|
||||
* @param player The player
|
||||
* @param target The target entity
|
||||
* @param maxDistance Maximum distance to check
|
||||
* @return true if player is looking at the target entity
|
||||
*/
|
||||
private static boolean isPlayerLookingAtEntity(
|
||||
Player player,
|
||||
net.minecraft.world.entity.LivingEntity target,
|
||||
double maxDistance
|
||||
) {
|
||||
// Get player's look vector
|
||||
net.minecraft.world.phys.Vec3 eyePos = player.getEyePosition(1.0F);
|
||||
net.minecraft.world.phys.Vec3 lookVec = player.getLookAngle();
|
||||
net.minecraft.world.phys.Vec3 endPos = eyePos.add(
|
||||
lookVec.x * maxDistance,
|
||||
lookVec.y * maxDistance,
|
||||
lookVec.z * maxDistance
|
||||
);
|
||||
|
||||
// Check if raycast hits the target entity
|
||||
net.minecraft.world.phys.AABB targetBounds = target
|
||||
.getBoundingBox()
|
||||
.inflate(0.3);
|
||||
java.util.Optional<net.minecraft.world.phys.Vec3> hit =
|
||||
targetBounds.clip(eyePos, endPos);
|
||||
|
||||
return hit.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the untying duration in seconds from GameRule.
|
||||
*
|
||||
* @param player The player (for accessing world/GameRules)
|
||||
* @return Duration in seconds (default: 10)
|
||||
*/
|
||||
private static int getUntyingDuration(Player player) {
|
||||
return SettingsAccessor.getUntyingPlayerTime(player.level().getGameRules());
|
||||
}
|
||||
|
||||
// ========== FORCE FEEDING MECHANIC ==========
|
||||
|
||||
/**
|
||||
* Handle force feeding a gagged entity (right-click with food).
|
||||
*
|
||||
* When a player right-clicks a gagged entity (player or NPC) while holding food,
|
||||
* starts or continues a force feeding task.
|
||||
*/
|
||||
@SubscribeEvent
|
||||
public static void onForceFeedingTarget(
|
||||
PlayerInteractEvent.EntityInteract event
|
||||
) {
|
||||
if (event.getLevel().isClientSide) {
|
||||
return;
|
||||
}
|
||||
|
||||
Entity target = event.getTarget();
|
||||
Player feeder = event.getEntity();
|
||||
|
||||
if (
|
||||
!(target instanceof LivingEntity targetEntity) ||
|
||||
event.getHand() != InteractionHand.MAIN_HAND
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack heldItem = feeder.getMainHandItem();
|
||||
if (heldItem.isEmpty() || !heldItem.getItem().isEdible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Target must have IBondageState state and be gagged
|
||||
IBondageState targetState = KidnappedHelper.getKidnappedState(
|
||||
targetEntity
|
||||
);
|
||||
if (targetState == null || !targetState.isGagged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Feeder must not be tied up
|
||||
IBondageState feederState = KidnappedHelper.getKidnappedState(feeder);
|
||||
if (feederState != null && feederState.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Distance and line-of-sight validation
|
||||
double distance = feeder.distanceTo(targetEntity);
|
||||
if (distance > 4.0) {
|
||||
return;
|
||||
}
|
||||
if (!feeder.hasLineOfSight(targetEntity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get feeder's PlayerBindState for task management
|
||||
PlayerBindState feederBindState = PlayerBindState.getInstance(feeder);
|
||||
if (feederBindState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block feeding while untying
|
||||
UntyingTask activeUntieTask = feederBindState.getCurrentUntyingTask();
|
||||
if (activeUntieTask != null && !activeUntieTask.isStopped()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current feeding task (if any)
|
||||
TimedInteractTask currentTask = feederBindState.getCurrentFeedingTask();
|
||||
|
||||
if (
|
||||
currentTask == null ||
|
||||
!currentTask.isSameTarget(targetEntity) ||
|
||||
currentTask.isStopped()
|
||||
) {
|
||||
// Create new force feeding task (5 seconds)
|
||||
ForceFeedingTask newTask = new ForceFeedingTask(
|
||||
targetState,
|
||||
targetEntity,
|
||||
5,
|
||||
feeder.level(),
|
||||
feeder,
|
||||
heldItem,
|
||||
feeder.getInventory().selected
|
||||
);
|
||||
|
||||
feederBindState.setCurrentFeedingTask(newTask);
|
||||
newTask.setUpTargetState();
|
||||
newTask.start();
|
||||
currentTask = newTask;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[RESTRAINT] {} started force feeding {} (5 seconds)",
|
||||
feeder.getName().getString(),
|
||||
targetEntity.getName().getString()
|
||||
);
|
||||
} else {
|
||||
// Continue existing task - ensure feeder is set
|
||||
if (currentTask instanceof ForceFeedingTask feedTask) {
|
||||
feedTask.setFeeder(feeder);
|
||||
}
|
||||
}
|
||||
|
||||
currentTask.update();
|
||||
|
||||
// Cancel to prevent mobInteract (avoids instant NPC feed)
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
event.setCanceled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert all collar owners that someone is trying to free their slave.
|
||||
* Phase 11: Multiplayer protection system
|
||||
*
|
||||
* @param slave The slave being freed
|
||||
* @param liberator The player trying to free them
|
||||
*/
|
||||
private static void alertCollarOwners(
|
||||
com.tiedup.remake.entities.AbstractTiedUpNpc slave,
|
||||
Player liberator
|
||||
) {
|
||||
if (!(slave.level() instanceof ServerLevel serverLevel)) return;
|
||||
|
||||
ItemStack collar = slave.getEquipment(BodyRegionV2.NECK);
|
||||
if (
|
||||
collar.isEmpty() ||
|
||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<UUID> owners = collarItem.getOwners(collar);
|
||||
if (owners.isEmpty()) return;
|
||||
|
||||
// Create alert packet
|
||||
PacketSlaveBeingFreed alertPacket = new PacketSlaveBeingFreed(
|
||||
slave.getNpcName(),
|
||||
liberator.getName().getString(),
|
||||
slave.blockPosition().getX(),
|
||||
slave.blockPosition().getY(),
|
||||
slave.blockPosition().getZ()
|
||||
);
|
||||
|
||||
// Send to all online owners
|
||||
for (UUID ownerUUID : owners) {
|
||||
ServerPlayer owner = serverLevel
|
||||
.getServer()
|
||||
.getPlayerList()
|
||||
.getPlayer(ownerUUID);
|
||||
if (owner != null && owner != liberator) {
|
||||
ModNetwork.sendToPlayer(alertPacket, owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user