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:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,68 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.PoseType;
import net.minecraft.world.item.Item;
/**
* Generic bind item created from BindVariant enum.
* Replaces individual bind classes (ItemRopes, ItemChain, ItemStraitjacket, etc.)
*
* Factory pattern: All bind variants are created using this single class.
*/
public class GenericBind extends ItemBind {
private final BindVariant variant;
public GenericBind(BindVariant variant) {
super(new Item.Properties().stacksTo(16));
this.variant = variant;
}
@Override
public String getItemName() {
return variant.getItemName();
}
@Override
public PoseType getPoseType() {
return variant.getPoseType();
}
/**
* Get the variant this bind was created from.
*/
public BindVariant getVariant() {
return variant;
}
/**
* Get the default resistance value for this bind variant.
* Note: Actual resistance is managed by GameRules, this is just the configured default.
*/
public int getDefaultResistance() {
return variant.getResistance();
}
/**
* Check if this bind can have a padlock attached via anvil.
* Adhesive (tape) and organic (slime, vine, web) binds cannot have padlocks.
*/
@Override
public boolean canAttachPadlock() {
return switch (variant) {
case DUCT_TAPE, SLIME, VINE_SEED, WEB_BIND -> false;
default -> true;
};
}
/**
* Get the texture subfolder for this bind variant.
* Issue #12 fix: Eliminates string checks in renderers.
*/
@Override
public String getTextureSubfolder() {
return variant.getTextureSubfolder();
}
}

View File

@@ -0,0 +1,37 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.ItemBlindfold;
import net.minecraft.world.item.Item;
/**
* Generic blindfold item created from BlindfoldVariant enum.
* Replaces individual blindfold classes (ItemClassicBlindfold, ItemBlindfoldMask).
*
* Factory pattern: All blindfold variants are created using this single class.
*/
public class GenericBlindfold extends ItemBlindfold {
private final BlindfoldVariant variant;
public GenericBlindfold(BlindfoldVariant variant) {
super(new Item.Properties().stacksTo(16));
this.variant = variant;
}
/**
* Get the variant this blindfold was created from.
*/
public BlindfoldVariant getVariant() {
return variant;
}
/**
* Get the texture subfolder for this blindfold variant.
* Issue #12 fix: Eliminates string checks in renderers.
*/
@Override
public String getTextureSubfolder() {
return variant.getTextureSubfolder();
}
}

View File

@@ -0,0 +1,37 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.EarplugsVariant;
import com.tiedup.remake.items.base.ItemEarplugs;
import net.minecraft.world.item.Item;
/**
* Generic earplugs item created from EarplugsVariant enum.
* Replaces individual earplugs classes (ItemClassicEarplugs).
*
* Factory pattern: All earplugs variants are created using this single class.
*/
public class GenericEarplugs extends ItemEarplugs {
private final EarplugsVariant variant;
public GenericEarplugs(EarplugsVariant variant) {
super(new Item.Properties().stacksTo(16));
this.variant = variant;
}
/**
* Get the variant this earplugs was created from.
*/
public EarplugsVariant getVariant() {
return variant;
}
/**
* Get the texture subfolder for this earplugs variant.
* Issue #12 fix: Eliminates string checks in renderers.
*/
@Override
public String getTextureSubfolder() {
return variant.getTextureSubfolder();
}
}

View File

@@ -0,0 +1,72 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.ItemGag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import org.jetbrains.annotations.Nullable;
/**
* Generic gag item created from GagVariant enum.
* Replaces individual gag classes (ItemBallGag, ItemTapeGag, etc.)
*
* Factory pattern: All gag variants are created using this single class.
*
* Note: ItemMedicalGag is NOT handled by this class because it implements
* IHasBlindingEffect (combo item with special behavior).
*/
public class GenericGag extends ItemGag {
private final GagVariant variant;
public GenericGag(GagVariant variant) {
super(new Item.Properties().stacksTo(16), variant.getMaterial());
this.variant = variant;
}
/**
* Get the variant this gag was created from.
*/
public GagVariant getVariant() {
return variant;
}
/**
* Check if this gag can have a padlock attached via anvil.
* Adhesive (tape) and organic (slime, vine, web) gags cannot have padlocks.
*/
@Override
public boolean canAttachPadlock() {
return switch (variant) {
case TAPE_GAG, SLIME_GAG, VINE_GAG, WEB_GAG -> false;
default -> true;
};
}
/**
* Get the texture subfolder for this gag variant.
* Issue #12 fix: Eliminates string checks in renderers.
*/
@Override
public String getTextureSubfolder() {
return variant.getTextureSubfolder();
}
/**
* Check if this gag uses a 3D OBJ model.
*/
@Override
public boolean uses3DModel() {
return variant.uses3DModel();
}
/**
* Get the 3D model location for this gag.
*/
@Override
@Nullable
public ResourceLocation get3DModelLocation() {
String path = variant.getModelPath();
return path != null ? ResourceLocation.tryParse(path) : null;
}
}

View File

@@ -0,0 +1,504 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.IKnife;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.KnifeVariant;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.UseAnim;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Generic knife item created from KnifeVariant enum.
* Replaces individual knife classes (ItemStoneKnife, ItemIronKnife, ItemGoldenKnife).
*
* v2.5 Changes:
* - Added active cutting mechanic (hold right-click)
* - Per-tier cutting speed: Stone=5, Iron=8, Golden=12 resistance/second
* - Durability consumed per second = cutting speed (1 dura = 1 resistance)
* - Can cut binds directly or locked accessories
*/
public class GenericKnife extends Item implements IKnife {
private final KnifeVariant variant;
public GenericKnife(KnifeVariant variant) {
super(
new Item.Properties()
.stacksTo(1)
.durability(variant.getDurability())
);
this.variant = variant;
}
/**
* Get the variant this knife was created from.
*/
public KnifeVariant getVariant() {
return variant;
}
// ==================== TOOLTIP ====================
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
int remaining = stack.getMaxDamage() - stack.getDamageValue();
int speed = variant.getCuttingSpeed();
int cuttingSeconds = remaining / speed;
// Show cutting speed
tooltip.add(
Component.literal("Cutting speed: " + speed + " res/s").withStyle(
ChatFormatting.GRAY
)
);
// Show cutting time remaining
tooltip.add(
Component.literal(
"Cutting time: " +
cuttingSeconds +
"s (" +
remaining +
" total res)"
).withStyle(ChatFormatting.DARK_GRAY)
);
}
// ==================== USE MECHANICS ====================
@Override
public int getUseDuration(ItemStack stack) {
// Max use time: 5 minutes (very long, will stop naturally when bind breaks)
return 20 * 60 * 5;
}
@Override
public UseAnim getUseAnimation(ItemStack stack) {
return UseAnim.BOW; // Shows a "using" animation
}
/**
* Called when player right-clicks with knife.
* Starts cutting if:
* - Player is tied up (cuts bind)
* - Player has a knife cut target set (cuts accessory lock)
*/
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
// Only check on server side for actual state
if (!level.isClientSide) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return InteractionResultHolder.pass(stack);
}
// v2.5: Block knife usage if wearing mittens
if (state.hasMittens()) {
TiedUpMod.LOGGER.debug(
"[GenericKnife] {} cannot use knife - wearing mittens",
player.getName().getString()
);
return InteractionResultHolder.fail(stack);
}
// Priority 1: If tied up, cut the bind
if (state.isTiedUp()) {
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
// Priority 2: If accessory target selected (via StruggleChoiceScreen)
if (state.getKnifeCutTarget() != null) {
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
// Priority 3: If wearing a collar (not tied), auto-target the collar
if (state.hasCollar()) {
state.setKnifeCutTarget(BodyRegionV2.NECK);
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
// Priority 4: Check other accessories (gag, blindfold, etc.)
if (state.isGagged()) {
state.setKnifeCutTarget(BodyRegionV2.MOUTH);
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
if (state.isBlindfolded()) {
state.setKnifeCutTarget(BodyRegionV2.EYES);
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
if (state.hasEarplugs()) {
state.setKnifeCutTarget(BodyRegionV2.EARS);
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
// Note: Don't auto-target mittens since you need hands to use knife
// Nothing to cut
return InteractionResultHolder.pass(stack);
}
// Client side - check mittens and allow use if valid target or has accessories
PlayerBindState state = PlayerBindState.getInstance(player);
if (
state != null &&
!state.hasMittens() &&
(state.isTiedUp() ||
state.getKnifeCutTarget() != null ||
state.hasCollar() ||
state.isGagged() ||
state.isBlindfolded() ||
state.hasEarplugs())
) {
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
return InteractionResultHolder.pass(stack);
}
/**
* Called every tick while player holds right-click.
* Performs the actual cutting logic.
*/
@Override
public void onUseTick(
Level level,
LivingEntity entity,
ItemStack stack,
int remainingTicks
) {
if (level.isClientSide || !(entity instanceof ServerPlayer player)) {
return;
}
// Calculate how many ticks have been used
int usedTicks = getUseDuration(stack) - remainingTicks;
// Only process every 20 ticks (1 second)
if (usedTicks > 0 && usedTicks % 20 == 0) {
performCutTick(player, stack);
}
}
/**
* Called when player releases right-click or item breaks.
*/
@Override
public void releaseUsing(
ItemStack stack,
Level level,
LivingEntity entity,
int remainingTicks
) {
if (!level.isClientSide && entity instanceof ServerPlayer player) {
TiedUpMod.LOGGER.debug(
"[GenericKnife] {} stopped cutting",
player.getName().getString()
);
}
}
/**
* Perform one "tick" of cutting (called every second while held).
* Consumes durability and removes resistance based on variant's cutting speed.
*/
private void performCutTick(ServerPlayer player, ItemStack stack) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
player.stopUsingItem();
return;
}
int speed = variant.getCuttingSpeed();
// Determine what to cut
if (state.isTiedUp()) {
// Cut BIND
cutBind(player, state, stack, speed);
} else if (state.getKnifeCutTarget() != null) {
// Cut ACCESSORY
cutAccessory(player, state, stack, speed);
} else {
// Nothing to cut
player.stopUsingItem();
return;
}
// Play cutting sound
player
.level()
.playSound(
null,
player.blockPosition(),
SoundEvents.SHEEP_SHEAR,
SoundSource.PLAYERS,
0.5f,
1.2f
);
// Consume durability equal to cutting speed
stack.hurtAndBreak(speed, player, p ->
p.broadcastBreakEvent(p.getUsedItemHand())
);
// Notify nearby guards (Kidnappers, Maids, Traders) about cutting noise
com.tiedup.remake.minigame.GuardNotificationHelper.notifyNearbyGuards(
player
);
// Force inventory sync so durability bar updates in real-time
player.inventoryMenu.broadcastChanges();
// Sync state to clients
SyncManager.syncBindState(player);
}
/**
* Cut the bind directly.
*/
private void cutBind(
ServerPlayer player,
PlayerBindState state,
ItemStack knifeStack,
int speed
) {
// Get bind stack for ILockable check
ItemStack bindStack = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.ARMS
);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
player.stopUsingItem();
return;
}
// Reduce resistance by cutting speed
int currentRes = state.getCurrentBindResistance();
int newRes = Math.max(0, currentRes - speed);
state.setCurrentBindResistance(newRes);
TiedUpMod.LOGGER.debug(
"[GenericKnife] {} cutting bind: resistance {} -> {}",
player.getName().getString(),
currentRes,
newRes
);
// Check if escaped
if (newRes <= 0) {
state.getStruggleBinds().successActionExternal(state);
player.stopUsingItem();
TiedUpMod.LOGGER.info(
"[GenericKnife] {} escaped by cutting bind!",
player.getName().getString()
);
}
}
/**
* Cut an accessory - either removes lock resistance (if locked) or removes the item directly (if unlocked).
*/
private void cutAccessory(
ServerPlayer player,
PlayerBindState state,
ItemStack knifeStack,
int speed
) {
BodyRegionV2 target = state.getKnifeCutTarget();
if (target == null) {
player.stopUsingItem();
return;
}
ItemStack accessory = V2EquipmentHelper.getInRegion(
player,
target
);
if (accessory.isEmpty()) {
// Target doesn't exist
state.clearKnifeCutTarget();
player.stopUsingItem();
return;
}
// Check if the accessory is locked
boolean isLocked = false;
if (accessory.getItem() instanceof ILockable lockable) {
isLocked = lockable.isLocked(accessory);
}
if (!isLocked) {
// NOT locked - directly cut and remove the accessory
IBondageState kidnapped = KidnappedHelper.getKidnappedState(player);
if (kidnapped != null) {
ItemStack removed = removeAccessory(kidnapped, target);
if (!removed.isEmpty()) {
// Drop the removed accessory
kidnapped.kidnappedDropItem(removed);
TiedUpMod.LOGGER.info(
"[GenericKnife] {} cut off unlocked {}",
player.getName().getString(),
target
);
}
}
state.clearKnifeCutTarget();
player.stopUsingItem();
return;
}
// Accessory IS locked - reduce lock resistance
ILockable lockable = (ILockable) accessory.getItem();
int currentRes = lockable.getCurrentLockResistance(accessory);
int newRes = Math.max(0, currentRes - speed);
lockable.setCurrentLockResistance(accessory, newRes);
TiedUpMod.LOGGER.debug(
"[GenericKnife] {} cutting {} lock: resistance {} -> {}",
player.getName().getString(),
target,
currentRes,
newRes
);
// Check if lock is destroyed
if (newRes <= 0) {
// Destroy the lock (remove padlock, clear lock state)
lockable.setLockedByKeyUUID(accessory, null); // Unlocks and clears locked state
lockable.setLockable(accessory, false); // Remove padlock entirely
lockable.clearLockResistance(accessory);
lockable.setJammed(accessory, false);
state.clearKnifeCutTarget();
player.stopUsingItem();
TiedUpMod.LOGGER.info(
"[GenericKnife] {} cut through {} lock!",
player.getName().getString(),
target
);
}
}
/**
* Remove an accessory from the player and return it.
*/
private ItemStack removeAccessory(
IBondageState kidnapped,
BodyRegionV2 target
) {
switch (target) {
case NECK -> {
ItemStack collar = kidnapped.getEquipment(BodyRegionV2.NECK);
if (collar != null && !collar.isEmpty()) {
kidnapped.unequip(BodyRegionV2.NECK);
return collar;
}
}
case MOUTH -> {
ItemStack gag = kidnapped.getEquipment(BodyRegionV2.MOUTH);
if (gag != null && !gag.isEmpty()) {
kidnapped.unequip(BodyRegionV2.MOUTH);
return gag;
}
}
case EYES -> {
ItemStack blindfold = kidnapped.getEquipment(BodyRegionV2.EYES);
if (blindfold != null && !blindfold.isEmpty()) {
kidnapped.unequip(BodyRegionV2.EYES);
return blindfold;
}
}
case EARS -> {
ItemStack earplugs = kidnapped.getEquipment(BodyRegionV2.EARS);
if (earplugs != null && !earplugs.isEmpty()) {
kidnapped.unequip(BodyRegionV2.EARS);
return earplugs;
}
}
case HANDS -> {
ItemStack mittens = kidnapped.getEquipment(BodyRegionV2.HANDS);
if (mittens != null && !mittens.isEmpty()) {
kidnapped.unequip(BodyRegionV2.HANDS);
return mittens;
}
}
default -> {
}
}
return ItemStack.EMPTY;
}
/**
* Find a knife in the player's inventory.
*
* @param player The player to search
* @return The knife ItemStack, or empty if not found
*/
public static ItemStack findKnifeInInventory(Player player) {
// Check main hand first
ItemStack mainHand = player.getMainHandItem();
if (mainHand.getItem() instanceof IKnife) {
return mainHand;
}
// Check off hand
ItemStack offHand = player.getOffhandItem();
if (offHand.getItem() instanceof IKnife) {
return offHand;
}
// Check inventory
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
ItemStack stack = player.getInventory().getItem(i);
if (stack.getItem() instanceof IKnife) {
return stack;
}
}
return ItemStack.EMPTY;
}
}

View File

@@ -0,0 +1,38 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.ItemMittens;
import com.tiedup.remake.items.base.MittensVariant;
import net.minecraft.world.item.Item;
/**
* Generic mittens item created from MittensVariant enum.
*
* Factory pattern: All mittens variants are created using this single class.
*
* Phase 14.4: Mittens system - blocks hand interactions when equipped.
*/
public class GenericMittens extends ItemMittens {
private final MittensVariant variant;
public GenericMittens(MittensVariant variant) {
super(new Item.Properties().stacksTo(16));
this.variant = variant;
}
/**
* Get the variant this mittens was created from.
*/
public MittensVariant getVariant() {
return variant;
}
/**
* Get the texture subfolder for this mittens variant.
* Issue #12 fix: Eliminates string checks in renderers.
*/
@Override
public String getTextureSubfolder() {
return variant.getTextureSubfolder();
}
}

View File

@@ -0,0 +1,768 @@
package com.tiedup.remake.items;
import com.tiedup.remake.blocks.BlockCellCore;
import com.tiedup.remake.blocks.BlockMarker;
import com.tiedup.remake.blocks.ModBlocks;
import com.tiedup.remake.blocks.entity.CellCoreBlockEntity;
import com.tiedup.remake.blocks.entity.MarkerBlockEntity;
import com.tiedup.remake.cells.*;
import com.tiedup.remake.core.SystemMessageManager;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
/**
* Admin Wand - Structure marker placement and Cell Core management.
*
* Features:
* - Right-click CellCore: rescan cell (flood-fill)
* - Shift+Right-click CellCore: display cell info
* - Right-click elsewhere: cycle structure marker type
* - Left-click: place/remove/info structure markers
*/
public class ItemAdminWand extends Item {
private static final String TAG_ACTIVE_CELL_ID = "ActiveCellId";
private static final String TAG_CURRENT_TYPE = "CurrentType";
private static final String TAG_WAYPOINT_MODE = "WaypointMode";
public ItemAdminWand() {
super(new Item.Properties().stacksTo(1));
}
// ==================== NBT ACCESS ====================
@Nullable
public static UUID getActiveCellId(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().contains(TAG_ACTIVE_CELL_ID)) {
return null;
}
return stack.getTag().getUUID(TAG_ACTIVE_CELL_ID);
}
public static void setActiveCellId(ItemStack stack, @Nullable UUID cellId) {
if (cellId != null) {
stack.getOrCreateTag().putUUID(TAG_ACTIVE_CELL_ID, cellId);
} else if (stack.hasTag()) {
stack.getTag().remove(TAG_ACTIVE_CELL_ID);
}
}
public static MarkerType getCurrentType(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().contains(TAG_CURRENT_TYPE)) {
return MarkerType.ENTRANCE; // Default to structure marker
}
return MarkerType.fromString(
stack.getTag().getString(TAG_CURRENT_TYPE)
);
}
public static void setCurrentType(ItemStack stack, MarkerType type) {
stack
.getOrCreateTag()
.putString(TAG_CURRENT_TYPE, type.getSerializedName());
}
// ==================== WAYPOINT MODE ====================
public static boolean isInWaypointMode(ItemStack stack) {
return stack.hasTag() && stack.getTag().getBoolean(TAG_WAYPOINT_MODE);
}
private static void enterWaypointMode(ItemStack stack, UUID cellId) {
stack.getOrCreateTag().putBoolean(TAG_WAYPOINT_MODE, true);
setActiveCellId(stack, cellId);
// Clear any previous waypoints
stack.getTag().remove("Waypoints");
}
private static void exitWaypointMode(ItemStack stack) {
if (stack.hasTag()) {
stack.getTag().remove(TAG_WAYPOINT_MODE);
stack.getTag().remove("Waypoints");
setActiveCellId(stack, null);
}
}
private static void addWaypointToStack(ItemStack stack, BlockPos pos) {
CompoundTag tag = stack.getOrCreateTag();
ListTag list = tag.contains("Waypoints")
? tag.getList("Waypoints", Tag.TAG_COMPOUND)
: new ListTag();
CompoundTag wp = new CompoundTag();
wp.putInt("X", pos.getX());
wp.putInt("Y", pos.getY());
wp.putInt("Z", pos.getZ());
list.add(wp);
tag.put("Waypoints", list);
}
private static List<BlockPos> getWaypointsFromStack(ItemStack stack) {
List<BlockPos> result = new ArrayList<>();
if (
!stack.hasTag() || !stack.getTag().contains("Waypoints")
) return result;
ListTag list = stack.getTag().getList("Waypoints", Tag.TAG_COMPOUND);
for (int i = 0; i < list.size(); i++) {
CompoundTag wp = list.getCompound(i);
result.add(
new BlockPos(wp.getInt("X"), wp.getInt("Y"), wp.getInt("Z"))
);
}
return result;
}
private static void removeLastWaypointFromStack(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().contains("Waypoints")) return;
ListTag list = stack.getTag().getList("Waypoints", Tag.TAG_COMPOUND);
if (!list.isEmpty()) list.remove(list.size() - 1);
}
private void saveWaypoints(
ItemStack stack,
Player player,
ServerLevel level
) {
UUID cellId = getActiveCellId(stack);
if (cellId == null) return;
CellRegistryV2 registry = CellRegistryV2.get(level);
CellDataV2 cell = registry.getCell(cellId);
if (cell == null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Cell not found"
);
return;
}
List<BlockPos> waypoints = getWaypointsFromStack(stack);
cell.setPathWaypoints(waypoints);
registry.setDirty();
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
waypoints.size() +
" waypoints saved to cell " +
cellId.toString().substring(0, 8)
);
}
// ==================== BLOCK INTERACTION ====================
@Override
public InteractionResult useOn(UseOnContext context) {
Level level = context.getLevel();
Player player = context.getPlayer();
ItemStack stack = context.getItemInHand();
BlockPos pos = context.getClickedPos();
if (player == null) return InteractionResult.PASS;
// Check if clicked block is a Cell Core
BlockState clickedState = level.getBlockState(pos);
if (clickedState.getBlock() instanceof BlockCellCore) {
if (
!level.isClientSide && level instanceof ServerLevel serverLevel
) {
if (player.isShiftKeyDown()) {
if (isInWaypointMode(stack)) {
// Save waypoints and exit waypoint mode
saveWaypoints(stack, player, serverLevel);
exitWaypointMode(stack);
} else {
// Enter waypoint mode if cell core has a cell
BlockEntity be = level.getBlockEntity(pos);
if (
be instanceof CellCoreBlockEntity coreBE &&
coreBE.getCellId() != null
) {
enterWaypointMode(stack, coreBE.getCellId());
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Waypoint mode: click blocks to add waypoints, Shift+RC Cell Core to save"
);
} else {
showCellCoreInfo(player, serverLevel, pos);
}
}
} else {
rescanCellCore(player, serverLevel, pos);
}
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
// Right-click on block: cycle marker type
return cycleMarkerType(stack, player, level);
}
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
if (player.isShiftKeyDown()) {
if (
!level.isClientSide && level instanceof ServerLevel serverLevel
) {
if (isInWaypointMode(stack)) {
// Shift+Right-click in air while in waypoint mode: cancel
exitWaypointMode(stack);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.WARNING,
"Waypoint edit cancelled"
);
} else {
// Shift+Right-click in air: delete selected cell
deleteSelectedCell(stack, player, serverLevel);
}
}
} else {
// Right-click in air: cycle marker type
cycleMarkerType(stack, player, level);
}
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
}
// ==================== CELL CORE ACTIONS ====================
/**
* Right-click Cell Core: rescan cell via flood-fill.
*/
private void rescanCellCore(
Player player,
ServerLevel level,
BlockPos corePos
) {
BlockEntity be = level.getBlockEntity(corePos);
if (!(be instanceof CellCoreBlockEntity coreBE)) return;
FloodFillResult result = FloodFillAlgorithm.tryFill(level, corePos);
if (!result.isSuccess()) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Rescan failed: " +
(result.getErrorKey() != null
? result.getErrorKey()
: "unknown error")
);
return;
}
CellRegistryV2 registry = CellRegistryV2.get(level);
UUID cellId = coreBE.getCellId();
if (cellId != null) {
CellDataV2 existing = registry.getCell(cellId);
if (existing != null) {
registry.rescanCell(cellId, result);
// Update interior face on block entity
if (result.getInteriorFace() != null) {
coreBE.setInteriorFace(result.getInteriorFace());
}
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Cell rescanned: " +
result.getInterior().size() +
" interior, " +
result.getWalls().size() +
" walls, " +
result.getBeds().size() +
" beds, " +
result.getAnchors().size() +
" anchors, " +
result.getDoors().size() +
" doors"
);
return;
}
}
// No existing cell — create new one
CellDataV2 newCell = registry.createCell(corePos, result, null);
coreBE.setCellId(newCell.getId());
if (result.getInteriorFace() != null) {
coreBE.setInteriorFace(result.getInteriorFace());
}
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"New cell created: " +
result.getInterior().size() +
" interior, " +
result.getWalls().size() +
" walls"
);
}
/**
* Shift+Right-click Cell Core: display cell info.
*/
private void showCellCoreInfo(
Player player,
ServerLevel level,
BlockPos corePos
) {
BlockEntity be = level.getBlockEntity(corePos);
if (!(be instanceof CellCoreBlockEntity coreBE)) return;
UUID cellId = coreBE.getCellId();
if (cellId == null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.WARNING,
"Cell Core has no linked cell (right-click to scan)"
);
return;
}
CellRegistryV2 registry = CellRegistryV2.get(level);
CellDataV2 cell = registry.getCell(cellId);
if (cell == null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.WARNING,
"Cell " +
cellId.toString().substring(0, 8) +
"... not found in registry"
);
return;
}
// Build info message
StringBuilder info = new StringBuilder();
info.append("--- Cell Info ---\n");
info
.append("ID: ")
.append(cellId.toString().substring(0, 8))
.append("...\n");
if (cell.getName() != null) {
info.append("Name: ").append(cell.getName()).append("\n");
}
info
.append("State: ")
.append(cell.getState().getSerializedName())
.append("\n");
info
.append("Volume: ")
.append(cell.getInteriorBlocks().size())
.append(" blocks\n");
info.append("Walls: ").append(cell.getWallBlocks().size());
if (!cell.getBreachedPositions().isEmpty()) {
info
.append(" (")
.append(cell.getBreachedPositions().size())
.append(" breached)");
}
info.append("\n");
info
.append("Beds: ")
.append(cell.getBeds().size())
.append(", Anchors: ")
.append(cell.getAnchors().size())
.append(", Doors: ")
.append(cell.getDoors().size())
.append("\n");
info
.append("Prisoners: ")
.append(cell.getPrisonerCount())
.append("/4\n");
if (cell.getOwnerId() != null) {
info
.append("Owner: ")
.append(cell.getOwnerId().toString().substring(0, 8))
.append("... (")
.append(cell.getOwnerType().getSerializedName())
.append(")");
} else {
info.append("Owner: none");
}
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
info.toString()
);
}
// ==================== ATTACK (LEFT-CLICK) ====================
@Override
public boolean onBlockStartBreak(
ItemStack stack,
BlockPos pos,
Player player
) {
Level level = player.level();
BlockState state = level.getBlockState(pos);
// Left-click on marker: show info about the marker (always, even in waypoint mode)
if (
state.getBlock() instanceof BlockMarker && !isInWaypointMode(stack)
) {
if (!level.isClientSide) {
showMarkerInfo(stack, player, level, pos);
}
return true;
}
if (isInWaypointMode(stack)) {
if (!level.isClientSide) {
if (player.isShiftKeyDown()) {
// Shift+Left-click in waypoint mode: remove last waypoint
removeLastWaypointFromStack(stack);
int remaining = getWaypointsFromStack(stack).size();
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Last waypoint removed (" + remaining + " remaining)"
);
} else {
// Left-click in waypoint mode: add waypoint
BlockPos waypointPos = pos.above();
addWaypointToStack(stack, waypointPos);
int count = getWaypointsFromStack(stack).size();
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Waypoint " +
count +
" added at " +
waypointPos.toShortString()
);
}
}
return true;
}
// Shift + Left-click: remove marker above this block (if exists)
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
removeStructureMarker(stack, player, (ServerLevel) level, pos);
}
return true;
}
// Left-click on block: place structure marker above it
if (!level.isClientSide) {
placeStructureMarker(stack, player, (ServerLevel) level, pos);
}
return true;
}
/**
* Place a structure marker (no cell needed).
*/
private void placeStructureMarker(
ItemStack stack,
Player player,
ServerLevel level,
BlockPos pos
) {
MarkerType type = getCurrentType(stack);
// Structure markers don't need a cell
BlockPos markerPos = pos.above();
BlockState currentState = level.getBlockState(markerPos);
if (
!currentState.isAir() &&
!(currentState.getBlock() instanceof BlockMarker)
) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Cannot place marker - block is occupied"
);
return;
}
// Place the marker block
level.setBlock(
markerPos,
ModBlocks.MARKER.get().defaultBlockState(),
3
);
// Set the marker type (no cell ID for structure markers)
BlockEntity be = level.getBlockEntity(markerPos);
if (be instanceof MarkerBlockEntity marker) {
marker.setMarkerType(type);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
type.name() + " marker placed at " + markerPos.toShortString()
);
}
}
/**
* Show info about a marker block.
*/
private void showMarkerInfo(
ItemStack stack,
Player player,
Level level,
BlockPos pos
) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof MarkerBlockEntity marker) {
MarkerType type = marker.getMarkerType();
UUID cellId = marker.getCellId();
if (cellId != null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Cell marker: " +
type.name() +
" (Cell: " +
cellId.toString().substring(0, 8) +
"...)"
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Structure marker: " + type.name() + " (no cell)"
);
}
}
}
/**
* Remove a structure marker above the clicked position.
*/
private void removeStructureMarker(
ItemStack stack,
Player player,
ServerLevel level,
BlockPos pos
) {
BlockPos markerPos = pos.above();
BlockState state = level.getBlockState(markerPos);
if (state.getBlock() instanceof BlockMarker) {
BlockEntity be = level.getBlockEntity(markerPos);
if (be instanceof MarkerBlockEntity marker) {
// Only remove if it's a structure marker (no cell ID)
if (marker.getCellId() == null) {
level.destroyBlock(markerPos, false);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Structure marker removed"
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"This is a cell marker - use Cell Wand to manage cells"
);
}
}
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.WARNING,
"No marker above this position"
);
}
}
// ==================== ACTIONS ====================
/**
* Cycle through STRUCTURE marker types (ENTRANCE, PATROL, LOOT, SPAWNER, ...).
*/
private InteractionResult cycleMarkerType(
ItemStack stack,
Player player,
Level level
) {
if (!level.isClientSide) {
MarkerType current = getCurrentType(stack);
MarkerType next = current.nextStructureType();
setCurrentType(stack, next);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Structure marker: " + next.name()
);
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
private void deleteSelectedCell(
ItemStack stack,
Player player,
ServerLevel level
) {
UUID cellId = getActiveCellId(stack);
if (cellId == null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"No cell selected"
);
return;
}
CellRegistryV2 registry = CellRegistryV2.get(level);
CellDataV2 cell = registry.getCell(cellId);
if (cell == null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Cell not found"
);
setActiveCellId(stack, null);
return;
}
BlockPos spawnPoint = cell.getSpawnPoint();
BlockState state = level.getBlockState(spawnPoint);
if (state.getBlock() instanceof BlockMarker) {
level.destroyBlock(spawnPoint, false);
}
setActiveCellId(stack, null);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Cell deleted"
);
}
// ==================== TOOLTIP ====================
private ChatFormatting getTypeColor(MarkerType type) {
return switch (type) {
case WALL -> ChatFormatting.BLUE;
case ANCHOR -> ChatFormatting.RED;
case BED -> ChatFormatting.LIGHT_PURPLE;
case DOOR -> ChatFormatting.AQUA;
case DELIVERY -> ChatFormatting.YELLOW;
case ENTRANCE -> ChatFormatting.GREEN;
case PATROL -> ChatFormatting.YELLOW;
case LOOT -> ChatFormatting.GOLD;
case SPAWNER -> ChatFormatting.DARK_RED;
case TRADER_SPAWN -> ChatFormatting.LIGHT_PURPLE;
case MAID_SPAWN -> ChatFormatting.LIGHT_PURPLE;
case MERCHANT_SPAWN -> ChatFormatting.AQUA;
};
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
tooltip.add(
Component.literal("ADMIN WAND").withStyle(
ChatFormatting.LIGHT_PURPLE,
ChatFormatting.BOLD
)
);
tooltip.add(
Component.literal(
"Structure markers + Cell Core management"
).withStyle(ChatFormatting.GRAY)
);
MarkerType type = getCurrentType(stack);
tooltip.add(
Component.literal("Type: " + type.name()).withStyle(
getTypeColor(type)
)
);
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Left-click: Place marker").withStyle(
ChatFormatting.YELLOW
)
);
tooltip.add(
Component.literal("Shift+Left-click: Remove marker").withStyle(
ChatFormatting.RED
)
);
tooltip.add(
Component.literal("Left-click on marker: Show info").withStyle(
ChatFormatting.DARK_GRAY
)
);
tooltip.add(
Component.literal("Right-click: Cycle type").withStyle(
ChatFormatting.DARK_GRAY
)
);
tooltip.add(
Component.literal("Right-click Cell Core: Rescan cell").withStyle(
ChatFormatting.AQUA
)
);
tooltip.add(
Component.literal(
"Shift+Right-click Cell Core: Cell info / Waypoint mode"
).withStyle(ChatFormatting.AQUA)
);
if (isInWaypointMode(stack)) {
int count = getWaypointsFromStack(stack).size();
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal(
"WAYPOINT MODE (" + count + " points)"
).withStyle(ChatFormatting.YELLOW, ChatFormatting.BOLD)
);
}
}
@Override
public boolean isFoil(ItemStack stack) {
return true; // Always glowing to indicate admin item
}
}

View File

@@ -0,0 +1,55 @@
package com.tiedup.remake.items;
import java.util.List;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
/**
* Cell Key - Universal key for iron bar doors.
*
* Phase: Kidnapper Revamp - Cell System
*
* Unlike regular keys which have UUIDs and can only unlock matching locks,
* the Cell Key can unlock any iron bar door regardless of which key locked it.
*
* This is a convenience item for kidnappers to manage their cells without
* needing to track individual keys.
*
* Does NOT unlock regular bondage items (collars, cuffs, etc.) - only iron bar doors.
*/
public class ItemCellKey extends Item {
public ItemCellKey() {
super(new Item.Properties().stacksTo(1));
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
tooltip.add(
Component.literal("Unlocks any Iron Bar Door").withStyle(
ChatFormatting.GRAY
)
);
tooltip.add(
Component.literal("Does not work on bondage items").withStyle(
ChatFormatting.DARK_GRAY
)
);
}
@Override
public boolean isFoil(ItemStack stack) {
// Slight shimmer to indicate it's a special key
return true;
}
}

View File

@@ -0,0 +1,110 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Chloroform Bottle - Used to soak rags for knocking out targets
* Has limited durability.
*
* Phase 15: Full chloroform system implementation
*
* Usage:
* - Hold chloroform bottle in main hand, rag in offhand
* - Right-click to soak the rag with chloroform
* - Consumes 1 durability from the bottle
*/
public class ItemChloroformBottle extends Item {
public ItemChloroformBottle() {
super(
new Item.Properties().stacksTo(1).durability(9) // Default durability (ModConfig not loaded yet during item registration)
);
}
/**
* Called when player right-clicks with the chloroform bottle.
* If holding a rag in offhand, soaks it with chloroform.
*/
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack bottle = player.getItemInHand(hand);
// Only works from main hand
if (hand != InteractionHand.MAIN_HAND) {
return InteractionResultHolder.pass(bottle);
}
// Check for rag in offhand
ItemStack offhandItem = player.getItemInHand(InteractionHand.OFF_HAND);
if (
offhandItem.isEmpty() || !(offhandItem.getItem() instanceof ItemRag)
) {
if (!level.isClientSide) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Hold a rag in your offhand to soak it"
);
}
return InteractionResultHolder.pass(bottle);
}
// Server side only
if (!level.isClientSide) {
// Check if rag is already wet
if (ItemRag.isWet(offhandItem)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"The rag is already soaked with chloroform"
);
return InteractionResultHolder.pass(bottle);
}
// Soak the rag
ItemRag.soak(offhandItem, ItemRag.getDefaultWetTime());
// Damage the bottle (consume 1 use)
bottle.hurtAndBreak(1, player, p -> {
p.broadcastBreakEvent(InteractionHand.MAIN_HAND);
});
// Play bottle pour sound
level.playSound(
null,
player.blockPosition(),
SoundEvents.BOTTLE_EMPTY,
SoundSource.PLAYERS,
1.0f,
1.0f
);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.RAG_SOAKED
);
TiedUpMod.LOGGER.info(
"[ItemChloroformBottle] {} soaked rag with chloroform",
player.getName().getString()
);
}
return InteractionResultHolder.success(bottle);
}
}

View File

@@ -0,0 +1,156 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.bondage3d.IHas3DModelConfig;
import com.tiedup.remake.items.bondage3d.Model3DConfig;
import java.util.List;
import java.util.Set;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Choke Collar - Pet play collar used by Masters.
*
* <p>Special feature: Can be put in "choke mode" which applies a drowning effect.</p>
* <p>Used by Masters for punishment. The effect simulates choking by reducing air supply,
* which triggers drowning damage if left active for too long.</p>
*
* <p><b>Mechanics:</b></p>
* <ul>
* <li>When choking is active, the wearer's air supply decreases rapidly</li>
* <li>This creates the drowning effect (damage and bubble particles)</li>
* <li>Masters should deactivate the choke before the pet dies</li>
* <li>The choke is controlled by the Master's punishment system</li>
* </ul>
*
* @see com.tiedup.remake.entities.ai.master.MasterPunishGoal
* @see com.tiedup.remake.events.restriction.PetPlayRestrictionHandler
*/
public class ItemChokeCollar extends ItemCollar implements IHas3DModelConfig {
private static final String NBT_CHOKING = "choking";
private static final Model3DConfig CONFIG = new Model3DConfig(
"tiedup:models/obj/choke_collar_leather/model.obj",
"tiedup:models/obj/choke_collar_leather/texture.png",
0.0f,
1.47f,
0.0f, // Collar band centered at neck level
1.0f,
0.0f,
0.0f,
180.0f, // Flip Y to match rendering convention
Set.of()
);
public ItemChokeCollar() {
super(new Item.Properties());
}
@Override
public String getItemName() {
return "choke_collar";
}
/**
* Check if choke mode is active.
*
* @param stack The collar ItemStack
* @return true if choking is active
*/
public boolean isChoking(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_CHOKING);
}
/**
* Set choke mode on/off.
* When active, applies drowning effect to wearer (handled by PetPlayRestrictionHandler).
*
* @param stack The collar ItemStack
* @param choking true to activate choking, false to deactivate
*/
public void setChoking(ItemStack stack, boolean choking) {
stack.getOrCreateTag().putBoolean(NBT_CHOKING, choking);
}
/**
* Check if collar is in pet play mode (from Master).
*
* @param stack The collar ItemStack
* @return true if this is a pet play collar
*/
public boolean isPetPlayMode(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean("petPlayMode");
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
// Show choke status
if (isChoking(stack)) {
tooltip.add(
Component.literal("CHOKING ACTIVE!")
.withStyle(ChatFormatting.DARK_RED)
.withStyle(ChatFormatting.BOLD)
);
}
// Show pet play mode status
if (isPetPlayMode(stack)) {
tooltip.add(
Component.literal("Pet Play Mode").withStyle(
ChatFormatting.LIGHT_PURPLE
)
);
}
// Description
tooltip.add(
Component.literal("A special collar used for pet play punishment")
.withStyle(ChatFormatting.DARK_GRAY)
.withStyle(ChatFormatting.ITALIC)
);
}
/**
* Choke collar cannot shock like shock collar.
*/
@Override
public boolean canShock() {
return false;
}
// ========================================
// 3D Model Support
// ========================================
@Override
public boolean uses3DModel() {
return true;
}
@Override
public ResourceLocation get3DModelLocation() {
return ResourceLocation.tryParse(CONFIG.objPath());
}
@Override
public Model3DConfig getModelConfig() {
return CONFIG;
}
}

View File

@@ -0,0 +1,22 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.ItemCollar;
import net.minecraft.world.item.Item;
/**
* Classic Collar - Basic collar item
* Standard collar for marking ownership.
*
* Based on original ItemCollar from 1.12.2
* Phase 1: No ownership system yet, just a basic wearable collar
* Note: Collars have maxStackSize of 1 (unique items)
*/
public class ItemClassicCollar extends ItemCollar {
public ItemClassicCollar() {
super(
new Item.Properties()
// stacksTo(1) is set by ItemCollar base class
);
}
}

View File

@@ -0,0 +1,537 @@
package com.tiedup.remake.items;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.personality.PacketOpenCommandWandScreen;
import com.tiedup.remake.personality.HomeType;
import com.tiedup.remake.personality.JobExperience;
import com.tiedup.remake.personality.NpcCommand;
import com.tiedup.remake.personality.NpcNeeds;
import com.tiedup.remake.personality.PersonalityState;
import java.util.List;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.state.BlockState;
/**
* Command Wand - Used to give commands to collared NPCs.
*
* Personality System Phase E: Command Wand Item
*
* <p><b>Usage:</b></p>
* <ul>
* <li>Right-click on a collared NPC you own to open command GUI</li>
* <li>Shows personality info, needs, and available commands</li>
* <li>Only works on NPCs wearing a collar you own</li>
* </ul>
*/
public class ItemCommandWand extends Item {
// NBT tags for selection mode
private static final String TAG_SELECTING_FOR = "SelectingFor";
private static final String TAG_JOB_TYPE = "JobType";
// Two-click selection for TRANSFER
private static final String TAG_SELECTING_STEP = "SelectingStep"; // 1 = chest A, 2 = chest B
private static final String TAG_FIRST_CHEST_POS = "FirstChestPos"; // BlockPos of chest A
public ItemCommandWand() {
super(new Item.Properties().stacksTo(1));
}
// ========== Selection Mode Methods ==========
/**
* Check if wand is in selection mode (waiting for chest click).
*/
public static boolean isInSelectionMode(ItemStack stack) {
return stack.hasTag() && stack.getTag().contains(TAG_SELECTING_FOR);
}
/**
* Enter selection mode - waiting for player to click a chest.
*
* HIGH FIX: Now uses UUID instead of entity ID for persistence across restarts.
*
* @param stack The wand item stack
* @param entityUUID The NPC entity UUID (persistent)
* @param job The job command to assign
*/
public static void enterSelectionMode(
ItemStack stack,
java.util.UUID entityUUID,
NpcCommand job
) {
stack.getOrCreateTag().putUUID(TAG_SELECTING_FOR, entityUUID);
stack.getOrCreateTag().putString(TAG_JOB_TYPE, job.name());
// For TRANSFER, we need two clicks
if (job == NpcCommand.TRANSFER) {
stack.getOrCreateTag().putInt(TAG_SELECTING_STEP, 1);
}
}
/**
* Exit selection mode.
*/
public static void exitSelectionMode(ItemStack stack) {
if (stack.hasTag()) {
stack.getTag().remove(TAG_SELECTING_FOR);
stack.getTag().remove(TAG_JOB_TYPE);
stack.getTag().remove(TAG_SELECTING_STEP);
stack.getTag().remove(TAG_FIRST_CHEST_POS);
}
}
/**
* Get the current selection step (1 = first chest, 2 = second chest).
* Returns 0 if not in multi-step selection.
*/
public static int getSelectingStep(ItemStack stack) {
return stack.hasTag() ? stack.getTag().getInt(TAG_SELECTING_STEP) : 0;
}
/**
* Get the first chest position (for TRANSFER).
*/
@Nullable
public static BlockPos getFirstChestPos(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().contains(TAG_FIRST_CHEST_POS)) {
return null;
}
return BlockPos.of(stack.getTag().getLong(TAG_FIRST_CHEST_POS));
}
/**
* Set the first chest position and advance to step 2.
*/
public static void setFirstChestAndAdvance(ItemStack stack, BlockPos pos) {
stack.getOrCreateTag().putLong(TAG_FIRST_CHEST_POS, pos.asLong());
stack.getOrCreateTag().putInt(TAG_SELECTING_STEP, 2);
}
/**
* Get the entity UUID being selected for.
*
* HIGH FIX: Returns UUID instead of entity ID for persistence.
*/
@Nullable
public static java.util.UUID getSelectingForEntity(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().hasUUID(TAG_SELECTING_FOR)) {
return null;
}
return stack.getTag().getUUID(TAG_SELECTING_FOR);
}
/**
* Get the job type being assigned.
*/
@Nullable
public static NpcCommand getSelectingJobType(ItemStack stack) {
if (!stack.hasTag() || !stack.getTag().contains(TAG_JOB_TYPE)) {
return null;
}
return NpcCommand.fromString(stack.getTag().getString(TAG_JOB_TYPE));
}
// ========== Block Interaction (Chest Selection) ==========
@Override
public InteractionResult useOn(UseOnContext context) {
ItemStack stack = context.getItemInHand();
Player player = context.getPlayer();
if (player == null) {
return InteractionResult.PASS;
}
BlockPos clickedPos = context.getClickedPos();
Level level = context.getLevel();
BlockState blockState = level.getBlockState(clickedPos);
// Selection mode for job commands
if (!isInSelectionMode(stack)) {
return InteractionResult.PASS;
}
// Must click on a chest
if (!(blockState.getBlock() instanceof ChestBlock)) {
if (!level.isClientSide) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"You must click on a chest to set the work zone!"
);
}
return InteractionResult.FAIL;
}
// Server-side: directly apply the command (we're already on server)
if (!level.isClientSide) {
// HIGH FIX: Use UUID instead of entity ID for persistence
java.util.UUID entityUUID = getSelectingForEntity(stack);
NpcCommand command = getSelectingJobType(stack);
int selectingStep = getSelectingStep(stack);
if (command != null && entityUUID != null) {
// TRANSFER requires two chests
if (command == NpcCommand.TRANSFER) {
if (selectingStep == 1) {
// First click: store source chest A
setFirstChestAndAdvance(stack, clickedPos);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Source chest set at " +
clickedPos.toShortString() +
". Now click on the DESTINATION chest."
);
return InteractionResult.SUCCESS;
} else if (selectingStep == 2) {
// Second click: apply command with both chests
BlockPos sourceChest = getFirstChestPos(stack);
if (sourceChest == null) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Error: Source chest not set. Try again."
);
exitSelectionMode(stack);
return InteractionResult.FAIL;
}
// Prevent selecting same chest twice
if (sourceChest.equals(clickedPos)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Destination must be different from source chest!"
);
return InteractionResult.FAIL;
}
// Find the NPC entity (HIGH FIX: lookup by UUID)
net.minecraft.world.entity.Entity entity = (
(net.minecraft.server.level.ServerLevel) level
).getEntity(entityUUID);
if (entity instanceof EntityDamsel damsel) {
// Give TRANSFER command with source (commandTarget) and dest (commandTarget2)
boolean success = damsel.giveCommandWithTwoTargets(
player,
command,
sourceChest, // Source chest A
clickedPos // Destination chest B
);
if (success) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
damsel.getNpcName() +
" will transfer items from " +
sourceChest.toShortString() +
" to " +
clickedPos.toShortString()
);
TiedUpMod.LOGGER.debug(
"[ItemCommandWand] {} set TRANSFER for {} from {} to {}",
player.getName().getString(),
damsel.getNpcName(),
sourceChest,
clickedPos
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
damsel.getNpcName() + " refused the job!"
);
}
}
exitSelectionMode(stack);
return InteractionResult.SUCCESS;
}
}
// Standard single-chest job commands (FARM, COOK, SHEAR, etc.) (HIGH FIX: lookup by UUID)
net.minecraft.world.entity.Entity entity = (
(net.minecraft.server.level.ServerLevel) level
).getEntity(entityUUID);
if (entity instanceof EntityDamsel damsel) {
// Give command directly (already validated acceptance in PacketNpcCommand)
boolean success = damsel.giveCommand(
player,
command,
clickedPos
);
if (success) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Work zone set! " +
damsel.getNpcName() +
" will " +
command.name() +
" at " +
clickedPos.toShortString()
);
TiedUpMod.LOGGER.debug(
"[ItemCommandWand] {} set work zone for {} at {}",
player.getName().getString(),
damsel.getNpcName(),
clickedPos
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
damsel.getNpcName() + " refused the job!"
);
}
}
// Exit selection mode
exitSelectionMode(stack);
}
}
return InteractionResult.SUCCESS;
}
// ========== Entity Interaction ==========
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Only works on EntityDamsel (and subclasses like EntityKidnapper)
if (!(target instanceof EntityDamsel damsel)) {
return InteractionResult.PASS;
}
// Server-side only
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Must have collar
if (!damsel.hasCollar()) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
damsel.getNpcName() + " is not wearing a collar!"
);
return InteractionResult.FAIL;
}
// Get collar and verify ownership
ItemStack collar = damsel.getEquipment(BodyRegionV2.NECK);
if (!(collar.getItem() instanceof ItemCollar collarItem)) {
return InteractionResult.PASS;
}
if (!collarItem.getOwners(collar).contains(player.getUUID())) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"You don't own " + damsel.getNpcName() + "'s collar!"
);
return InteractionResult.FAIL;
}
// Get personality data
PersonalityState state = damsel.getPersonalityState();
if (state == null) {
TiedUpMod.LOGGER.warn(
"[ItemCommandWand] No personality state for {}",
damsel.getNpcName()
);
return InteractionResult.FAIL;
}
// Always show personality (discovery system removed)
String personalityName = state.getPersonality().name();
// Get home type
String homeType = state.getHomeType().name();
// Cell info for GUI
String cellName = "";
String cellQualityName = "";
if (
state.getCellId() != null &&
player.level() instanceof net.minecraft.server.level.ServerLevel sl
) {
CellDataV2 cell = CellRegistryV2.get(sl).getCell(state.getCellId());
if (cell != null) {
cellName =
cell.getName() != null
? cell.getName()
: "Cell " + cell.getId().toString().substring(0, 8);
}
}
cellQualityName =
state.getCellQuality() != null ? state.getCellQuality().name() : "";
// Get needs
NpcNeeds needs = state.getNeeds();
// Get job experience for active command
JobExperience jobExp = state.getJobExperience();
NpcCommand activeCmd = state.getActiveCommand();
String activeJobLevelName = "";
int activeJobXp = 0;
int activeJobXpMax = 10;
if (activeCmd.isActiveJob()) {
JobExperience.JobLevel level = jobExp.getJobLevel(activeCmd);
activeJobLevelName = level.name();
activeJobXp = jobExp.getExperience(activeCmd);
activeJobXpMax = level.maxExp;
}
// Send packet to open GUI (HIGH FIX: pass UUID instead of entity ID)
if (player instanceof ServerPlayer sp) {
ModNetwork.sendToPlayer(
new PacketOpenCommandWandScreen(
damsel.getUUID(),
damsel.getNpcName(),
personalityName,
activeCmd.name(),
needs.getHunger(),
needs.getRest(),
state.getMood(),
state.getFollowDistance().name(),
homeType,
state.isAutoRestEnabled(),
cellName,
cellQualityName,
activeJobLevelName,
activeJobXp,
activeJobXpMax
),
sp
);
}
TiedUpMod.LOGGER.debug(
"[ItemCommandWand] {} opened command wand for {}",
player.getName().getString(),
damsel.getNpcName()
);
return InteractionResult.SUCCESS;
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
if (isInSelectionMode(stack)) {
NpcCommand job = getSelectingJobType(stack);
int step = getSelectingStep(stack);
tooltip.add(
Component.literal("SELECTION MODE").withStyle(
ChatFormatting.GOLD,
ChatFormatting.BOLD
)
);
// Different messages for TRANSFER two-step selection
if (job == NpcCommand.TRANSFER) {
if (step == 1) {
tooltip.add(
Component.literal("Click SOURCE chest (1/2)").withStyle(
ChatFormatting.YELLOW
)
);
} else if (step == 2) {
BlockPos source = getFirstChestPos(stack);
tooltip.add(
Component.literal(
"Click DESTINATION chest (2/2)"
).withStyle(ChatFormatting.YELLOW)
);
if (source != null) {
tooltip.add(
Component.literal(
"Source: " + source.toShortString()
).withStyle(ChatFormatting.GRAY)
);
}
}
} else {
tooltip.add(
Component.literal(
"Click a chest to set work zone"
).withStyle(ChatFormatting.YELLOW)
);
}
if (job != null) {
tooltip.add(
Component.literal("Job: " + job.name()).withStyle(
ChatFormatting.AQUA
)
);
}
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Right-click empty to cancel").withStyle(
ChatFormatting.RED
)
);
} else {
tooltip.add(
Component.literal("Right-click a collared NPC").withStyle(
ChatFormatting.GRAY
)
);
tooltip.add(
Component.literal("to give commands").withStyle(
ChatFormatting.GRAY
)
);
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Commands: FOLLOW, STAY, HEEL...").withStyle(
ChatFormatting.DARK_PURPLE
)
);
tooltip.add(
Component.literal("Jobs: FARM, COOK, STORE").withStyle(
ChatFormatting.GREEN
)
);
}
}
@Override
public boolean isFoil(ItemStack stack) {
// Enchantment glint when in selection mode
return isInSelectionMode(stack);
}
}

View File

@@ -0,0 +1,240 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.personality.PersonalityType;
import java.util.List;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
/**
* Debug Wand for testing the Personality System.
*
* <p>OP item for developers/testers to manipulate NPC personality state.
*
* <p>Controls:
* <ul>
* <li>Right-click on Damsel: Cycle personality type</li>
* <li>Shift + Right-click on Damsel: Show status</li>
* </ul>
*/
public class ItemDebugWand extends Item {
public ItemDebugWand() {
super(new Properties().stacksTo(1));
}
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
if (player.level().isClientSide()) {
return InteractionResult.SUCCESS;
}
if (!(target instanceof EntityDamsel damsel)) {
player.displayClientMessage(
Component.literal("Target must be a Damsel!").withStyle(
ChatFormatting.RED
),
true
);
return InteractionResult.FAIL;
}
PersonalityState state = damsel.getPersonalityState();
if (state == null) {
player.displayClientMessage(
Component.literal("Damsel has no personality state!").withStyle(
ChatFormatting.RED
),
true
);
return InteractionResult.FAIL;
}
boolean isShift = player.isShiftKeyDown();
if (isShift) {
// Show status
showStatus(damsel, state, player);
} else {
// Cycle personality type
cyclePersonality(damsel, state, player);
}
return InteractionResult.SUCCESS;
}
private void cyclePersonality(
EntityDamsel damsel,
PersonalityState state,
Player player
) {
PersonalityType current = state.getPersonality();
PersonalityType[] types = PersonalityType.values();
// Find next type
int currentIndex = current.ordinal();
int nextIndex = (currentIndex + 1) % types.length;
PersonalityType next = types[nextIndex];
// Create new state with new personality
damsel.setPersonalityType(next);
player.displayClientMessage(
Component.literal("Personality: ")
.withStyle(ChatFormatting.YELLOW)
.append(
Component.literal(current.name()).withStyle(
ChatFormatting.GRAY
)
)
.append(
Component.literal(" -> ").withStyle(ChatFormatting.WHITE)
)
.append(
Component.literal(next.name()).withStyle(
ChatFormatting.GOLD
)
),
false
);
TiedUpMod.LOGGER.info(
"[DebugWand] {} personality changed: {} -> {}",
damsel.getNpcName(),
current,
next
);
}
private void showStatus(
EntityDamsel damsel,
PersonalityState state,
Player player
) {
player.displayClientMessage(Component.literal(""), false); // Blank line
player.displayClientMessage(
Component.literal(
"=== " + damsel.getNpcName() + " ==="
).withStyle(ChatFormatting.GOLD, ChatFormatting.BOLD),
false
);
// Personality
player.displayClientMessage(
Component.literal("Personality: ")
.withStyle(ChatFormatting.GRAY)
.append(
Component.literal(state.getPersonality().name()).withStyle(
ChatFormatting.YELLOW
)
),
false
);
// Mood
float mood = state.getMood();
ChatFormatting moodColor =
mood > 60
? ChatFormatting.GREEN
: mood < 40
? ChatFormatting.RED
: ChatFormatting.YELLOW;
player.displayClientMessage(
Component.literal("Mood: ")
.withStyle(ChatFormatting.GRAY)
.append(
Component.literal(String.format("%.0f%%", mood)).withStyle(
moodColor
)
),
false
);
// Needs
var needs = state.getNeeds();
player.displayClientMessage(
Component.literal("Needs: ")
.withStyle(ChatFormatting.GRAY)
.append(
Component.literal(
String.format(
"Hunger:%.0f Rest:%.0f",
needs.getHunger(),
needs.getRest()
)
).withStyle(ChatFormatting.WHITE)
),
false
);
// Bondage state
player.displayClientMessage(
Component.literal("State: ")
.withStyle(ChatFormatting.GRAY)
.append(
Component.literal(
(damsel.isTiedUp() ? "TIED " : "") +
(damsel.isGagged() ? "GAGGED " : "") +
(damsel.isBlindfolded() ? "BLIND " : "") +
(damsel.hasCollar() ? "COLLARED " : "") +
(damsel.isSitting() ? "SIT " : "") +
(damsel.isKneeling() ? "KNEEL " : "")
).withStyle(ChatFormatting.RED)
),
false
);
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
tooltip.add(
Component.literal("Debug tool for Personality System").withStyle(
ChatFormatting.GRAY
)
);
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Right-click: Cycle Personality").withStyle(
ChatFormatting.GREEN
)
);
tooltip.add(
Component.literal("Shift + Right-click: Show Status").withStyle(
ChatFormatting.YELLOW
)
);
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("OP Item - Testing Only").withStyle(
ChatFormatting.RED,
ChatFormatting.ITALIC
)
);
}
@Override
public boolean isFoil(ItemStack stack) {
return true; // Enchantment glint to show it's special
}
}

View File

@@ -0,0 +1,370 @@
package com.tiedup.remake.items;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* GPS Collar - Advanced shock collar with tracking and safe zone features.
*
* <p>Mechanics:</p>
* <ul>
* <li><b>Safe Zones:</b> Can store multiple coordinates (SafeSpots) where the wearer is allowed to be.</li>
* <li><b>Auto-Shock:</b> If the wearer is outside ALL active safe zones, they are shocked at intervals.</li>
* <li><b>Master Warning:</b> Masters receive an alert message when a safe zone violation is detected.</li>
* <li><b>Public Tracking:</b> If enabled, allows anyone with a Locator to see distance and direction.</li>
* </ul>
*/
public class ItemGpsCollar extends ItemShockCollar {
private static final String NBT_PUBLIC_TRACKING = "publicTracking";
private static final String NBT_GPS_ACTIVE = "gpsActive";
private static final String NBT_SAFE_SPOTS = "gpsSafeSpots";
private static final String NBT_SHOCK_INTERVAL = "gpsShockInterval";
private static final String NBT_WARN_MASTERS = "warn_masters";
private final int defaultInterval;
public ItemGpsCollar() {
this(200); // 10 seconds default
}
public ItemGpsCollar(int defaultInterval) {
super();
this.defaultInterval = defaultInterval;
}
@Override
public boolean hasGPS() {
return true;
}
/**
* Renders detailed GPS status, safe zone list, and alert settings in the item tooltip.
*/
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
tooltip.add(
Component.literal("GPS Enabled").withStyle(
ChatFormatting.DARK_GREEN
)
);
if (hasPublicTracking(stack)) {
tooltip.add(
Component.literal("Public Tracking Enabled").withStyle(
ChatFormatting.GREEN
)
);
}
if (shouldWarnMasters(stack)) {
tooltip.add(
Component.literal("Alert Masters on Violation").withStyle(
ChatFormatting.GOLD
)
);
}
List<SafeSpot> safeSpots = getSafeSpots(stack);
if (!safeSpots.isEmpty()) {
tooltip.add(
Component.literal("GPS Shocks: ")
.withStyle(ChatFormatting.GREEN)
.append(
Component.literal(
isActive(stack) ? "ENABLED" : "DISABLED"
).withStyle(
isActive(stack)
? ChatFormatting.RED
: ChatFormatting.GRAY
)
)
);
tooltip.add(
Component.literal(
"Safe Spots (" + safeSpots.size() + "):"
).withStyle(ChatFormatting.GREEN)
);
for (int i = 0; i < safeSpots.size(); i++) {
SafeSpot spot = safeSpots.get(i);
tooltip.add(
Component.literal(
(spot.active ? "[+] " : "[-] ") +
(i + 1) +
": " +
spot.x +
"," +
spot.y +
"," +
spot.z +
" (Range: " +
spot.distance +
"m)"
).withStyle(ChatFormatting.GRAY)
);
}
}
}
public boolean shouldWarnMasters(ItemStack stack) {
CompoundTag tag = stack.getTag();
// Default to true if tag doesn't exist
return (
tag == null ||
!tag.contains(NBT_WARN_MASTERS) ||
tag.getBoolean(NBT_WARN_MASTERS)
);
}
public void setWarnMasters(ItemStack stack, boolean warn) {
stack.getOrCreateTag().putBoolean(NBT_WARN_MASTERS, warn);
}
public boolean hasPublicTracking(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_PUBLIC_TRACKING);
}
public void setPublicTracking(ItemStack stack, boolean publicTracking) {
stack.getOrCreateTag().putBoolean(NBT_PUBLIC_TRACKING, publicTracking);
}
public boolean isActive(ItemStack stack) {
CompoundTag tag = stack.getTag();
// Default to active if tag doesn't exist
return (
tag == null ||
!tag.contains(NBT_GPS_ACTIVE) ||
tag.getBoolean(NBT_GPS_ACTIVE)
);
}
public void setActive(ItemStack stack, boolean active) {
stack.getOrCreateTag().putBoolean(NBT_GPS_ACTIVE, active);
}
/**
* Parses the NBT List into a Java List of SafeSpot objects.
*/
public List<SafeSpot> getSafeSpots(ItemStack stack) {
List<SafeSpot> list = new ArrayList<>();
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_SAFE_SPOTS)) {
ListTag spotList = tag.getList(NBT_SAFE_SPOTS, 10);
for (int i = 0; i < spotList.size(); i++) {
list.add(new SafeSpot(spotList.getCompound(i)));
}
}
return list;
}
/**
* Adds a new safe zone to the collar's NBT data.
*/
public void addSafeSpot(
ItemStack stack,
int x,
int y,
int z,
String dimension,
int distance
) {
CompoundTag tag = stack.getOrCreateTag();
ListTag spotList = tag.getList(NBT_SAFE_SPOTS, 10);
SafeSpot spot = new SafeSpot(x, y, z, dimension, distance, true);
spotList.add(spot.toNBT());
tag.put(NBT_SAFE_SPOTS, spotList);
}
/**
* Gets frequency of GPS violation shocks.
*/
public int getShockInterval(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_SHOCK_INTERVAL)) {
return tag.getInt(NBT_SHOCK_INTERVAL);
}
return defaultInterval;
}
/**
* Phase 14.1.4: Reset auto-shock timer when GPS collar is removed.
*/
@Override
public void onUnequipped(ItemStack stack, LivingEntity entity) {
// Use IRestrainable interface instead of Player-only
IRestrainable state = KidnappedHelper.getKidnappedState(entity);
if (state != null) {
state.resetAutoShockTimer();
}
super.onUnequipped(stack, entity);
}
/**
* Represents a defined safe zone in the 3D world.
*/
public static class SafeSpot {
public int x, y, z;
public String dimension;
public int distance;
public boolean active;
public SafeSpot(
int x,
int y,
int z,
String dimension,
int distance,
boolean active
) {
this.x = x;
this.y = y;
this.z = z;
this.dimension = dimension;
this.distance = distance;
this.active = active;
}
public SafeSpot(CompoundTag nbt) {
this.x = nbt.getInt("x");
this.y = nbt.getInt("y");
this.z = nbt.getInt("z");
this.dimension = nbt.getString("dim");
this.distance = nbt.getInt("dist");
this.active = !nbt.contains("active") || nbt.getBoolean("active");
}
public CompoundTag toNBT() {
CompoundTag nbt = new CompoundTag();
nbt.putInt("x", x);
nbt.putInt("y", y);
nbt.putInt("z", z);
nbt.putString("dim", dimension);
nbt.putInt("dist", distance);
nbt.putBoolean("active", active);
return nbt;
}
/**
* Checks if an entity is within the cuboid boundaries of this safe zone.
* Faithful to original 1.12.2 distance logic.
*/
public boolean isInside(Entity entity) {
if (!active) return true;
// LOW FIX: Cross-dimension GPS fix
// If entity is in a different dimension, consider them as "inside" the zone
// to prevent false positive shocks when traveling between dimensions
if (
!entity
.level()
.dimension()
.location()
.toString()
.equals(dimension)
) return true; // Changed from false to true
// Cuboid distance check
return (
Math.abs(entity.getX() - x) < distance &&
Math.abs(entity.getY() - y) < distance &&
Math.abs(entity.getZ() - z) < distance
);
}
}
}

View File

@@ -0,0 +1,245 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemOwnerTarget;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
public class ItemGpsLocator extends ItemOwnerTarget {
public ItemGpsLocator() {
super(new net.minecraft.world.item.Item.Properties().stacksTo(1));
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
appendOwnerTooltip(stack, tooltip, "Right-click a player");
if (hasTarget(stack)) {
String displayName = resolveTargetDisplayName(stack, level);
tooltip.add(
Component.literal("Target: ")
.withStyle(ChatFormatting.BLUE)
.append(
Component.literal(displayName).withStyle(
ChatFormatting.WHITE
)
)
);
}
}
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
if (level.isClientSide) return InteractionResultHolder.success(stack);
if (!hasOwner(stack)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"You must claim this locator first! (Right-click a player)"
);
return InteractionResultHolder.fail(stack);
}
if (!isOwner(stack, player)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.LOCATOR_NOT_OWNER
);
return InteractionResultHolder.fail(stack);
}
if (hasTarget(stack)) {
// Use server player list for cross-dimension tracking
Player target = level
.getServer()
.getPlayerList()
.getPlayer(getTargetId(stack));
if (target != null) {
IBondageState targetState = KidnappedHelper.getKidnappedState(
target
);
if (targetState != null && targetState.hasCollar()) {
ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK);
if (
collarStack.getItem() instanceof
ItemGpsCollar collarItem
) {
if (
collarItem.isOwner(collarStack, player) ||
collarItem.hasPublicTracking(collarStack)
) {
// Check if same dimension
boolean sameDimension = player
.level()
.dimension()
.equals(target.level().dimension());
if (sameDimension) {
double distance = player.distanceTo(target);
String direction = getDirection(player, target);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.LOCATOR_DETECTED,
(int) distance + "m [" + direction + "]"
);
} else {
// Cross-dimension: show dimension name
String dimName = getDimensionDisplayName(
target
.level()
.dimension()
.location()
.getPath()
);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.LOCATOR_DETECTED,
"Target is in [" + dimName + "]"
);
}
playLocatorSound(player);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"You are not allowed to access this GPS Collar!"
);
}
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Target is not wearing a GPS Collar!"
);
}
}
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Unable to locate target! (Offline)"
);
}
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"No target connected!"
);
}
return InteractionResultHolder.success(stack);
}
/**
* Phase 14.1.5: Refactored to support IBondageState (LivingEntity + NPCs)
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
if (player.level().isClientSide) return InteractionResult.SUCCESS;
IBondageState playerState = KidnappedHelper.getKidnappedState(player);
if (
playerState != null && playerState.isTiedUp()
) return InteractionResult.FAIL;
if (!hasOwner(stack)) {
setOwner(stack, player);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.LOCATOR_CLAIMED
);
} else if (!isOwner(stack, player)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.LOCATOR_NOT_OWNER
);
return InteractionResult.FAIL;
}
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState != null) {
setTarget(stack, target);
player.setItemInHand(hand, stack); // Force sync
SystemMessageManager.sendChatToPlayer(
player,
"Connected to " + target.getName().getString(),
ChatFormatting.GREEN
);
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
private String getDirection(Player source, Player target) {
double dx = target.getX() - source.getX();
double dz = target.getZ() - source.getZ();
if (Math.abs(dx) > Math.abs(dz)) {
return dx > 0 ? "EAST" : "WEST";
} else {
return dz > 0 ? "SOUTH" : "NORTH";
}
}
private void playLocatorSound(Player player) {
player
.level()
.playSound(
null,
player.blockPosition(),
com.tiedup.remake.core.ModSounds.SHOCKER_ACTIVATED.get(),
net.minecraft.sounds.SoundSource.PLAYERS,
0.5f,
1.0f
);
}
/**
* Get a user-friendly display name for a dimension.
*/
private String getDimensionDisplayName(String dimensionPath) {
return switch (dimensionPath) {
case "overworld" -> "Overworld";
case "the_nether" -> "The Nether";
case "the_end" -> "The End";
default -> dimensionPath.replace("_", " ");
};
}
}

View File

@@ -0,0 +1,36 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.IHasGaggingEffect;
import com.tiedup.remake.items.base.ItemBlindfold;
import com.tiedup.remake.util.GagMaterial;
import net.minecraft.world.item.Item;
/**
* Hood - Covers the head completely
* Combines blindfold effect with gagging effect.
*
* Phase 15: Combo item (BLINDFOLD slot + gag effect)
* Extends ItemBlindfold for slot behavior, implements IHasGaggingEffect for speech muffling.
*/
public class ItemHood extends ItemBlindfold implements IHasGaggingEffect {
private final GagMaterial gagMaterial;
public ItemHood() {
super(new Item.Properties().stacksTo(16));
this.gagMaterial = GagMaterial.STUFFED; // Hoods muffle speech like stuffed gags
}
/**
* Get the gag material type for speech conversion.
* @return The gag material (STUFFED for hoods)
*/
public GagMaterial getGagMaterial() {
return gagMaterial;
}
@Override
public String getTextureSubfolder() {
return "hoods";
}
}

View File

@@ -0,0 +1,313 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.items.base.ItemOwnerTarget;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;
/**
* Collar Key - Used to lock/unlock bondage items via the Slave Management GUI.
*
* <p>Phase 20: Key-Lock System</p>
* <ul>
* <li><b>Linking:</b> Right-click a player wearing a collar to link (claim) the key to them.</li>
* <li><b>Management:</b> Opens SlaveItemManagementScreen to lock/unlock individual items.</li>
* <li><b>Key UUID:</b> Each key has a unique UUID used to identify which locks it created.</li>
* <li><b>Security:</b> Only items locked with this key can be unlocked by it.</li>
* </ul>
*/
public class ItemKey extends ItemOwnerTarget {
private static final String NBT_KEY_UUID = "keyUUID";
public ItemKey() {
super(new net.minecraft.world.item.Item.Properties().durability(64));
}
// ========== Phase 20: Key UUID System ==========
/**
* Get the unique UUID for this key.
* Generates one if it doesn't exist yet.
* This UUID is used to identify locks created by this specific key.
*
* @param stack The key ItemStack
* @return The key's unique UUID
*/
public UUID getKeyUUID(ItemStack stack) {
CompoundTag tag = stack.getOrCreateTag();
if (!tag.hasUUID(NBT_KEY_UUID)) {
// Generate a new UUID for this key
tag.putUUID(NBT_KEY_UUID, UUID.randomUUID());
}
return tag.getUUID(NBT_KEY_UUID);
}
/**
* Check if this key matches the given UUID.
*
* @param stack The key ItemStack
* @param lockUUID The lock's UUID to check against
* @return true if this key matches the lock
*/
public boolean matchesLock(ItemStack stack, UUID lockUUID) {
if (lockUUID == null) return false;
return lockUUID.equals(getKeyUUID(stack));
}
/**
* Shows ownership and target information when hovering over the item in inventory.
*/
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
if (hasOwner(stack)) {
tooltip.add(
Component.literal("Owner: ")
.withStyle(ChatFormatting.GOLD)
.append(
Component.literal(getOwnerName(stack)).withStyle(
ChatFormatting.WHITE
)
)
);
} else {
tooltip.add(
Component.literal(
"Unclaimed (Right-click a collar wearer to claim)"
).withStyle(ChatFormatting.GRAY)
);
}
if (hasTarget(stack)) {
tooltip.add(
Component.literal("Target: ")
.withStyle(ChatFormatting.BLUE)
.append(
Component.literal(getTargetName(stack)).withStyle(
ChatFormatting.WHITE
)
)
);
}
tooltip.add(
Component.literal("Right-click a collared player to toggle LOCK")
.withStyle(ChatFormatting.DARK_GRAY)
.withStyle(ChatFormatting.ITALIC)
);
}
/**
* Logic for interacting with entities wearing collars.
* Opens the Slave Item Management GUI to lock/unlock individual items.
*
* Phase 14.1.5: Refactored to support IBondageState (LivingEntity + NPCs)
* Phase 20: Opens GUI instead of direct toggle
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Check if target can wear collars (Player, EntityDamsel, EntityKidnapper)
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
// Target must be wearing a collar
if (targetState == null || !targetState.hasCollar()) {
if (!player.level().isClientSide) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Target is not wearing a collar!"
);
}
return InteractionResult.FAIL;
}
// Server-side: Handle claiming and validation
if (!player.level().isClientSide) {
// 1. Claim logic - first interaction with a collar wearer links the key
if (!hasOwner(stack)) {
setOwner(stack, player);
setTarget(stack, target);
// Ensure key UUID is generated
getKeyUUID(stack);
// Also link the player to the collar (become collar owner)
linkPlayerToCollar(player, target, targetState);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.KEY_CLAIMED,
target
);
player.setItemInHand(hand, stack); // Sync NBT to client
return InteractionResult.SUCCESS;
}
// 2. Ownership check - only the person who claimed the key can use it
if (!isOwner(stack, player)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.KEY_NOT_OWNER
);
return InteractionResult.FAIL;
}
// 3. Target check - this key only fits the entity it was first linked to
if (
target instanceof Player targetPlayer &&
!isTarget(stack, targetPlayer)
) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.KEY_WRONG_TARGET
);
return InteractionResult.FAIL;
} else if (
!(target instanceof Player) &&
!target.getUUID().equals(getTargetId(stack))
) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.KEY_WRONG_TARGET
);
return InteractionResult.FAIL;
}
// Server validation passed - client will open GUI
return InteractionResult.SUCCESS;
}
// Client-side: Open the Slave Item Management GUI
// Only open if key is already claimed and we're the owner (client trusts server validation)
if (hasOwner(stack) && isOwner(stack, player)) {
// Verify target matches (client-side check for responsiveness)
boolean targetMatches = false;
if (target instanceof Player targetPlayer) {
targetMatches = isTarget(stack, targetPlayer);
} else {
targetMatches = target.getUUID().equals(getTargetId(stack));
}
if (targetMatches) {
openUnifiedBondageScreen(target);
}
}
return InteractionResult.SUCCESS;
}
/**
* Opens the UnifiedBondageScreen in master mode targeting a specific entity.
* Called from client-side code only.
*
* @param target The living entity to manage bondage for
*/
@OnlyIn(Dist.CLIENT)
private void openUnifiedBondageScreen(
net.minecraft.world.entity.LivingEntity target
) {
net.minecraft.client.Minecraft.getInstance().setScreen(
new com.tiedup.remake.client.gui.screens.UnifiedBondageScreen(target)
);
}
/**
* Link a player to a collar - make them an owner.
*
* <p>When a key is claimed on a collared entity:
* <ul>
* <li>Add player as owner to the collar item</li>
* <li>Register the relationship in CollarRegistry</li>
* </ul>
*
* @param player The player claiming the key
* @param target The collared entity
* @param targetState The target's IBondageState state
*/
private void linkPlayerToCollar(
Player player,
LivingEntity target,
IBondageState targetState
) {
ItemStack collarStack = targetState.getEquipment(BodyRegionV2.NECK);
if (collarStack.isEmpty()) return;
if (
collarStack.getItem() instanceof
com.tiedup.remake.items.base.ItemCollar collar
) {
// Add player as owner to the collar (if not already)
if (!collar.getOwners(collarStack).contains(player.getUUID())) {
collar.addOwner(collarStack, player);
// Update the collar in the target's inventory
targetState.equip(BodyRegionV2.NECK, collarStack);
}
// Register in CollarRegistry (if on server)
if (
player.level() instanceof
net.minecraft.server.level.ServerLevel serverLevel
) {
com.tiedup.remake.state.CollarRegistry registry =
com.tiedup.remake.state.CollarRegistry.get(serverLevel);
if (registry != null) {
registry.registerCollar(target.getUUID(), player.getUUID());
// Sync the registry to the new owner
if (
player instanceof
net.minecraft.server.level.ServerPlayer serverPlayer
) {
java.util.Set<UUID> slaves = registry.getSlaves(
player.getUUID()
);
com.tiedup.remake.network.ModNetwork.sendToPlayer(
new com.tiedup.remake.network.sync.PacketSyncCollarRegistry(
slaves
),
serverPlayer
);
}
}
// Sync the target's inventory (collar was modified)
if (
target instanceof
net.minecraft.server.level.ServerPlayer targetPlayer
) {
com.tiedup.remake.network.sync.SyncManager.syncInventory(
targetPlayer
);
}
}
}
}
}

View File

@@ -0,0 +1,395 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.jetbrains.annotations.Nullable;
/**
* Lockpick item for picking locks on bondage restraints.
*
* Phase 21: Revamped Lockpick System
*
* Behavior:
* - 25% chance of success per attempt
* - SUCCESS: Instant unlock, padlock PRESERVED (lockable=true)
* - FAIL:
* - 2.5% chance to JAM the lock (blocks future lockpick attempts)
* - 15% chance to break the lockpick
* - If shock collar equipped: SHOCK + notify owners
* - Cannot be used while wearing mittens
* - Durability: 10 uses
*/
public class ItemLockpick extends Item {
private static final Random random = new Random();
public ItemLockpick() {
super(new Item.Properties().durability(5)); // 5 tentatives max
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
tooltip.add(
Component.translatable("item.tiedup.lockpick.tooltip").withStyle(
ChatFormatting.GRAY
)
);
int remaining = stack.getMaxDamage() - stack.getDamageValue();
tooltip.add(
Component.literal(
"Uses: " + remaining + "/" + stack.getMaxDamage()
).withStyle(ChatFormatting.DARK_GRAY)
);
// LOW FIX: Removed server config access from client tooltip (desync issue)
// Success/break chances depend on server config, not client config
// Displaying client config values here would be misleading in multiplayer
tooltip.add(
Component.literal("Success/break chances: Check server config")
.withStyle(ChatFormatting.GRAY)
.withStyle(ChatFormatting.ITALIC)
);
}
/**
* v2.5: Right-click with lockpick opens the struggle choice screen.
* This allows the player to choose which locked item to pick.
*/
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return InteractionResultHolder.pass(stack);
}
// Block mittens
if (state.hasMittens()) {
if (!level.isClientSide) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.CANT_USE_ITEM_MITTENS
);
}
return InteractionResultHolder.fail(stack);
}
// Client side: open the unified bondage screen
if (level.isClientSide) {
openUnifiedBondageScreen();
return InteractionResultHolder.success(stack);
}
return InteractionResultHolder.consume(stack);
}
/**
* Client-only method to open the unified bondage screen.
* Separated to avoid classloading issues on server.
* Uses fully qualified names to prevent class loading on server.
*/
@OnlyIn(Dist.CLIENT)
private void openUnifiedBondageScreen() {
net.minecraft.client.Minecraft.getInstance().setScreen(
new com.tiedup.remake.client.gui.screens.UnifiedBondageScreen()
);
}
/**
* Check if this lockpick can be used (has durability remaining).
*/
public static boolean canUse(ItemStack stack) {
if (stack.isEmpty() || !(stack.getItem() instanceof ItemLockpick)) {
return false;
}
return stack.getDamageValue() < stack.getMaxDamage();
}
/**
* Result of a lockpick attempt.
*/
public enum PickResult {
/** Successfully picked the lock - item unlocked, padlock preserved */
SUCCESS,
/** Failed but lock still pickable */
FAIL,
/** Failed and jammed the lock - lockpick no longer usable on this item */
JAMMED,
/** Lockpick broke during attempt */
BROKE,
/** Cannot attempt - mittens equipped */
BLOCKED_MITTENS,
/** Cannot attempt - lock is jammed */
BLOCKED_JAMMED,
/** Cannot attempt - item not locked */
NOT_LOCKED,
}
/**
* Attempt to pick a lock on a target item.
*
* @param player The player attempting to pick
* @param state The player's bind state
* @param lockpickStack The lockpick being used
* @param targetStack The item to pick
* @param targetRegion The V2 body region of the target item
* @return The result of the pick attempt
*/
public static PickResult attemptPick(
Player player,
PlayerBindState state,
ItemStack lockpickStack,
ItemStack targetStack,
BodyRegionV2 targetRegion
) {
// Check if lockpick is usable
if (!canUse(lockpickStack)) {
return PickResult.BROKE;
}
// Check if wearing mittens
if (state.hasMittens()) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.CANT_USE_ITEM_MITTENS
);
return PickResult.BLOCKED_MITTENS;
}
// Check if target is lockable and locked
if (!(targetStack.getItem() instanceof ILockable lockable)) {
return PickResult.NOT_LOCKED;
}
if (!lockable.isLocked(targetStack)) {
return PickResult.NOT_LOCKED;
}
// Check if lock is jammed
if (lockable.isJammed(targetStack)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"This lock is jammed! Use struggle instead."
);
return PickResult.BLOCKED_JAMMED;
}
// Roll for success
boolean success =
random.nextInt(100) < ModConfig.SERVER.lockpickSuccessChance.get();
if (success) {
// SUCCESS: Unlock the item, PRESERVE the padlock
lockable.setLockedByKeyUUID(targetStack, null); // Unlock
lockable.clearLockResistance(targetStack); // Clear struggle progress
// lockable stays true - padlock preserved!
SystemMessageManager.sendToPlayer(
player,
"Lock picked!",
ChatFormatting.GREEN
);
// Damage lockpick
damageLockpick(lockpickStack);
TiedUpMod.LOGGER.info(
"[LOCKPICK] {} successfully picked lock on {} ({})",
player.getName().getString(),
targetStack.getDisplayName().getString(),
targetRegion
);
return PickResult.SUCCESS;
} else {
// FAIL: Various bad things can happen
// 1. Check for shock collar and trigger shock
triggerShockIfCollar(player, state);
// 2. Check for jam
boolean jammed =
random.nextDouble() * 100 <
ModConfig.SERVER.lockpickJamChance.get();
if (jammed) {
lockable.setJammed(targetStack, true);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"The lock jammed! Only struggle can open it now."
);
TiedUpMod.LOGGER.info(
"[LOCKPICK] {} jammed the lock on {} ({})",
player.getName().getString(),
targetStack.getDisplayName().getString(),
targetRegion
);
// Damage lockpick
boolean broke = damageLockpick(lockpickStack);
return broke ? PickResult.BROKE : PickResult.JAMMED;
}
// 3. Check for break
boolean broke =
random.nextInt(100) <
ModConfig.SERVER.lockpickBreakChance.get();
if (broke) {
lockpickStack.shrink(1);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Lockpick broke!"
);
TiedUpMod.LOGGER.info(
"[LOCKPICK] {}'s lockpick broke while picking {} ({})",
player.getName().getString(),
targetStack.getDisplayName().getString(),
targetRegion
);
return PickResult.BROKE;
}
// 4. Normal fail - just damage lockpick
damageLockpick(lockpickStack);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.WARNING,
"Lockpick slipped..."
);
TiedUpMod.LOGGER.debug(
"[LOCKPICK] {} failed to pick lock on {} ({})",
player.getName().getString(),
targetStack.getDisplayName().getString(),
targetRegion
);
return PickResult.FAIL;
}
}
/**
* Damage the lockpick by 1 use.
* @return true if the lockpick broke (ran out of durability)
*/
private static boolean damageLockpick(ItemStack stack) {
stack.setDamageValue(stack.getDamageValue() + 1);
if (stack.getDamageValue() >= stack.getMaxDamage()) {
stack.shrink(1);
return true;
}
return false;
}
/**
* Trigger shock collar if player has one equipped.
* Also notifies the collar owners.
*/
private static void triggerShockIfCollar(
Player player,
PlayerBindState state
) {
ItemStack collar = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
if (collar.isEmpty()) return;
if (
collar.getItem() instanceof
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
// Shock the player
state.shockKidnapped(" (Failed lockpick attempt)", 2.0f);
// Notify owners
notifyOwnersLockpickAttempt(player, collar, shockCollar);
TiedUpMod.LOGGER.info(
"[LOCKPICK] {} was shocked for failed lockpick attempt",
player.getName().getString()
);
}
}
/**
* Notify shock collar owners about the lockpick attempt.
*/
private static void notifyOwnersLockpickAttempt(
Player player,
ItemStack collar,
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
if (player.getServer() == null) return;
Component warning = Component.literal("ALERT: ")
.withStyle(ChatFormatting.RED, ChatFormatting.BOLD)
.append(
Component.literal(
player.getName().getString() + " tried to pick a lock!"
).withStyle(ChatFormatting.GOLD)
);
List<UUID> owners = shockCollar.getOwners(collar);
for (UUID ownerId : owners) {
ServerPlayer owner = player
.getServer()
.getPlayerList()
.getPlayer(ownerId);
if (owner != null) {
owner.sendSystemMessage(warning);
}
}
}
/**
* Find a lockpick in the player's inventory.
* @return The first usable lockpick found, or EMPTY if none
*/
public static ItemStack findLockpickInInventory(Player player) {
for (ItemStack stack : player.getInventory().items) {
if (canUse(stack)) {
return stack;
}
}
return ItemStack.EMPTY;
}
}

View File

@@ -0,0 +1,244 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.TiedUpSounds;
import java.util.UUID;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
/**
* Master Key - Universal key that opens any padlock.
*
* Phase 15: Full master key implementation
* Phase 20: Opens SlaveItemManagementScreen in master mode
*
* Behavior:
* - Right-click: Opens Slave Management GUI (can unlock any lock)
* - Shift+Right-click: Quick unlock all restraints on target
* - Does not consume the key (reusable)
* - Cannot lock items (unlock only)
*/
public class ItemMasterKey extends Item {
public ItemMasterKey() {
super(new Item.Properties().stacksTo(8));
}
/**
* Called when player right-clicks another entity with the master key.
* Opens the Slave Item Management GUI or quick-unlocks all if sneaking.
*
* @param stack The item stack
* @param player The player using the key
* @param target The entity being interacted with
* @param hand The hand holding the key
* @return SUCCESS if action taken, PASS otherwise
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Check if target can be restrained
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null) {
return InteractionResult.PASS;
}
// Target must have a collar
if (!targetState.hasCollar()) {
if (!player.level().isClientSide) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"Target is not wearing a collar!"
);
}
return InteractionResult.FAIL;
}
// Server-side: Handle Shift+click quick unlock
if (!player.level().isClientSide) {
if (player.isShiftKeyDown()) {
// Quick unlock all - original behavior
int unlocked = unlockAllRestraints(targetState, target);
if (unlocked > 0) {
TiedUpSounds.playUnlockSound(target);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Unlocked " +
unlocked +
" restraint(s) on " +
target.getName().getString()
);
TiedUpMod.LOGGER.info(
"[ItemMasterKey] {} quick-unlocked {} restraints on {}",
player.getName().getString(),
unlocked,
target.getName().getString()
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"No locked restraints found"
);
}
return InteractionResult.SUCCESS;
}
// Normal click - validation only, client opens GUI
return InteractionResult.SUCCESS;
}
// Client-side: Open GUI (normal click only)
if (!player.isShiftKeyDown()) {
openUnifiedBondageScreen(target);
}
return InteractionResult.SUCCESS;
}
/**
* Opens the UnifiedBondageScreen in master mode targeting a specific entity.
*
* @param target The living entity to manage bondage for
*/
@OnlyIn(Dist.CLIENT)
private void openUnifiedBondageScreen(
net.minecraft.world.entity.LivingEntity target
) {
net.minecraft.client.Minecraft.getInstance().setScreen(
new com.tiedup.remake.client.gui.screens.UnifiedBondageScreen(target)
);
}
/**
* Unlock all locked restraints on the target.
* Uses setLockedByKeyUUID(null) to properly clear the lock.
* Optionally drops padlocks if the item's dropLockOnUnlock() returns true.
*
* @param targetState The target's IBondageState state
* @param target The target entity (for dropping items)
* @return Number of items unlocked
*/
private int unlockAllRestraints(
IBondageState targetState,
LivingEntity target
) {
int unlocked = 0;
// Unlock bind
ItemStack bind = targetState.getEquipment(BodyRegionV2.ARMS);
if (!bind.isEmpty() && bind.getItem() instanceof ILockable lockable) {
if (lockable.isLocked(bind)) {
lockable.setLockedByKeyUUID(bind, null); // Clear lock with keyUUID system
unlocked++;
if (lockable.dropLockOnUnlock()) {
dropPadlock(targetState);
}
}
}
// Unlock gag
ItemStack gag = targetState.getEquipment(BodyRegionV2.MOUTH);
if (!gag.isEmpty() && gag.getItem() instanceof ILockable lockable) {
if (lockable.isLocked(gag)) {
lockable.setLockedByKeyUUID(gag, null);
unlocked++;
if (lockable.dropLockOnUnlock()) {
dropPadlock(targetState);
}
}
}
// Unlock blindfold
ItemStack blindfold = targetState.getEquipment(BodyRegionV2.EYES);
if (
!blindfold.isEmpty() &&
blindfold.getItem() instanceof ILockable lockable
) {
if (lockable.isLocked(blindfold)) {
lockable.setLockedByKeyUUID(blindfold, null);
unlocked++;
if (lockable.dropLockOnUnlock()) {
dropPadlock(targetState);
}
}
}
// Unlock earplugs
ItemStack earplugs = targetState.getEquipment(BodyRegionV2.EARS);
if (
!earplugs.isEmpty() &&
earplugs.getItem() instanceof ILockable lockable
) {
if (lockable.isLocked(earplugs)) {
lockable.setLockedByKeyUUID(earplugs, null);
unlocked++;
if (lockable.dropLockOnUnlock()) {
dropPadlock(targetState);
}
}
}
// Unlock collar
ItemStack collar = targetState.getEquipment(BodyRegionV2.NECK);
if (
!collar.isEmpty() && collar.getItem() instanceof ILockable lockable
) {
if (lockable.isLocked(collar)) {
lockable.setLockedByKeyUUID(collar, null);
unlocked++;
if (lockable.dropLockOnUnlock()) {
dropPadlock(targetState);
}
}
}
// Unlock mittens
ItemStack mittens = targetState.getEquipment(BodyRegionV2.HANDS);
if (
!mittens.isEmpty() &&
mittens.getItem() instanceof ILockable lockable
) {
if (lockable.isLocked(mittens)) {
lockable.setLockedByKeyUUID(mittens, null);
unlocked++;
if (lockable.dropLockOnUnlock()) {
dropPadlock(targetState);
}
}
}
return unlocked;
}
/**
* Drop a padlock item near the target.
*
* @param targetState The target's IBondageState state
*/
private void dropPadlock(IBondageState targetState) {
// Create a padlock item to drop
ItemStack padlock = new ItemStack(
com.tiedup.remake.items.ModItems.PADLOCK.get()
);
targetState.kidnappedDropItem(padlock);
}
}

View File

@@ -0,0 +1,25 @@
package com.tiedup.remake.items;
import com.tiedup.remake.items.base.IHasBlindingEffect;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.util.GagMaterial;
import net.minecraft.world.item.Item;
/**
* Medical Gag - Full face medical restraint
* Combines gag effect with blinding effect.
*
* Phase 15: Combo item (GAG slot + blinding effect)
* Extends ItemGag for slot behavior, implements IHasBlindingEffect for vision obstruction.
*/
public class ItemMedicalGag extends ItemGag implements IHasBlindingEffect {
public ItemMedicalGag() {
super(new Item.Properties().stacksTo(16), GagMaterial.PANEL);
}
@Override
public String getTextureSubfolder() {
return "straps";
}
}

View File

@@ -0,0 +1,90 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.util.TiedUpSounds;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
/**
* Paddle - Tool for disciplining NPCs (gentle discipline).
*
* Phase 7: Basic paddle
* Refactored: Tighten moved to keybind, paddle now only does discipline on NPCs
*/
public class ItemPaddle extends Item {
public ItemPaddle() {
super(
new Item.Properties()
.stacksTo(1) // Paddles don't stack (tool)
.durability(64) // 64 uses
);
}
/**
* Called when player right-clicks another entity with the paddle.
* Applies gentle discipline to NPCs.
*
* Note: Tighten functionality moved to keybind (key.tiedup.tighten)
*
* @param stack The item stack
* @param player The player using the paddle
* @param target The entity being interacted with
* @param hand The hand holding the paddle
* @return SUCCESS if discipline applied, PASS otherwise
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Only run on server side
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// NPC discipline - visual/sound feedback only (no personality effect)
if (target instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npc) {
// Visual feedback - gentler than whip
TiedUpSounds.playSlapSound(target);
if (player.level() instanceof ServerLevel serverLevel) {
serverLevel.sendParticles(
ParticleTypes.SMOKE,
target.getX(),
target.getY() + target.getBbHeight() / 2.0,
target.getZ(),
5,
0.3,
0.3,
0.3,
0.05
);
}
// Consume durability
stack.hurtAndBreak(1, player, p -> p.broadcastBreakEvent(hand));
TiedUpMod.LOGGER.debug(
"[ItemPaddle] {} disciplined {} with paddle",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
// Paddle only works on NPCs now
// Use keybind (T) to tighten binds on any target
return InteractionResult.PASS;
}
}

View File

@@ -0,0 +1,45 @@
package com.tiedup.remake.items;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Padlock - Used to make bondage items lockable.
*
* Phase 20: Anvil-based padlock attachment
*
* Usage:
* - Combine with a bondage item (ILockable) in an Anvil
* - The item becomes "lockable" (can be locked with a Key)
* - Cost: 1 XP level
*
* @see com.tiedup.remake.events.system.AnvilEventHandler
*/
public class ItemPadlock extends Item {
public ItemPadlock() {
super(new Item.Properties().stacksTo(16));
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
tooltip.add(
Component.translatable("item.tiedup.padlock.tooltip").withStyle(
ChatFormatting.GRAY
)
);
}
}

View File

@@ -0,0 +1,289 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Rag - Can be soaked in chloroform to knock out targets
* Has wet/dry state managed via NBT.
*
* Phase 15: Full chloroform system implementation
*
* Usage:
* 1. Hold chloroform bottle, rag in offhand, right-click → rag becomes wet
* 2. Hold wet rag, right-click target → apply chloroform effect
* 3. Wet rag evaporates over time (configurable)
*
* Effects on target:
* - Slowness 127 (cannot move)
* - Blindness
* - Nausea
* - UNCONSCIOUS pose
*/
public class ItemRag extends Item {
private static final String NBT_WET = "wet";
private static final String NBT_WET_TIME = "wetTime"; // Ticks remaining
public ItemRag() {
super(new Item.Properties().stacksTo(16));
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
if (isWet(stack)) {
int ticksRemaining = getWetTime(stack);
int secondsRemaining = ticksRemaining / 20;
tooltip.add(
Component.literal("Soaked with chloroform").withStyle(
ChatFormatting.GREEN
)
);
tooltip.add(
Component.literal(
"Evaporates in: " + secondsRemaining + "s"
).withStyle(ChatFormatting.GRAY)
);
} else {
tooltip.add(
Component.literal("Dry - needs chloroform").withStyle(
ChatFormatting.GRAY
)
);
}
}
/**
* Called when player right-clicks another entity with the rag.
* If wet, applies chloroform effect to the target.
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Only run on server side
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Must be wet to apply chloroform
if (!isWet(stack)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.RAG_DRY
);
return InteractionResult.PASS;
}
// Apply chloroform to target
applyChloroformToTarget(target, player);
// The rag stays wet (can be used multiple times until it evaporates)
TiedUpMod.LOGGER.info(
"[ItemRag] {} applied chloroform to {}",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
/**
* Tick the rag to handle evaporation of wet state.
*/
@Override
public void inventoryTick(
ItemStack stack,
Level level,
Entity entity,
int slot,
boolean selected
) {
if (level.isClientSide) return;
if (isWet(stack)) {
int wetTime = getWetTime(stack);
if (wetTime > 0) {
setWetTime(stack, wetTime - 1);
} else {
// Evaporated
setWet(stack, false);
if (entity instanceof Player player) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.RAG_EVAPORATED
);
}
TiedUpMod.LOGGER.debug("[ItemRag] Chloroform evaporated");
}
}
}
/**
* Apply chloroform effect to a target.
* Effects: Slowness 127, Blindness, Nausea for configured duration.
*
* @param target The target entity
* @param player The player applying chloroform
*/
private void applyChloroformToTarget(LivingEntity target, Player player) {
// Get duration from config via SettingsAccessor (single source of truth)
int duration = SettingsAccessor.getChloroformDuration();
// Apply effects
// Slowness 127 = cannot move at all
target.addEffect(
new MobEffectInstance(
MobEffects.MOVEMENT_SLOWDOWN,
duration,
127,
false,
false
)
);
// Blindness
target.addEffect(
new MobEffectInstance(
MobEffects.BLINDNESS,
duration,
0,
false,
false
)
);
// Nausea (confusion)
target.addEffect(
new MobEffectInstance(
MobEffects.CONFUSION,
duration,
0,
false,
false
)
);
// Weakness (cannot fight back)
target.addEffect(
new MobEffectInstance(
MobEffects.WEAKNESS,
duration,
127,
false,
false
)
);
// If target is IRestrainable, call applyChloroform to apply effects
IRestrainable kidnapped = KidnappedHelper.getKidnappedState(target);
if (kidnapped != null) {
kidnapped.applyChloroform(duration);
}
TiedUpMod.LOGGER.debug(
"[ItemRag] Applied chloroform to target for {} seconds",
duration
);
}
// ========== Wet/Dry State Management ==========
/**
* Check if this rag is soaked with chloroform.
* @param stack The item stack
* @return true if wet with chloroform
*/
public static boolean isWet(ItemStack stack) {
if (stack.isEmpty()) return false;
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_WET);
}
/**
* Set the wet state of this rag.
* @param stack The item stack
* @param wet true to make wet, false for dry
*/
public static void setWet(ItemStack stack, boolean wet) {
if (stack.isEmpty()) return;
stack.getOrCreateTag().putBoolean(NBT_WET, wet);
if (!wet) {
// Clear wet time when drying
stack.getOrCreateTag().remove(NBT_WET_TIME);
}
}
/**
* Get the remaining wet time in ticks.
* @param stack The item stack
* @return Ticks remaining, or 0 if not wet
*/
public static int getWetTime(ItemStack stack) {
if (stack.isEmpty()) return 0;
CompoundTag tag = stack.getTag();
return tag != null ? tag.getInt(NBT_WET_TIME) : 0;
}
/**
* Set the remaining wet time in ticks.
* @param stack The item stack
* @param ticks Ticks remaining
*/
public static void setWetTime(ItemStack stack, int ticks) {
if (stack.isEmpty()) return;
stack.getOrCreateTag().putInt(NBT_WET_TIME, ticks);
}
/**
* Soak this rag with chloroform.
* Sets wet = true and initializes the evaporation timer.
*
* @param stack The item stack
* @param wetTime Time in ticks before evaporation
*/
public static void soak(ItemStack stack, int wetTime) {
if (stack.isEmpty()) return;
setWet(stack, true);
setWetTime(stack, wetTime);
TiedUpMod.LOGGER.debug(
"[ItemRag] Soaked with chloroform ({} ticks)",
wetTime
);
}
/**
* Get the default wet time for soaking.
* @return Default wet time in ticks
*/
public static int getDefaultWetTime() {
return ModConfig.SERVER.ragWetTime.get();
}
}

View File

@@ -0,0 +1,44 @@
package com.tiedup.remake.items;
import com.tiedup.remake.entities.EntityRopeArrow;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.projectile.AbstractArrow;
import net.minecraft.world.item.ArrowItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Rope Arrow - Arrow that ties up targets on hit
* When fired from a bow and hits an entity, it has 75% chance to bind them.
*
* Phase 15: Full rope arrow implementation
*
* Behavior:
* - Works like regular arrows for firing
* - On hit: 75% chance to bind the target with rope
* - Target must be IRestrainable (Player, Damsel, Kidnapper)
*/
public class ItemRopeArrow extends ArrowItem {
public ItemRopeArrow() {
super(new Properties().stacksTo(64));
}
/**
* Create the arrow entity when fired from a bow.
* Returns EntityRopeArrow for special binding behavior on hit.
*
* @param level The world
* @param stack The arrow item stack
* @param shooter The entity firing the bow
* @return EntityRopeArrow instance
*/
@Override
public AbstractArrow createArrow(
Level level,
ItemStack stack,
LivingEntity shooter
) {
return new EntityRopeArrow(level, shooter);
}
}

View File

@@ -0,0 +1,133 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Shock Collar - Advanced collar that can be remotely triggered.
*
* <p>Mechanics:</p>
* <ul>
* <li><b>Remote Shocking:</b> Can be triggered by anyone holding a linked Shocker Controller.</li>
* <li><b>Struggle Penalty:</b> If locked, has a chance to shock the wearer during struggle attempts, interrupting them.</li>
* <li><b>Public Mode:</b> Can be set to public mode, allowing anyone to shock the wearer even if they aren't the owner.</li>
* </ul>
*/
public class ItemShockCollar extends ItemCollar {
private static final String NBT_PUBLIC_MODE = "public_mode";
public ItemShockCollar() {
super(new Item.Properties());
}
@Override
public boolean canShock() {
return true;
}
/**
* Shows current mode (PUBLIC/PRIVATE) and usage instructions in tooltip.
*/
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
tooltip.add(
Component.literal("Shock Feature: ")
.withStyle(ChatFormatting.YELLOW)
.append(
Component.literal(
isPublic(stack) ? "PUBLIC" : "PRIVATE"
).withStyle(
isPublic(stack)
? ChatFormatting.GREEN
: ChatFormatting.RED
)
)
);
tooltip.add(
Component.literal("Shift + Right-click to toggle public mode")
.withStyle(ChatFormatting.DARK_GRAY)
.withStyle(ChatFormatting.ITALIC)
);
}
/**
* Toggles Public mode when shift-right-clicking in air.
*/
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
boolean newState = !isPublic(stack);
setPublic(stack, newState);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.SHOCKER_MODE_SET,
(newState ? "PUBLIC" : "PRIVATE")
);
}
return InteractionResultHolder.sidedSuccess(
stack,
level.isClientSide()
);
}
return super.use(level, player, hand);
}
/**
* Handles the risk of shocking the wearer during a struggle attempt.
*
* NOTE: For the new continuous struggle mini-game, shock logic is handled
* directly in MiniGameSessionManager.tickContinuousSessions(). This method
* is now a no-op that always returns true, kept for API compatibility.
*
* @param entity The wearer of the collar
* @param stack The collar instance
* @return Always true (shock logic moved to MiniGameSessionManager)
*/
public boolean notifyStruggle(LivingEntity entity, ItemStack stack) {
// Shock collar checks during continuous struggle are now handled by
// MiniGameSessionManager.shouldTriggerShock() with 10% chance every 5 seconds.
// This method is kept for backwards compatibility but no longer performs the check.
return true;
}
public boolean isPublic(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_PUBLIC_MODE);
}
public void setPublic(ItemStack stack, boolean publicMode) {
stack.getOrCreateTag().putBoolean(NBT_PUBLIC_MODE, publicMode);
}
}

View File

@@ -0,0 +1,60 @@
package com.tiedup.remake.items;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
/**
* Automatic Shock Collar - Shocks the wearer at regular intervals.
*
* Phase 14.1.5: Refactored to support IRestrainable (LivingEntity + NPCs)
*
* <p>Mechanics:</p>
* <ul>
* <li><b>Self-Triggering:</b> Has an internal timer stored in NBT that shocks the entity when it reaches 0.</li>
* <li><b>Unstruggable:</b> By default, cannot be escaped via struggle mechanics (requires key).</li>
* </ul>
*/
public class ItemShockCollarAuto extends ItemShockCollar {
private final int interval;
/**
* @param interval Frequency of shocks in TICKS (20 ticks = 1 second).
*/
public ItemShockCollarAuto() {
this(600); // 30 seconds default
}
public ItemShockCollarAuto(int interval) {
super();
this.interval = interval;
}
public int getInterval() {
return interval;
}
/**
* Ensures the internal shock timer is cleaned up when the item is removed.
*
* Phase 14.1.5: Refactored to support IRestrainable (LivingEntity + NPCs)
*/
@Override
public void onUnequipped(ItemStack stack, LivingEntity entity) {
IRestrainable state = KidnappedHelper.getKidnappedState(entity);
if (state != null) {
state.resetAutoShockTimer();
}
super.onUnequipped(stack, entity);
}
/**
* Prevents escaping through struggle mechanics for this specific collar type.
*/
@Override
public boolean canBeStruggledOut(ItemStack stack) {
return false;
}
}

View File

@@ -0,0 +1,398 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.ModSounds;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.items.base.ItemOwnerTarget;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
public class ItemShockerController extends ItemOwnerTarget {
private static final String NBT_BROADCAST = "broadcast";
private static final String NBT_RADIUS = "radius";
public ItemShockerController() {
super(new net.minecraft.world.item.Item.Properties().stacksTo(1));
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
appendOwnerTooltip(stack, tooltip, "Right-click a player");
if (isBroadcastEnabled(stack)) {
tooltip.add(
Component.literal("MODE: BROADCAST").withStyle(
ChatFormatting.DARK_RED
)
);
tooltip.add(
Component.literal("(Affects ALL your slaves in radius)")
.withStyle(ChatFormatting.GRAY)
.withStyle(ChatFormatting.ITALIC)
);
} else {
tooltip.add(
Component.literal("MODE: TARGETED").withStyle(
ChatFormatting.BLUE
)
);
if (hasTarget(stack)) {
String displayName = getTargetName(stack);
boolean isDisconnected = true;
if (level != null) {
Player target = level.getPlayerByUUID(getTargetId(stack));
if (target != null) {
IRestrainable targetState =
KidnappedHelper.getKidnappedState(target);
if (targetState != null && targetState.hasCollar()) {
isDisconnected = false;
ItemStack collar = targetState.getEquipment(BodyRegionV2.NECK);
if (
collar.getItem() instanceof
ItemCollar collarItem &&
collarItem.hasNickname(collar)
) {
displayName =
collarItem.getNickname(collar) +
" (" +
displayName +
")";
}
}
}
}
MutableComponent targetComp = Component.literal(" > ")
.withStyle(ChatFormatting.BLUE)
.append(
Component.literal(displayName).withStyle(
isDisconnected
? ChatFormatting.STRIKETHROUGH
: ChatFormatting.WHITE
)
);
if (isDisconnected) {
targetComp.append(
Component.literal(" [FREED]")
.withStyle(ChatFormatting.RED)
.withStyle(ChatFormatting.BOLD)
);
}
tooltip.add(targetComp);
} else {
tooltip.add(
Component.literal(" > No target connected").withStyle(
ChatFormatting.GRAY
)
);
}
}
tooltip.add(
Component.literal("Radius: " + getRadius(stack) + "m").withStyle(
ChatFormatting.GREEN
)
);
tooltip.add(
Component.literal("Shift + Right-click to toggle Broadcast mode")
.withStyle(ChatFormatting.DARK_GRAY)
.withStyle(ChatFormatting.ITALIC)
);
}
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
if (hasOwner(stack) && !isOwner(stack, player)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.SHOCKER_NOT_OWNER
);
return InteractionResultHolder.fail(stack);
}
boolean newState = !isBroadcastEnabled(stack);
setBroadcastEnabled(stack, newState);
player.setItemInHand(hand, stack);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.SHOCKER_MODE_SET,
(newState ? "BROADCAST" : "TARGETED")
);
}
return InteractionResultHolder.sidedSuccess(
stack,
level.isClientSide()
);
}
if (level.isClientSide) return InteractionResultHolder.success(stack);
IRestrainable playerState = KidnappedHelper.getKidnappedState(player);
if (
playerState != null && playerState.isTiedUp()
) return InteractionResultHolder.fail(stack);
if (!hasOwner(stack)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"You must claim this shocker first! (Right-click a player)"
);
return InteractionResultHolder.fail(stack);
}
if (!isOwner(stack, player)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.SHOCKER_NOT_OWNER
);
return InteractionResultHolder.fail(stack);
}
List<LivingEntity> nearbyTargets = getNearbyKidnappedTargets(
level,
player,
stack
);
if (isBroadcastEnabled(stack)) {
for (LivingEntity target : nearbyTargets) {
IRestrainable targetState = KidnappedHelper.getKidnappedState(
target
);
if (targetState != null) targetState.shockKidnapped();
}
if (nearbyTargets.isEmpty()) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"No valid targets in range!"
);
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.INFO,
"Broadcast shock triggered! (" +
nearbyTargets.size() +
" targets)"
);
playTriggerSound(player);
}
} else if (hasTarget(stack)) {
Player target = level.getPlayerByUUID(getTargetId(stack));
IRestrainable targetState =
target != null
? KidnappedHelper.getKidnappedState(target)
: null;
if (
target != null &&
targetState != null &&
targetState.hasCollar() &&
nearbyTargets.contains(target)
) {
targetState.shockKidnapped();
String name = target.getName().getString();
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.SHOCKER_TRIGGERED,
name
);
playTriggerSound(player);
} else {
String error = (target == null)
? "Target is out of range or in another dimension!"
: (!targetState.hasCollar()
? "Target is no longer wearing a collar!"
: "Target is out of range!");
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
error
);
}
} else {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.ERROR,
"No target set and broadcast is disabled!"
);
}
return InteractionResultHolder.success(stack);
}
/**
* Phase 14.1.5: Refactored to support IRestrainable (LivingEntity + NPCs)
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
if (player.level().isClientSide) return InteractionResult.SUCCESS;
IRestrainable playerState = KidnappedHelper.getKidnappedState(player);
if (
playerState != null && playerState.isTiedUp()
) return InteractionResult.FAIL;
// Claim shocker if unclaimed
if (!hasOwner(stack)) {
setOwner(stack, player);
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.SHOCKER_CLAIMED
);
} else if (!isOwner(stack, player)) {
SystemMessageManager.sendToPlayer(
player,
SystemMessageManager.MessageCategory.SHOCKER_NOT_OWNER
);
return InteractionResult.FAIL;
}
// Connect to target (works with any LivingEntity that can be kidnapped)
IRestrainable targetState = KidnappedHelper.getKidnappedState(target);
if (targetState != null) {
setTarget(stack, target);
player.setItemInHand(hand, stack);
SystemMessageManager.sendChatToPlayer(
player,
"Connected to " + target.getName().getString(),
ChatFormatting.GREEN
);
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
/**
* Phase 14.1.5: New method to support LivingEntity (Players + NPCs)
* Returns all kidnappable entities in range wearing shock collars owned by the shocker owner or in public mode.
*/
private List<LivingEntity> getNearbyKidnappedTargets(
Level level,
Player source,
ItemStack stack
) {
double radius = getRadius(stack);
UUID ownerId = getOwnerId(stack);
List<LivingEntity> targets = new ArrayList<>();
// Check all living entities in range
for (LivingEntity entity : level.getEntitiesOfClass(
LivingEntity.class,
source.getBoundingBox().inflate(radius)
)) {
if (entity == source) continue;
IRestrainable state = KidnappedHelper.getKidnappedState(entity);
if (state != null && state.hasCollar()) {
ItemStack collarStack = state.getEquipment(BodyRegionV2.NECK);
if (
collarStack.getItem() instanceof ItemShockCollar collarItem
) {
if (
collarItem.getOwners(collarStack).contains(ownerId) ||
collarItem.isPublic(collarStack)
) {
targets.add(entity);
}
}
}
}
return targets;
}
private void playTriggerSound(Player player) {
player
.level()
.playSound(
null,
player.blockPosition(),
ModSounds.SHOCKER_ACTIVATED.get(),
net.minecraft.sounds.SoundSource.PLAYERS,
0.5f,
1.0f
);
}
public boolean isBroadcastEnabled(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_BROADCAST);
}
public void setBroadcastEnabled(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_BROADCAST, enabled);
}
public int getRadius(ItemStack stack) {
CompoundTag tag = stack.getTag();
return (tag != null && tag.contains(NBT_RADIUS))
? tag.getInt(NBT_RADIUS)
: com.tiedup.remake.core.SettingsAccessor.getShockerControllerRadius(null);
}
public void setRadius(ItemStack stack, int radius) {
stack.getOrCreateTag().putInt(NBT_RADIUS, radius);
}
public static ItemStack mergeShockers(List<ItemStack> stacks) {
if (stacks == null || stacks.size() <= 1) return ItemStack.EMPTY;
int totalRadius = 0;
for (ItemStack s : stacks) {
if (s.getItem() instanceof ItemShockerController sc) {
totalRadius += sc.getRadius(s);
}
}
ItemStack result = new ItemStack(stacks.get(0).getItem());
((ItemShockerController) result.getItem()).setRadius(
result,
totalRadius
);
return result;
}
}

View File

@@ -0,0 +1,116 @@
package com.tiedup.remake.items;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.ModSounds;
import java.util.UUID;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
/**
* Taser - Kidnapper's defensive weapon
*
* Used by kidnappers when attacked while holding a captive.
* On hit:
* - Plays electric shock sound
* - Applies Slowness I + Weakness I for 5 seconds
* - Deals 3 hearts of damage
*/
public class ItemTaser extends Item {
/** UUID for the attack damage modifier */
private static final UUID ATTACK_DAMAGE_UUID = UUID.fromString(
"CB3F55D3-645C-4F38-A497-9C13A33DB5CF"
);
private final Multimap<Attribute, AttributeModifier> defaultModifiers;
public ItemTaser() {
super(new Item.Properties().stacksTo(1).durability(64));
// Build attribute modifiers for attack damage
// NOTE: Using default value 5.0 (ModConfig not loaded yet during item registration)
ImmutableMultimap.Builder<Attribute, AttributeModifier> builder =
ImmutableMultimap.builder();
builder.put(
Attributes.ATTACK_DAMAGE,
new AttributeModifier(
ATTACK_DAMAGE_UUID,
"Weapon modifier",
5.0, // Default damage (matches ModConfig default)
AttributeModifier.Operation.ADDITION
)
);
this.defaultModifiers = builder.build();
}
/**
* Called when this item is used to attack an entity.
* Applies shock effects on successful hit.
*/
@Override
public boolean hurtEnemy(
ItemStack stack,
LivingEntity target,
LivingEntity attacker
) {
// Play electric shock sound
target
.level()
.playSound(
null,
target.blockPosition(),
ModSounds.ELECTRIC_SHOCK.get(),
SoundSource.HOSTILE,
1.0f,
1.0f
);
int duration = ModConfig.SERVER.taserStunDuration.get();
// Apply Slowness I
target.addEffect(
new MobEffectInstance(
MobEffects.MOVEMENT_SLOWDOWN,
duration,
0 // Amplifier 0 = level I
)
);
// Apply Weakness I
target.addEffect(
new MobEffectInstance(
MobEffects.WEAKNESS,
duration,
0 // Amplifier 0 = level I
)
);
// Consume durability
stack.hurtAndBreak(1, attacker, e ->
e.broadcastBreakEvent(EquipmentSlot.MAINHAND)
);
return true;
}
/**
* Get attribute modifiers for this item when equipped.
*/
@Override
public Multimap<Attribute, AttributeModifier> getDefaultAttributeModifiers(
EquipmentSlot slot
) {
return slot == EquipmentSlot.MAINHAND
? this.defaultModifiers
: super.getDefaultAttributeModifiers(slot);
}
}

View File

@@ -0,0 +1,86 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.TiedUpMod;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.registries.ForgeRegistries;
import org.jetbrains.annotations.NotNull;
/**
* TiedUp! Guide Book Item
*
* When used, gives the player the Patchouli guide_book item with the correct NBT.
* If Patchouli is not installed, displays a message.
*/
public class ItemTiedUpGuide extends Item {
public ItemTiedUpGuide() {
super(new Item.Properties().stacksTo(1));
}
@Override
public @NotNull InteractionResultHolder<ItemStack> use(
@NotNull Level level,
@NotNull Player player,
@NotNull InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
if (!level.isClientSide()) {
// Check if Patchouli is installed
if (!ModList.get().isLoaded("patchouli")) {
player.displayClientMessage(
Component.literal(
"§cPatchouli is not installed! Install it to use this guide."
),
false
);
return InteractionResultHolder.fail(stack);
}
// Get the Patchouli guide_book item
Item guideBookItem = ForgeRegistries.ITEMS.getValue(
ResourceLocation.fromNamespaceAndPath("patchouli", "guide_book")
);
if (guideBookItem == null) {
player.displayClientMessage(
Component.literal(
"§cFailed to find Patchouli guide_book item."
),
false
);
return InteractionResultHolder.fail(stack);
}
// Create the guide book with NBT pointing to our book
ItemStack guideBook = new ItemStack(guideBookItem);
CompoundTag nbt = new CompoundTag();
nbt.putString("patchouli:book", TiedUpMod.MOD_ID + ":guide");
guideBook.setTag(nbt);
// Give the player the guide book
if (!player.getInventory().add(guideBook)) {
// Drop if inventory is full
player.drop(guideBook, false);
}
// Consume this item
stack.shrink(1);
player.displayClientMessage(
Component.literal("§aReceived TiedUp! Guide Book!"),
true
);
}
return InteractionResultHolder.consume(stack);
}
}

View File

@@ -0,0 +1,71 @@
package com.tiedup.remake.items;
import java.util.List;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Rarity;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
/**
* ItemToken - Access pass for kidnapper camps.
*
* Slave Trader & Maid System
*
* Behavior:
* - Reusable (no durability, permanent)
* - Drop: 5% chance from killed kidnappers
* - Effect: Kidnappers won't target the holder
* - Effect: Allows peaceful interaction with SlaveTrader
*
* When a player has a token in their inventory:
* - EntityKidnapper.canTarget() returns false
* - EntitySlaveTrader opens trade menu instead of attacking
*/
public class ItemToken extends Item {
public ItemToken() {
super(new Item.Properties().stacksTo(1).rarity(Rarity.RARE));
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
tooltip.add(
Component.literal("Camp Access Token").withStyle(
ChatFormatting.GOLD,
ChatFormatting.BOLD
)
);
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Kidnappers won't target you").withStyle(
ChatFormatting.GREEN
)
);
tooltip.add(
Component.literal("Allows trading with Slave Traders").withStyle(
ChatFormatting.GREEN
)
);
tooltip.add(Component.literal(""));
tooltip.add(
Component.literal("Keep in your inventory for effect").withStyle(
ChatFormatting.GRAY,
ChatFormatting.ITALIC
)
);
}
@Override
public boolean isFoil(ItemStack stack) {
return true; // Always glowing to indicate special item
}
}

View File

@@ -0,0 +1,181 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.TiedUpSounds;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
/**
* Whip - Tool for discipline
* Right-click a tied entity to deal damage and decrease their resistance.
*
* Phase 15: Full whip mechanics implementation
*
* Effects:
* - Deals damage (configurable)
* - Decreases bind resistance (configurable)
* - Plays whip crack sound
* - Shows damage particles
* - Consumes durability
*
* Opposite of paddle (which increases resistance).
*/
public class ItemWhip extends Item {
public ItemWhip() {
super(new Item.Properties().stacksTo(1).durability(256));
}
/**
* Called when player right-clicks another entity with the whip.
* Deals damage and decreases resistance if target is restrained.
*
* @param stack The item stack
* @param player The player using the whip
* @param target The entity being interacted with
* @param hand The hand holding the whip
* @return SUCCESS if whipping happened, PASS otherwise
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Only run on server side
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// NPC whip - visual/sound feedback only (no personality effect)
if (target instanceof EntityDamsel damsel) {
// Set whip time for anti-flee system (stops fleeing for ~10 seconds)
damsel.setLastWhipTime(player.level().getGameTime());
// Visual feedback
TiedUpSounds.playWhipSound(target);
if (player.level() instanceof ServerLevel serverLevel) {
serverLevel.sendParticles(
ParticleTypes.CRIT,
target.getX(),
target.getY() + target.getBbHeight() / 2.0,
target.getZ(),
10,
0.5,
0.5,
0.5,
0.1
);
}
// Consume durability
stack.hurtAndBreak(1, player, p -> p.broadcastBreakEvent(hand));
return InteractionResult.SUCCESS;
}
// Check if target can be restrained (Player, EntityDamsel, EntityKidnapper)
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null || !targetState.isTiedUp()) {
return InteractionResult.PASS;
}
float damage = ModConfig.SERVER.whipDamage.get().floatValue();
int resistanceDecrease = ModConfig.SERVER.whipResistanceDecrease.get();
// 1. Play whip sound
TiedUpSounds.playWhipSound(target);
// 2. Deal damage
DamageSource damageSource = player.damageSources().playerAttack(player);
target.hurt(damageSource, damage);
// 3. Show damage particles (critical hit particles)
if (player.level() instanceof ServerLevel serverLevel) {
serverLevel.sendParticles(
ParticleTypes.CRIT,
target.getX(),
target.getY() + target.getBbHeight() / 2.0,
target.getZ(),
10, // count
0.5,
0.5,
0.5, // spread
0.1 // speed
);
}
// 4. Decrease resistance
decreaseResistance(targetState, target, resistanceDecrease);
// 5. Damage the whip (consume durability)
stack.hurtAndBreak(1, player, p -> {
p.broadcastBreakEvent(hand);
});
TiedUpMod.LOGGER.debug(
"[ItemWhip] {} whipped {} (damage: {}, resistance -{})",
player.getName().getString(),
target.getName().getString(),
damage,
resistanceDecrease
);
return InteractionResult.SUCCESS;
}
/**
* Decrease the target's bind resistance.
* Works for both players (via PlayerBindState) and NPCs.
*
* @param targetState The target's IBondageState state
* @param target The target entity
* @param amount The amount to decrease
*/
private void decreaseResistance(
IBondageState targetState,
LivingEntity target,
int amount
) {
if (target instanceof Player player) {
// For players, use PlayerBindState
PlayerBindState bindState = PlayerBindState.getInstance(player);
int currentResistance = bindState.getCurrentBindResistance();
int newResistance = Math.max(0, currentResistance - amount);
bindState.setCurrentBindResistance(newResistance);
// MEDIUM FIX: Sync resistance change to client
// Resistance is stored in bind item NBT, so we must sync inventory
// Without this, client still shows old resistance value in UI
// Sync V2 equipment (resistance NBT changed on the stored ItemStack)
if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.sync(serverPlayer);
}
TiedUpMod.LOGGER.debug(
"[ItemWhip] Player resistance: {} -> {}",
currentResistance,
newResistance
);
} else {
// For NPCs, resistance is not tracked the same way
// Just log the whip action (NPC doesn't struggle, so resistance is less relevant)
TiedUpMod.LOGGER.debug(
"[ItemWhip] Whipped NPC (resistance not tracked for NPCs)"
);
}
}
}

View File

@@ -0,0 +1,215 @@
package com.tiedup.remake.items;
import com.tiedup.remake.blocks.ModBlocks;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.KidnapperItemSelector;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.v2.V2Items;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.RegistryObject;
/**
* Creative Mode Tabs Registration
* Defines the creative inventory tabs where TiedUp items will appear.
*
* Updated to use factory pattern with enum-based item registration.
*/
@SuppressWarnings("null") // Minecraft API guarantees non-null returns
public class ModCreativeTabs {
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, TiedUpMod.MOD_ID);
public static final RegistryObject<CreativeModeTab> TIEDUP_TAB =
CREATIVE_MODE_TABS.register("tiedup_tab", () ->
CreativeModeTab.builder()
.title(Component.translatable("itemGroup.tiedup"))
.icon(() -> new ItemStack(ModItems.getBind(BindVariant.ROPES)))
.displayItems((parameters, output) -> {
// ========== BINDS (from enum) ==========
for (BindVariant variant : BindVariant.values()) {
// Add base item
output.accept(ModItems.getBind(variant));
// Add colored variants if supported
if (variant.supportsColor()) {
for (ItemColor color : ItemColor.values()) {
// Skip special colors (caution, clear) except for duct tape
if (
color.isSpecial() &&
variant != BindVariant.DUCT_TAPE
) continue;
// Use validation method to check if color has texture
if (
KidnapperItemSelector.isColorValidForBind(
color,
variant
)
) {
output.accept(
KidnapperItemSelector.createBind(
variant,
color
)
);
}
}
}
}
// ========== GAGS (from enum) ==========
for (GagVariant variant : GagVariant.values()) {
// Add base item
output.accept(ModItems.getGag(variant));
// Add colored variants if supported
if (variant.supportsColor()) {
for (ItemColor color : ItemColor.values()) {
// Skip special colors (caution, clear) except for tape gag
if (
color.isSpecial() &&
variant != GagVariant.TAPE_GAG
) continue;
// Use validation method to check if color has texture
if (
KidnapperItemSelector.isColorValidForGag(
color,
variant
)
) {
output.accept(
KidnapperItemSelector.createGag(
variant,
color
)
);
}
}
}
}
// ========== BLINDFOLDS (from enum) ==========
for (BlindfoldVariant variant : BlindfoldVariant.values()) {
// Add base item
output.accept(ModItems.getBlindfold(variant));
// Add colored variants if supported
if (variant.supportsColor()) {
for (ItemColor color : ItemColor.values()) {
// Skip special colors for blindfolds
if (color.isSpecial()) continue;
// Use validation method to check if color has texture
if (
KidnapperItemSelector.isColorValidForBlindfold(
color,
variant
)
) {
output.accept(
KidnapperItemSelector.createBlindfold(
variant,
color
)
);
}
}
}
}
// Hood (combo item, not in enum)
output.accept(ModItems.HOOD.get());
// ========== 3D ITEMS ==========
output.accept(ModItems.BALL_GAG_3D.get());
// ========== COMBO ITEMS ==========
output.accept(ModItems.MEDICAL_GAG.get());
// ========== CLOTHES ==========
output.accept(ModItems.CLOTHES.get());
// ========== COLLARS ==========
output.accept(ModItems.CLASSIC_COLLAR.get());
output.accept(ModItems.CHOKE_COLLAR.get());
output.accept(ModItems.SHOCK_COLLAR.get());
output.accept(ModItems.GPS_COLLAR.get());
// ========== EARPLUGS (from enum) ==========
for (EarplugsVariant variant : EarplugsVariant.values()) {
output.accept(ModItems.getEarplugs(variant));
}
// ========== MITTENS (from enum) ==========
for (MittensVariant variant : MittensVariant.values()) {
output.accept(ModItems.getMittens(variant));
}
// ========== KNIVES (from enum) ==========
for (KnifeVariant variant : KnifeVariant.values()) {
output.accept(ModItems.getKnife(variant));
}
// ========== OTHER TOOLS ==========
output.accept(ModItems.WHIP.get());
output.accept(ModItems.PADDLE.get());
output.accept(ModItems.SHOCKER_CONTROLLER.get());
output.accept(ModItems.GPS_LOCATOR.get());
output.accept(ModItems.COLLAR_KEY.get());
output.accept(ModItems.LOCKPICK.get());
output.accept(ModItems.COMMAND_WAND.get());
// ========== SPECIAL ITEMS ==========
output.accept(ModItems.CHLOROFORM_BOTTLE.get());
output.accept(ModItems.RAG.get());
output.accept(ModItems.PADLOCK.get());
output.accept(ModItems.MASTER_KEY.get());
output.accept(ModItems.ROPE_ARROW.get());
output.accept(ModItems.TOKEN.get());
// ========== SPAWN EGGS ==========
output.accept(ModItems.DAMSEL_SPAWN_EGG.get());
// ========== GUIDE BOOK ==========
output.accept(ModItems.TIEDUP_GUIDE.get());
// ========== BLOCKS ==========
output.accept(ModBlocks.PADDED_BLOCK.get());
output.accept(ModBlocks.PADDED_SLAB.get());
output.accept(ModBlocks.PADDED_STAIRS.get());
output.accept(ModBlocks.ROPE_TRAP.get());
output.accept(ModBlocks.KIDNAP_BOMB.get());
output.accept(ModBlocks.TRAPPED_CHEST.get());
output.accept(ModBlocks.CELL_DOOR.get());
output.accept(ModBlocks.CELL_CORE.get());
// ========== V2 PET FURNITURE ==========
output.accept(V2Items.PET_BOWL.get());
output.accept(V2Items.PET_BED.get());
output.accept(V2Items.PET_CAGE.get());
// ========== V2 BONDAGE ITEMS ==========
output.accept(com.tiedup.remake.v2.bondage.V2BondageItems.V2_HANDCUFFS.get());
// ========== DATA-DRIVEN BONDAGE ITEMS ==========
for (com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition def :
com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry.getAll()) {
output.accept(
com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.createStack(def.id())
);
}
// ========== FURNITURE PLACER ITEMS ==========
for (com.tiedup.remake.v2.furniture.FurnitureDefinition def :
com.tiedup.remake.v2.furniture.FurnitureRegistry.getAll()) {
output.accept(
com.tiedup.remake.v2.furniture.FurniturePlacerItem.createStack(def.id())
);
}
})
.build()
);
}

View File

@@ -0,0 +1,411 @@
package com.tiedup.remake.items;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.ModEntities;
import com.tiedup.remake.items.base.*;
import com.tiedup.remake.items.bondage3d.gags.ItemBallGag3D;
import com.tiedup.remake.items.clothes.GenericClothes;
import java.util.EnumMap;
import java.util.Map;
import net.minecraft.world.item.Item;
import net.minecraftforge.common.ForgeSpawnEggItem;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
/**
* Mod Items Registration
* Handles registration of all TiedUp items using DeferredRegister.
*
* Refactored with Factory Pattern:
* - Binds, Gags, Blindfolds, Earplugs, Knives use EnumMaps and factory methods
* - Complex items (collars, whip, chloroform, etc.) remain individual registrations
*
* Usage:
* - ModItems.getBind(BindVariant.ROPES) - Get a specific bind item
* - ModItems.getGag(GagVariant.BALL_GAG) - Get a specific gag item
* - ModItems.WHIP.get() - Get complex items directly
*/
public class ModItems {
// DeferredRegister for items
public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(
ForgeRegistries.ITEMS,
TiedUpMod.MOD_ID
);
// ========== FACTORY-BASED ITEMS ==========
/**
* All bind items (15 variants via BindVariant enum)
*/
public static final Map<BindVariant, RegistryObject<Item>> BINDS =
registerAllBinds();
/**
* All gag items (via GagVariant enum)
* Note: ItemMedicalGag is registered separately as it has special behavior
* Note: BALL_GAG_3D is a separate 3D item (not in enum)
*/
public static final Map<GagVariant, RegistryObject<Item>> GAGS =
registerAllGags();
/**
* Ball Gag 3D - Uses 3D OBJ model rendering via dedicated class.
* This is a separate item from BALL_GAG (which uses 2D textures).
*/
public static final RegistryObject<Item> BALL_GAG_3D = ITEMS.register(
"ball_gag_3d",
ItemBallGag3D::new
);
/**
* All blindfold items (2 variants via BlindfoldVariant enum)
*/
public static final Map<BlindfoldVariant, RegistryObject<Item>> BLINDFOLDS =
registerAllBlindfolds();
/**
* All earplugs items (1 variant via EarplugsVariant enum)
*/
public static final Map<EarplugsVariant, RegistryObject<Item>> EARPLUGS =
registerAllEarplugs();
/**
* All knife items (3 variants via KnifeVariant enum)
*/
public static final Map<KnifeVariant, RegistryObject<Item>> KNIVES =
registerAllKnives();
/**
* All mittens items (1 variant via MittensVariant enum)
* Phase 14.4: Blocks hand interactions when equipped
*/
public static final Map<MittensVariant, RegistryObject<Item>> MITTENS =
registerAllMittens();
/**
* Clothes item - uses dynamic textures from URLs.
* Users can create presets via anvil naming.
*/
public static final RegistryObject<Item> CLOTHES = ITEMS.register(
"clothes",
GenericClothes::new
);
// ========== COMPLEX ITEMS (individual registrations) ==========
// Medical gag - combo item with IHasBlindingEffect
public static final RegistryObject<Item> MEDICAL_GAG = ITEMS.register(
"medical_gag",
ItemMedicalGag::new
);
// Hood - combo item
public static final RegistryObject<Item> HOOD = ITEMS.register(
"hood",
ItemHood::new
);
// Collars - complex logic
public static final RegistryObject<Item> CLASSIC_COLLAR = ITEMS.register(
"classic_collar",
ItemClassicCollar::new
);
public static final RegistryObject<Item> SHOCK_COLLAR = ITEMS.register(
"shock_collar",
ItemShockCollar::new
);
public static final RegistryObject<Item> SHOCK_COLLAR_AUTO = ITEMS.register(
"shock_collar_auto",
ItemShockCollarAuto::new
);
public static final RegistryObject<Item> GPS_COLLAR = ITEMS.register(
"gps_collar",
ItemGpsCollar::new
);
// Choke Collar - Pet play collar used by Masters
public static final RegistryObject<Item> CHOKE_COLLAR = ITEMS.register(
"choke_collar",
ItemChokeCollar::new
);
// Tools with complex behavior
public static final RegistryObject<Item> WHIP = ITEMS.register(
"whip",
ItemWhip::new
);
public static final RegistryObject<Item> CHLOROFORM_BOTTLE = ITEMS.register(
"chloroform_bottle",
ItemChloroformBottle::new
);
public static final RegistryObject<Item> RAG = ITEMS.register(
"rag",
ItemRag::new
);
public static final RegistryObject<Item> PADLOCK = ITEMS.register(
"padlock",
ItemPadlock::new
);
public static final RegistryObject<Item> MASTER_KEY = ITEMS.register(
"master_key",
ItemMasterKey::new
);
public static final RegistryObject<Item> ROPE_ARROW = ITEMS.register(
"rope_arrow",
ItemRopeArrow::new
);
public static final RegistryObject<Item> PADDLE = ITEMS.register(
"paddle",
ItemPaddle::new
);
public static final RegistryObject<Item> SHOCKER_CONTROLLER =
ITEMS.register("shocker_controller", ItemShockerController::new);
public static final RegistryObject<Item> GPS_LOCATOR = ITEMS.register(
"gps_locator",
ItemGpsLocator::new
);
public static final RegistryObject<Item> COLLAR_KEY = ITEMS.register(
"collar_key",
ItemKey::new
);
// Phase 20: Lockpick for picking locks without keys
public static final RegistryObject<Item> LOCKPICK = ITEMS.register(
"lockpick",
ItemLockpick::new
);
// Taser - Kidnapper's defensive weapon (Fight Back system)
public static final RegistryObject<Item> TASER = ITEMS.register(
"taser",
ItemTaser::new
);
// TiedUp! Guide Book - Opens Patchouli documentation
public static final RegistryObject<Item> TIEDUP_GUIDE = ITEMS.register(
"tiedup_guide",
ItemTiedUpGuide::new
);
// Command Wand - Gives commands to collared NPCs (Personality System)
public static final RegistryObject<Item> COMMAND_WAND = ITEMS.register(
"command_wand",
ItemCommandWand::new
);
// Debug Wand - Testing tool for Personality System (OP item)
public static final RegistryObject<Item> DEBUG_WAND = ITEMS.register(
"debug_wand",
ItemDebugWand::new
);
// ========== CELL SYSTEM ITEMS ==========
// Admin Wand - Structure marker placement and Cell Core management
public static final RegistryObject<Item> ADMIN_WAND = ITEMS.register(
"admin_wand",
ItemAdminWand::new
);
// Cell Key - Universal key for iron bar doors
public static final RegistryObject<Item> CELL_KEY = ITEMS.register(
"cell_key",
ItemCellKey::new
);
// ========== SLAVE TRADER SYSTEM ==========
// Token - Access pass for kidnapper camps
public static final RegistryObject<Item> TOKEN = ITEMS.register(
"token",
ItemToken::new
);
// ========== SPAWN EGGS ==========
/**
* Damsel Spawn Egg
* Colors: Light Pink (0xFFB6C1) / Hot Pink (0xFF69B4)
*/
public static final RegistryObject<Item> DAMSEL_SPAWN_EGG = ITEMS.register(
"damsel_spawn_egg",
() ->
new ForgeSpawnEggItem(
ModEntities.DAMSEL,
0xFFB6C1, // Light pink (primary)
0xFF69B4, // Hot pink (secondary)
new Item.Properties()
)
);
// ========== FACTORY METHODS ==========
private static Map<BindVariant, RegistryObject<Item>> registerAllBinds() {
Map<BindVariant, RegistryObject<Item>> map = new EnumMap<>(
BindVariant.class
);
for (BindVariant variant : BindVariant.values()) {
map.put(
variant,
ITEMS.register(variant.getRegistryName(), () ->
new GenericBind(variant)
)
);
}
return map;
}
private static Map<GagVariant, RegistryObject<Item>> registerAllGags() {
Map<GagVariant, RegistryObject<Item>> map = new EnumMap<>(
GagVariant.class
);
for (GagVariant variant : GagVariant.values()) {
map.put(
variant,
ITEMS.register(variant.getRegistryName(), () ->
new GenericGag(variant)
)
);
}
return map;
}
private static Map<
BlindfoldVariant,
RegistryObject<Item>
> registerAllBlindfolds() {
Map<BlindfoldVariant, RegistryObject<Item>> map = new EnumMap<>(
BlindfoldVariant.class
);
for (BlindfoldVariant variant : BlindfoldVariant.values()) {
map.put(
variant,
ITEMS.register(variant.getRegistryName(), () ->
new GenericBlindfold(variant)
)
);
}
return map;
}
private static Map<
EarplugsVariant,
RegistryObject<Item>
> registerAllEarplugs() {
Map<EarplugsVariant, RegistryObject<Item>> map = new EnumMap<>(
EarplugsVariant.class
);
for (EarplugsVariant variant : EarplugsVariant.values()) {
map.put(
variant,
ITEMS.register(variant.getRegistryName(), () ->
new GenericEarplugs(variant)
)
);
}
return map;
}
private static Map<KnifeVariant, RegistryObject<Item>> registerAllKnives() {
Map<KnifeVariant, RegistryObject<Item>> map = new EnumMap<>(
KnifeVariant.class
);
for (KnifeVariant variant : KnifeVariant.values()) {
map.put(
variant,
ITEMS.register(variant.getRegistryName(), () ->
new GenericKnife(variant)
)
);
}
return map;
}
private static Map<
MittensVariant,
RegistryObject<Item>
> registerAllMittens() {
Map<MittensVariant, RegistryObject<Item>> map = new EnumMap<>(
MittensVariant.class
);
for (MittensVariant variant : MittensVariant.values()) {
map.put(
variant,
ITEMS.register(variant.getRegistryName(), () ->
new GenericMittens(variant)
)
);
}
return map;
}
// ========== HELPER ACCESSORS ==========
/**
* Get a bind item by variant.
* @param variant The bind variant
* @return The bind item
*/
public static Item getBind(BindVariant variant) {
return BINDS.get(variant).get();
}
/**
* Get a gag item by variant.
* @param variant The gag variant
* @return The gag item
*/
public static Item getGag(GagVariant variant) {
return GAGS.get(variant).get();
}
/**
* Get a blindfold item by variant.
* @param variant The blindfold variant
* @return The blindfold item
*/
public static Item getBlindfold(BlindfoldVariant variant) {
return BLINDFOLDS.get(variant).get();
}
/**
* Get an earplugs item by variant.
* @param variant The earplugs variant
* @return The earplugs item
*/
public static Item getEarplugs(EarplugsVariant variant) {
return EARPLUGS.get(variant).get();
}
/**
* Get a knife item by variant.
* @param variant The knife variant
* @return The knife item
*/
public static Item getKnife(KnifeVariant variant) {
return KNIVES.get(variant).get();
}
/**
* Get a mittens item by variant.
* @param variant The mittens variant
* @return The mittens item
*/
public static Item getMittens(MittensVariant variant) {
return MITTENS.get(variant).get();
}
}

View File

@@ -0,0 +1,173 @@
package com.tiedup.remake.items.base;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemStack;
/**
* Helper class for reading/writing adjustment values to ItemStack NBT.
*
* Adjustment values represent vertical offset in pixels (-4.0 to +4.0).
* These are stored in the ItemStack's NBT and automatically synced to clients
* via the equipment sync system (PacketSyncV2Equipment).
*/
public class AdjustmentHelper {
/** NBT key for Y adjustment value */
public static final String NBT_ADJUSTMENT_Y = "AdjustY";
/** NBT key for scale adjustment value */
public static final String NBT_ADJUSTMENT_SCALE = "AdjustScale";
/** Default adjustment value (no offset) */
public static final float DEFAULT_VALUE = 0.0f;
/** Minimum allowed adjustment value */
public static final float MIN_VALUE = -4.0f;
/** Maximum allowed adjustment value */
public static final float MAX_VALUE = 4.0f;
/** Minimum allowed scale value */
public static final float MIN_SCALE = 0.5f;
/** Maximum allowed scale value */
public static final float MAX_SCALE = 2.0f;
/** Default scale value (no scaling) */
public static final float DEFAULT_SCALE = 1.0f;
/** Scale adjustment step */
public static final float SCALE_STEP = 0.1f;
/**
* Get the Y adjustment value from an ItemStack.
*
* @param stack The ItemStack to read from
* @return adjustment in pixels (-4.0 to +4.0), or default if not set
*/
public static float getAdjustment(ItemStack stack) {
if (stack.isEmpty()) {
return DEFAULT_VALUE;
}
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_ADJUSTMENT_Y)) {
return tag.getFloat(NBT_ADJUSTMENT_Y);
}
// Fallback to item's default adjustment
if (stack.getItem() instanceof IAdjustable adj) {
return adj.getDefaultAdjustment();
}
return DEFAULT_VALUE;
}
/**
* Set the Y adjustment value on an ItemStack.
* Value is clamped to the valid range.
*
* @param stack The ItemStack to modify
* @param value The adjustment value in pixels
*/
public static void setAdjustment(ItemStack stack, float value) {
if (stack.isEmpty()) {
return;
}
float clamped = Mth.clamp(value, MIN_VALUE, MAX_VALUE);
stack.getOrCreateTag().putFloat(NBT_ADJUSTMENT_Y, clamped);
}
/**
* Check if an ItemStack has a custom adjustment set.
*
* @param stack The ItemStack to check
* @return true if a custom adjustment is stored in NBT
*/
public static boolean hasAdjustment(ItemStack stack) {
if (stack.isEmpty()) {
return false;
}
CompoundTag tag = stack.getTag();
return tag != null && tag.contains(NBT_ADJUSTMENT_Y);
}
/**
* Remove custom adjustment from an ItemStack, reverting to item default.
*
* @param stack The ItemStack to modify
*/
public static void clearAdjustment(ItemStack stack) {
if (stack.isEmpty()) {
return;
}
CompoundTag tag = stack.getTag();
if (tag != null) {
tag.remove(NBT_ADJUSTMENT_Y);
}
}
/**
* Convert pixel adjustment to Minecraft units for PoseStack.translate().
* 1 pixel = 1/16 block in Minecraft's coordinate system.
*
* Note: The result is negated because positive adjustment values should
* move the item UP (negative Y in model space).
*
* @param pixels Adjustment value in pixels
* @return Offset in Minecraft units for PoseStack.translate()
*/
public static double toMinecraftUnits(float pixels) {
return -pixels / 16.0;
}
/**
* Check if an ItemStack's item supports adjustment.
*
* @param stack The ItemStack to check
* @return true if the item implements IAdjustable and canBeAdjusted() returns true
*/
public static boolean isAdjustable(ItemStack stack) {
if (stack.isEmpty()) {
return false;
}
if (stack.getItem() instanceof IAdjustable adj) {
return adj.canBeAdjusted();
}
return false;
}
/**
* Get the scale adjustment value from an ItemStack.
*
* @param stack The ItemStack to read from
* @return scale factor (0.5 to 2.0), or 1.0 if not set
*/
public static float getScale(ItemStack stack) {
if (stack.isEmpty()) {
return DEFAULT_SCALE;
}
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_ADJUSTMENT_SCALE)) {
return tag.getFloat(NBT_ADJUSTMENT_SCALE);
}
return DEFAULT_SCALE;
}
/**
* Set the scale adjustment value on an ItemStack.
* Value is clamped to the valid range.
*
* @param stack The ItemStack to modify
* @param value The scale value (0.5 to 2.0)
*/
public static void setScale(ItemStack stack, float value) {
if (stack.isEmpty()) {
return;
}
float clamped = Mth.clamp(value, MIN_SCALE, MAX_SCALE);
stack.getOrCreateTag().putFloat(NBT_ADJUSTMENT_SCALE, clamped);
}
}

View File

@@ -0,0 +1,88 @@
package com.tiedup.remake.items.base;
/**
* Enum defining all bind variants with their properties.
* Used by GenericBind to create bind items via factory pattern.
*
* <p><b>Issue #12 fix:</b> Added textureSubfolder to eliminate 40+ string checks in renderers.
*/
public enum BindVariant {
// Standard binds (PoseType.STANDARD)
ROPES("ropes", PoseType.STANDARD, true, "ropes"),
ARMBINDER("armbinder", PoseType.STANDARD, false, "armbinder"),
DOGBINDER("dogbinder", PoseType.DOG, false, "armbinder"),
CHAIN("chain", PoseType.STANDARD, false, "chain"),
RIBBON("ribbon", PoseType.STANDARD, false, "ribbon"),
SLIME("slime", PoseType.STANDARD, false, "slime"),
VINE_SEED("vine_seed", PoseType.STANDARD, false, "vine"),
WEB_BIND("web_bind", PoseType.STANDARD, false, "web"),
SHIBARI("shibari", PoseType.STANDARD, true, "shibari"),
LEATHER_STRAPS("leather_straps", PoseType.STANDARD, false, "straps"),
MEDICAL_STRAPS("medical_straps", PoseType.STANDARD, false, "straps"),
BEAM_CUFFS("beam_cuffs", PoseType.STANDARD, false, "beam"),
DUCT_TAPE("duct_tape", PoseType.STANDARD, true, "tape"),
// Pose items (special PoseType)
STRAITJACKET("straitjacket", PoseType.STRAITJACKET, false, "straitjacket"),
WRAP("wrap", PoseType.WRAP, false, "wrap"),
LATEX_SACK("latex_sack", PoseType.LATEX_SACK, false, "latex");
private final String registryName;
private final PoseType poseType;
private final boolean supportsColor;
private final String textureSubfolder;
BindVariant(
String registryName,
PoseType poseType,
boolean supportsColor,
String textureSubfolder
) {
this.registryName = registryName;
this.poseType = poseType;
this.supportsColor = supportsColor;
this.textureSubfolder = textureSubfolder;
}
public String getRegistryName() {
return registryName;
}
/**
* Get the configured resistance for this bind variant.
* Delegates to {@link com.tiedup.remake.core.SettingsAccessor#getBindResistance(String)}.
*/
public int getResistance() {
return com.tiedup.remake.core.SettingsAccessor.getBindResistance(registryName);
}
public PoseType getPoseType() {
return poseType;
}
/**
* Check if this bind variant supports color variations.
* Items with colors: ropes, shibari, duct_tape
*/
public boolean supportsColor() {
return supportsColor;
}
/**
* Get the texture subfolder for this bind variant.
* Used by renderers to locate texture files.
*
* @return Subfolder path under textures/entity/bondage/ (e.g., "ropes", "straps")
*/
public String getTextureSubfolder() {
return textureSubfolder;
}
/**
* Get the item name used for textures and translations.
* For most variants this is the same as registryName.
*/
public String getItemName() {
return registryName;
}
}

View File

@@ -0,0 +1,48 @@
package com.tiedup.remake.items.base;
/**
* Enum defining all blindfold variants.
* Used by GenericBlindfold to create blindfold items via factory pattern.
*
* <p><b>Issue #12 fix:</b> Added textureSubfolder to eliminate string checks in renderers.
*/
public enum BlindfoldVariant {
CLASSIC("classic_blindfold", true, "blindfolds"),
MASK("blindfold_mask", true, "blindfolds/mask");
private final String registryName;
private final boolean supportsColor;
private final String textureSubfolder;
BlindfoldVariant(
String registryName,
boolean supportsColor,
String textureSubfolder
) {
this.registryName = registryName;
this.supportsColor = supportsColor;
this.textureSubfolder = textureSubfolder;
}
public String getRegistryName() {
return registryName;
}
/**
* Check if this blindfold variant supports color variations.
* Both variants support colors in the original mod.
*/
public boolean supportsColor() {
return supportsColor;
}
/**
* Get the texture subfolder for this blindfold variant.
* Used by renderers to locate texture files.
*
* @return Subfolder path under textures/entity/bondage/ (e.g., "blindfolds", "blindfolds/mask")
*/
public String getTextureSubfolder() {
return textureSubfolder;
}
}

View File

@@ -0,0 +1,33 @@
package com.tiedup.remake.items.base;
/**
* Enum defining all earplugs variants.
* Used by GenericEarplugs to create earplugs items via factory pattern.
*
* <p><b>Issue #12 fix:</b> Added textureSubfolder to eliminate string checks in renderers.
*/
public enum EarplugsVariant {
CLASSIC("classic_earplugs", "earplugs");
private final String registryName;
private final String textureSubfolder;
EarplugsVariant(String registryName, String textureSubfolder) {
this.registryName = registryName;
this.textureSubfolder = textureSubfolder;
}
public String getRegistryName() {
return registryName;
}
/**
* Get the texture subfolder for this earplugs variant.
* Used by renderers to locate texture files.
*
* @return Subfolder path under textures/entity/bondage/ (e.g., "earplugs")
*/
public String getTextureSubfolder() {
return textureSubfolder;
}
}

View File

@@ -0,0 +1,163 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.util.GagMaterial;
/**
* Enum defining all gag variants with their properties.
* Used by GenericGag to create gag items via factory pattern.
*
* <p>Note: ItemMedicalGag is NOT included here because it implements
* IHasBlindingEffect (combo item with special behavior).
*
* <p><b>Issue #12 fix:</b> Added textureSubfolder to eliminate 40+ string checks in renderers.
*/
public enum GagVariant {
// Cloth-based gags
CLOTH_GAG("cloth_gag", GagMaterial.CLOTH, true, "cloth", false, null),
ROPES_GAG("ropes_gag", GagMaterial.CLOTH, true, "shibari", false, null),
CLEAVE_GAG("cleave_gag", GagMaterial.CLOTH, true, "cleave", false, null),
RIBBON_GAG("ribbon_gag", GagMaterial.CLOTH, false, "ribbon", false, null),
// Ball gags - standard 2D texture rendering
BALL_GAG(
"ball_gag",
GagMaterial.BALL,
true,
"ballgags/normal",
false,
null
),
BALL_GAG_STRAP(
"ball_gag_strap",
GagMaterial.BALL,
true,
"ballgags/harness",
false,
null
),
// Tape gags
TAPE_GAG("tape_gag", GagMaterial.TAPE, true, "tape", false, null),
// Stuffed/filling gags (no colors)
WRAP_GAG("wrap_gag", GagMaterial.STUFFED, false, "wrap", false, null),
SLIME_GAG("slime_gag", GagMaterial.STUFFED, false, "slime", false, null),
VINE_GAG("vine_gag", GagMaterial.STUFFED, false, "vine", false, null),
WEB_GAG("web_gag", GagMaterial.STUFFED, false, "web", false, null),
// Panel gags (no colors)
PANEL_GAG(
"panel_gag",
GagMaterial.PANEL,
false,
"straitjacket",
false,
null
),
BEAM_PANEL_GAG(
"beam_panel_gag",
GagMaterial.PANEL,
false,
"beam",
false,
null
),
CHAIN_PANEL_GAG(
"chain_panel_gag",
GagMaterial.PANEL,
false,
"chain",
false,
null
),
// Latex gags (no colors)
LATEX_GAG("latex_gag", GagMaterial.LATEX, false, "latex", false, null),
// Ring/tube gags (no colors)
TUBE_GAG("tube_gag", GagMaterial.RING, false, "tube", false, null),
// Bite gags (no colors)
BITE_GAG("bite_gag", GagMaterial.BITE, false, "armbinder", false, null),
// Sponge gags (no colors)
SPONGE_GAG("sponge_gag", GagMaterial.SPONGE, false, "sponge", false, null),
// Baguette gags (no colors)
BAGUETTE_GAG(
"baguette_gag",
GagMaterial.BAGUETTE,
false,
"baguette",
false,
null
);
private final String registryName;
private final GagMaterial material;
private final boolean supportsColor;
private final String textureSubfolder;
private final boolean uses3DModel;
private final String modelPath;
GagVariant(
String registryName,
GagMaterial material,
boolean supportsColor,
String textureSubfolder,
boolean uses3DModel,
String modelPath
) {
this.registryName = registryName;
this.material = material;
this.supportsColor = supportsColor;
this.textureSubfolder = textureSubfolder;
this.uses3DModel = uses3DModel;
this.modelPath = modelPath;
}
public String getRegistryName() {
return registryName;
}
public GagMaterial getMaterial() {
return material;
}
/**
* Check if this gag variant supports color variations.
* Items with colors: cloth_gag, ropes_gag, cleave_gag, ribbon_gag,
* ball_gag, ball_gag_strap, tape_gag
*/
public boolean supportsColor() {
return supportsColor;
}
/**
* Get the texture subfolder for this gag variant.
* Used by renderers to locate texture files.
*
* @return Subfolder path under textures/entity/bondage/ (e.g., "cloth", "ballgags/normal")
*/
public String getTextureSubfolder() {
return textureSubfolder;
}
/**
* Check if this gag variant uses a 3D OBJ model.
*
* @return true if this variant uses a 3D model
*/
public boolean uses3DModel() {
return uses3DModel;
}
/**
* Get the model path for 3D rendering.
*
* @return ResourceLocation string path (e.g., "tiedup:models/obj/ball_gag.obj"), or null if no 3D model
*/
public String getModelPath() {
return modelPath;
}
}

View File

@@ -0,0 +1,49 @@
package com.tiedup.remake.items.base;
/**
* Interface for items that can have their render position adjusted.
* Typically gags and blindfolds that render on the player's head.
*
* Players can adjust the Y position of these items to better fit their skin.
* Adjustment values are stored in the ItemStack's NBT via AdjustmentHelper.
*/
public interface IAdjustable {
/**
* Whether this item supports position adjustment.
* @return true if adjustable
*/
boolean canBeAdjusted();
/**
* Default Y offset for this item type (in pixels, 1 pixel = 1/16 block).
* Override for items that need a non-zero default position.
* @return default adjustment value
*/
default float getDefaultAdjustment() {
return 0.0f;
}
/**
* Minimum allowed adjustment value (pixels).
* @return minimum value (typically -4.0)
*/
default float getMinAdjustment() {
return -4.0f;
}
/**
* Maximum allowed adjustment value (pixels).
* @return maximum value (typically +4.0)
*/
default float getMaxAdjustment() {
return 4.0f;
}
/**
* Step size for GUI slider (smaller = more precise).
* @return step size (typically 0.25)
*/
default float getAdjustmentStep() {
return 0.25f;
}
}

View File

@@ -0,0 +1,102 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Interface for all bondage equipment items.
* Defines the core behavior for items that can be equipped in custom bondage slots.
*
* Based on original IExtraBondageItem from 1.12.2
*/
public interface IBondageItem {
/**
* Get the body region this item occupies when equipped.
* @return The body region
*/
BodyRegionV2 getBodyRegion();
/**
* Called every tick while this item is equipped on an entity.
* @param stack The equipped item stack
* @param entity The entity wearing the item
*/
default void onWornTick(ItemStack stack, LivingEntity entity) {
// Default: do nothing
}
/**
* Called when this item is equipped on an entity.
* @param stack The equipped item stack
* @param entity The entity wearing the item
*/
default void onEquipped(ItemStack stack, LivingEntity entity) {
// Default: do nothing
}
/**
* Called when this item is unequipped from an entity.
* @param stack The unequipped item stack
* @param entity The entity that was wearing the item
*/
default void onUnequipped(ItemStack stack, LivingEntity entity) {
// Default: do nothing
}
/**
* Check if this item can be equipped on the given entity.
* @param stack The item stack to equip
* @param entity The target entity
* @return true if the item can be equipped, false otherwise
*/
default boolean canEquip(ItemStack stack, LivingEntity entity) {
return true;
}
/**
* Check if this item can be unequipped from the given entity.
* @param stack The equipped item stack
* @param entity The entity wearing the item
* @return true if the item can be unequipped, false otherwise
*/
default boolean canUnequip(ItemStack stack, LivingEntity entity) {
return true;
}
/**
* Get the texture subfolder for this bondage item.
* Used by renderers to locate texture files.
*
* <p><b>Issue #12 fix:</b> Eliminates 40+ string checks in renderers by letting
* each item type declare its own texture subfolder.
*
* @return Subfolder path under textures/entity/bondage/ (e.g., "ropes", "ballgags/normal")
*/
default String getTextureSubfolder() {
return "misc"; // Fallback for items without explicit subfolder
}
/**
* Check if this bondage item uses a 3D OBJ model instead of a flat texture.
* Items with 3D models will be rendered using ObjModelRenderer.
*
* @return true if this item uses a 3D model, false for standard texture rendering
*/
default boolean uses3DModel() {
return false;
}
/**
* Get the ResourceLocation of the 3D model for this item.
* Only called if uses3DModel() returns true.
*
* @return ResourceLocation pointing to the .obj file, or null if no 3D model
*/
@Nullable
default ResourceLocation get3DModelLocation() {
return null;
}
}

View File

@@ -0,0 +1,33 @@
package com.tiedup.remake.items.base;
/**
* Marker interface for items that have a blinding visual effect.
*
* <p>Items implementing this interface will:
* <ul>
* <li>Apply a screen overlay when worn (client-side)</li>
* <li>Reduce the player's visibility</li>
* <li>Potentially disable certain UI elements</li>
* </ul>
*
* <h2>Usage</h2>
* <pre>{@code
* if (blindfold.getItem() instanceof IHasBlindingEffect) {
* // Apply blinding overlay
* renderBlindingOverlay();
* }
* }</pre>
*
* <h2>Implementations</h2>
* <ul>
* <li>{@link ItemBlindfold} - All blindfold items</li>
* </ul>
*
* <p>Based on original IHasBlindingEffect.java from 1.12.2
*
* @see ItemBlindfold
*/
public interface IHasBlindingEffect {
// Marker interface - no methods required
// Presence of this interface indicates the item has a blinding effect
}

View File

@@ -0,0 +1,33 @@
package com.tiedup.remake.items.base;
/**
* Marker interface for items that have a gagging (speech muffling) effect.
*
* <p>Items implementing this interface will:
* <ul>
* <li>Convert chat messages to "mmpphh" sounds</li>
* <li>Play gagged speech sounds</li>
* <li>Potentially block certain chat commands</li>
* </ul>
*
* <h2>Usage</h2>
* <pre>{@code
* if (gag.getItem() instanceof IHasGaggingEffect) {
* // Convert chat message to gagged speech
* message = GagTalkConverter.convert(message);
* }
* }</pre>
*
* <h2>Implementations</h2>
* <ul>
* <li>{@link ItemGag} - Ball gags, tape gags, cloth gags, etc.</li>
* </ul>
*
* <p>Based on original ItemGaggingEffect.java from 1.12.2
*
* @see ItemGag
*/
public interface IHasGaggingEffect {
// Marker interface - no methods required
// Presence of this interface indicates the item has a gagging effect
}

View File

@@ -0,0 +1,235 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.core.SettingsAccessor;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
/**
* Interface for bondage items that have a resistance value.
*
* <p>The resistance system allows players to "struggle" out of restraints.
* Higher resistance = more struggle attempts needed to escape.
*
* <h2>How Resistance Works</h2>
* <ol>
* <li>Item has a base resistance from config (via SettingsAccessor)</li>
* <li>When equipped, current resistance = base resistance</li>
* <li>Each struggle attempt decreases current resistance</li>
* <li>When current resistance reaches 0, player escapes</li>
* <li>Resistance resets when item is unequipped</li>
* </ol>
*
* <h2>Implementations</h2>
* <ul>
* <li>{@link ItemBind} - Ropes, chains, straitjackets</li>
* <li>{@link ItemGag} - Ball gags, tape gags, cloth gags</li>
* <li>{@link ItemBlindfold} - Blindfolds</li>
* <li>{@link ItemCollar} - Collars (special: may not be struggleable)</li>
* </ul>
*
* <p>Based on original IHasResistance.java from 1.12.2
*
* @see SettingsAccessor
*/
public interface IHasResistance {
// ========================================
// NBT KEYS
// ========================================
/** NBT key for storing current resistance value (camelCase standard) */
String NBT_CURRENT_RESISTANCE = "currentResistance";
/** Legacy NBT key for migration from older versions */
String NBT_CURRENT_RESISTANCE_LEGACY = "currentresistance";
/** NBT key for storing whether item can be struggled out of */
String NBT_CAN_STRUGGLE = "canBeStruggledOut";
// ========================================
// ABSTRACT METHODS (must implement)
// ========================================
/**
* Get the item name/ID for resistance config lookup.
*
* <p>This is used to look up the base resistance from ModConfig
* via {@link SettingsAccessor#getBindResistance(String)}.
*
* @return Item identifier for resistance lookup
*/
String getResistanceId();
/**
* Called when the entity struggles against this item.
*
* <p>Implementations should:
* <ul>
* <li>Play struggle sound</li>
* <li>Show message to player</li>
* <li>Potentially notify nearby players</li>
* </ul>
*
* @param entity The entity struggling
*/
void notifyStruggle(LivingEntity entity);
// ========================================
// DEFAULT METHODS (NBT handling)
// ========================================
/**
* Get the base resistance from config via SettingsAccessor.
*
* @param entity The entity (kept for API compatibility)
* @return Base resistance value
*/
default int getBaseResistance(LivingEntity entity) {
return SettingsAccessor.getBindResistance(getResistanceId());
}
/**
* Get the current resistance from ItemStack NBT.
*
* <p>If no current resistance is stored (or <= 0), returns base resistance.
* <p>Handles migration from legacy lowercase key to camelCase.
*
* @param stack The item stack
* @param entity The entity (for accessing base resistance)
* @return Current resistance value
*/
default int getCurrentResistance(ItemStack stack, LivingEntity entity) {
if (stack.isEmpty()) {
return 0;
}
CompoundTag tag = stack.getTag();
if (tag != null) {
// Check new camelCase key first
if (tag.contains(NBT_CURRENT_RESISTANCE)) {
int resistance = tag.getInt(NBT_CURRENT_RESISTANCE);
if (resistance > 0) {
return resistance;
}
}
// Migration: check legacy lowercase key
else if (tag.contains(NBT_CURRENT_RESISTANCE_LEGACY)) {
int resistance = tag.getInt(NBT_CURRENT_RESISTANCE_LEGACY);
// Migrate to new key
tag.remove(NBT_CURRENT_RESISTANCE_LEGACY);
if (resistance > 0) {
tag.putInt(NBT_CURRENT_RESISTANCE, resistance);
return resistance;
}
}
}
// Default to base resistance
return getBaseResistance(entity);
}
/**
* Set the current resistance in ItemStack NBT.
*
* @param stack The item stack
* @param resistance The resistance value to set
* @return The modified item stack
*/
default ItemStack setCurrentResistance(ItemStack stack, int resistance) {
if (!stack.isEmpty()) {
stack.getOrCreateTag().putInt(NBT_CURRENT_RESISTANCE, resistance);
}
return stack;
}
/**
* Reset the current resistance (remove from NBT).
*
* <p>Called when the item is unequipped. Next time it's equipped,
* it will start fresh with base resistance.
*
* @param stack The item stack
* @return The modified item stack
*/
default ItemStack resetCurrentResistance(ItemStack stack) {
if (!stack.isEmpty()) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_CURRENT_RESISTANCE)) {
tag.remove(NBT_CURRENT_RESISTANCE);
// Clean up empty tag
if (tag.isEmpty()) {
stack.setTag(null);
}
}
}
return stack;
}
/**
* Decrease the current resistance by one struggle attempt.
*
* @param stack The item stack
* @param entity The entity struggling
* @return The new resistance value after decreasing
*/
default int decreaseResistance(ItemStack stack, LivingEntity entity) {
int current = getCurrentResistance(stack, entity);
int newResistance = Math.max(0, current - 1);
setCurrentResistance(stack, newResistance);
return newResistance;
}
/**
* Check if this item can be struggled out of.
*
* <p>Some items (like locked collars) cannot be escaped via struggling.
* Default is true (can be struggled).
*
* @param stack The item stack
* @return True if struggling is allowed
*/
default boolean canBeStruggledOut(ItemStack stack) {
if (stack.isEmpty()) {
return true;
}
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_CAN_STRUGGLE)) {
return tag.getBoolean(NBT_CAN_STRUGGLE);
}
return true; // Default: can be struggled
}
/**
* Set whether this item can be struggled out of.
*
* @param stack The item stack
* @param canStruggle True to allow struggling
* @return The modified item stack
*/
default ItemStack setCanBeStruggledOut(
ItemStack stack,
boolean canStruggle
) {
if (!stack.isEmpty()) {
stack.getOrCreateTag().putBoolean(NBT_CAN_STRUGGLE, canStruggle);
}
return stack;
}
/**
* Check if the entity can escape from this item.
*
* <p>Combines struggle permission with current resistance check.
*
* @param stack The item stack
* @param entity The entity trying to escape
* @return True if escape is possible (resistance = 0 and struggling allowed)
*/
default boolean canEscape(ItemStack stack, LivingEntity entity) {
return (
canBeStruggledOut(stack) && getCurrentResistance(stack, entity) <= 0
);
}
}

View File

@@ -0,0 +1,15 @@
package com.tiedup.remake.items.base;
/**
* Marker interface for knife items.
*
* v2.5: Knives now work by active cutting (hold right-click).
* - Consumes 5 durability/second
* - Removes 5 resistance/second from bind or locked accessory
*
* See GenericKnife for the active cutting implementation.
*/
public interface IKnife {
// Marker interface - no methods required
// Implementation provides: use(), onUseTick() for active cutting
}

View File

@@ -0,0 +1,350 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.util.ItemNBTHelper;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Interface for bondage items that can be locked with padlocks.
*
* <p>Items implementing this interface can be locked to prevent removal
* without a key or force action.
*
* <h2>Lock Safety Pattern</h2>
* When an item is locked:
* <ul>
* <li>Cannot be removed by normal means</li>
* <li>Cannot be replaced unless forced</li>
* <li>Must be unlocked with a key or master key</li>
* </ul>
*
* <h2>Implementations</h2>
* <ul>
* <li>{@link ItemGag} - Gags can be locked</li>
* <li>{@link ItemBlindfold} - Blindfolds can be locked</li>
* <li>{@link ItemCollar} - Collars can be locked</li>
* <li>{@link ItemEarplugs} - Earplugs can be locked</li>
* <li>ItemBind - Binds can be locked (future)</li>
* </ul>
*
* <h2>NBT Storage</h2>
* Lock state is stored in NBT:
* <pre>{@code
* {
* "locked": true,
* "lockable": true,
* "lockedByKeyUUID": "uuid-string" // UUID of the key that locked this item
* }
* }</pre>
*
* <h2>Key-Lock System</h2>
* Each lock is tied to a specific key via UUID:
* <ul>
* <li>When locked with a key, the key's UUID is stored</li>
* <li>Only the matching key (or master key) can unlock</li>
* <li>Master key bypasses UUID check</li>
* </ul>
*
* @see ItemPadlock
* @see ItemKey
*/
public interface ILockable {
// ========== NBT CONSTANTS ==========
/** NBT key for locked state */
String NBT_LOCKED = "locked";
/** NBT key for lockable state (can accept padlock) */
String NBT_LOCKABLE = "lockable";
/** NBT key for the UUID of the key that locked this item */
String NBT_LOCKED_BY_KEY_UUID = "lockedByKeyUUID";
// ========== LOCK STATE METHODS ==========
/**
* Set the locked state of this item.
*
* <p><b>CRITICAL:</b> When unlocking (state = false), some items may reset
* their resistance to base value to prevent exploits.</p>
*
* @param stack The ItemStack to modify
* @param state true to lock, false to unlock
* @return The modified ItemStack (for chaining)
*/
default ItemStack setLocked(ItemStack stack, boolean state) {
ItemNBTHelper.setBoolean(stack, NBT_LOCKED, state);
return stack;
}
/**
* Check if this item is currently locked.
*
* @param stack The ItemStack to check
* @return true if locked
*/
default boolean isLocked(ItemStack stack) {
return ItemNBTHelper.getBoolean(stack, NBT_LOCKED);
}
/**
* Set whether this item can be locked (lockable state).
*
* <p>This is different from locked state:
* <ul>
* <li>lockable = true: Item can accept a padlock</li>
* <li>locked = true: Item currently has a padlock on it</li>
* </ul>
*
* @param stack The ItemStack to modify
* @param state true to make lockable, false to prevent locking
* @return The modified ItemStack (for chaining)
*/
default ItemStack setLockable(ItemStack stack, boolean state) {
ItemNBTHelper.setBoolean(stack, NBT_LOCKABLE, state);
return stack;
}
/**
* Check if this item can be locked.
*
* @param stack The ItemStack to check
* @return true if lockable (can accept a padlock)
*/
default boolean isLockable(ItemStack stack) {
return ItemNBTHelper.getBoolean(stack, NBT_LOCKABLE);
}
// ========== TOOLTIP HELPER ==========
/**
* Append lock status tooltip to item hover text.
* Provides consistent lock/lockable display across all lockable items.
*
* @param stack The ItemStack being displayed
* @param tooltip The tooltip list to append to
*/
default void appendLockTooltip(ItemStack stack, List<Component> tooltip) {
if (isLockable(stack)) {
if (isLocked(stack)) {
tooltip.add(
Component.translatable(
"item.tiedup.tooltip.locked"
).withStyle(ChatFormatting.RED)
);
} else {
tooltip.add(
Component.translatable(
"item.tiedup.tooltip.lockable"
).withStyle(ChatFormatting.GOLD)
);
}
}
}
/**
* Check if the padlock should be dropped when unlocking.
*
* <p>Default implementation returns true (drop padlock on unlock).</p>
* <p>Some items may override this to consume the padlock permanently.</p>
*
* @return true if padlock should be dropped
*/
default boolean dropLockOnUnlock() {
return true;
}
/**
* Check if this item can have a padlock attached via anvil.
*
* <p>Some items cannot have padlocks attached due to their nature:</p>
* <ul>
* <li>Adhesive items (tape) - stick to themselves, no attachment point</li>
* <li>Organic items (slime, vine, web) - living/organic matter</li>
* </ul>
*
* <p>Default implementation returns true (can attach padlock).</p>
*
* @return true if padlock can be attached
*/
default boolean canAttachPadlock() {
return true;
}
// ========== KEY-LOCK SYSTEM ==========
/**
* Get the UUID of the key that locked this item.
*
* @param stack The ItemStack to check
* @return The UUID of the locking key, or null if not locked or locked without key
*/
@Nullable
default UUID getLockedByKeyUUID(ItemStack stack) {
return ItemNBTHelper.getUUID(stack, NBT_LOCKED_BY_KEY_UUID);
}
/**
* Set the key UUID that locks this item.
*
* <p>Setting a non-null keyUUID will also set locked=true.
* Setting null will unlock the item (locked=false).</p>
*
* @param stack The ItemStack to modify
* @param keyUUID The UUID of the key, or null to unlock
*/
default void setLockedByKeyUUID(ItemStack stack, @Nullable UUID keyUUID) {
if (stack.isEmpty()) return;
ItemNBTHelper.setUUID(stack, NBT_LOCKED_BY_KEY_UUID, keyUUID);
ItemNBTHelper.setBoolean(stack, NBT_LOCKED, keyUUID != null);
}
/**
* Check if a key matches this item's lock.
*
* <p>Default implementation compares the stored keyUUID with the provided one.</p>
*
* @param stack The ItemStack to check
* @param keyUUID The key UUID to test
* @return true if the key matches this lock
*/
default boolean matchesKey(ItemStack stack, UUID keyUUID) {
if (keyUUID == null) return false;
UUID lockedBy = getLockedByKeyUUID(stack);
return lockedBy != null && lockedBy.equals(keyUUID);
}
// ========== STRUGGLE/LOCKPICK SYSTEM ==========
/**
* NBT key for jammed state.
*/
String NBT_JAMMED = "jammed";
/**
* Get the resistance added by the lock for struggle mechanics.
*
* <p>When locked, this value is added to the item's base resistance.
* Configurable via server config and GameRule.</p>
*
* @return Lock resistance value (default: 250, configurable)
*/
default int getLockResistance() {
return com.tiedup.remake.core.SettingsAccessor.getPadlockResistance(null);
}
/**
* Check if the lock is jammed (lockpick failed critically).
*
* <p>When jammed, only struggle can unlock the item, lockpick is blocked.
* Jam state is set when lockpick has a 2.5% critical failure.</p>
*
* @param stack The ItemStack to check
* @return true if the lock is jammed
*/
default boolean isJammed(ItemStack stack) {
return ItemNBTHelper.getBoolean(stack, NBT_JAMMED);
}
/**
* Set the jammed state of this item's lock.
*
* <p>When jammed, lockpick cannot be used on this item.
* Only struggle can unlock a jammed lock.</p>
*
* @param stack The ItemStack to modify
* @param jammed true to jam the lock, false to clear jam
*/
default void setJammed(ItemStack stack, boolean jammed) {
if (jammed) {
ItemNBTHelper.setBoolean(stack, NBT_JAMMED, true);
} else {
ItemNBTHelper.remove(stack, NBT_JAMMED);
}
}
// ========== LOCK RESISTANCE (for struggle) ==========
/**
* NBT key for current lock resistance during struggle.
*/
String NBT_LOCK_RESISTANCE = "lockResistance";
/**
* Get the current lock resistance remaining for struggle.
* Initialized to getLockResistance() (configurable, default 250) when first locked.
*
* @param stack The ItemStack to check
* @return Current lock resistance (0 if not locked or fully struggled)
*/
default int getCurrentLockResistance(ItemStack stack) {
if (stack.isEmpty()) return 0;
// If locked but no resistance stored yet, initialize it
if (
isLocked(stack) &&
!ItemNBTHelper.contains(stack, NBT_LOCK_RESISTANCE)
) {
return getLockResistance(); // Configurable via ModConfig
}
return ItemNBTHelper.getInt(stack, NBT_LOCK_RESISTANCE);
}
/**
* Set the current lock resistance remaining for struggle.
*
* @param stack The ItemStack to modify
* @param resistance The new resistance value
*/
default void setCurrentLockResistance(ItemStack stack, int resistance) {
ItemNBTHelper.setInt(stack, NBT_LOCK_RESISTANCE, resistance);
}
/**
* Initialize lock resistance when item is locked.
* Called when setLockedByKeyUUID is called with a non-null UUID.
*
* @param stack The ItemStack to initialize
*/
default void initializeLockResistance(ItemStack stack) {
setCurrentLockResistance(stack, getLockResistance());
}
/**
* Clear lock resistance when item is unlocked.
*
* @param stack The ItemStack to clear
*/
default void clearLockResistance(ItemStack stack) {
ItemNBTHelper.remove(stack, NBT_LOCK_RESISTANCE);
}
// ========== LOCK BREAKING (struggle/force) ==========
/**
* Completely break/destroy the lock on an item.
*
* <p>Used when a padlock is destroyed through struggle or force.
* This removes all lock-related state from the item:
* <ul>
* <li>Unlocks the item (lockedByKeyUUID = null)</li>
* <li>Removes the lockable flag (no more padlock slot)</li>
* <li>Clears any jam state</li>
* <li>Clears stored lock resistance</li>
* </ul>
*
* @param stack The ItemStack to break the lock on
*/
default void breakLock(ItemStack stack) {
if (stack.isEmpty()) return;
setLockedByKeyUUID(stack, null);
setLockable(stack, false);
setJammed(stack, false);
clearLockResistance(stack);
}
}

View File

@@ -0,0 +1,623 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.action.PacketTying;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.tasks.TyingPlayerTask;
import com.tiedup.remake.tasks.TyingTask;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.util.TiedUpSounds;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Base class for binding/restraint items (ropes, chains, straitjacket, etc.)
* These items restrain a player's movement and actions when equipped.
*
* <p>Implements {@link IHasResistance} for the struggle/escape system.
* <p>Implements {@link ILockable} for the padlock system (Phase 15).
*
* Based on original ItemBind from 1.12.2
*
* Phase 5: Movement speed reduction implemented
* Phase 7: Resistance system implemented via IHasResistance
* Phase 15: Added ILockable interface for padlock support
*/
public abstract class ItemBind
extends Item
implements IBondageItem, IHasResistance, ILockable
{
// ========== Leg Binding: Bind Mode NBT Key ==========
private static final String NBT_BIND_MODE = "bindMode";
public ItemBind(Properties properties) {
super(properties);
}
@Override
public BodyRegionV2 getBodyRegion() {
return BodyRegionV2.ARMS;
}
// ========== Leg Binding: Bind Mode Methods ==========
// String constants matching NBT values
public static final String BIND_MODE_FULL = "full";
private static final String MODE_FULL = BIND_MODE_FULL;
private static final String MODE_ARMS = "arms";
private static final String MODE_LEGS = "legs";
private static final String[] MODE_CYCLE = {MODE_FULL, MODE_ARMS, MODE_LEGS};
private static final java.util.Map<String, String> MODE_TRANSLATION_KEYS = java.util.Map.of(
MODE_FULL, "tiedup.bindmode.full",
MODE_ARMS, "tiedup.bindmode.arms",
MODE_LEGS, "tiedup.bindmode.legs"
);
/**
* Get the bind mode ID string from the stack's NBT.
* @param stack The bind ItemStack
* @return "full", "arms", or "legs" (defaults to "full" if absent)
*/
public static String getBindModeId(ItemStack stack) {
if (stack.isEmpty()) return MODE_FULL;
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(NBT_BIND_MODE)) return MODE_FULL;
String value = tag.getString(NBT_BIND_MODE);
if (MODE_FULL.equals(value) || MODE_ARMS.equals(value) || MODE_LEGS.equals(value)) {
return value;
}
return MODE_FULL;
}
/**
* Check if arms are bound (mode is "arms" or "full").
* @param stack The bind ItemStack
* @return true if arms are restrained
*/
public static boolean hasArmsBound(ItemStack stack) {
String mode = getBindModeId(stack);
return MODE_ARMS.equals(mode) || MODE_FULL.equals(mode);
}
/**
* Check if legs are bound (mode is "legs" or "full").
* @param stack The bind ItemStack
* @return true if legs are restrained
*/
public static boolean hasLegsBound(ItemStack stack) {
String mode = getBindModeId(stack);
return MODE_LEGS.equals(mode) || MODE_FULL.equals(mode);
}
/**
* Cycle bind mode: full -> arms -> legs -> full.
* @param stack The bind ItemStack
* @return the new mode ID string
*/
public static String cycleBindModeId(ItemStack stack) {
String current = getBindModeId(stack);
String next = MODE_FULL;
for (int i = 0; i < MODE_CYCLE.length; i++) {
if (MODE_CYCLE[i].equals(current)) {
next = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
break;
}
}
stack.getOrCreateTag().putString(NBT_BIND_MODE, next);
return next;
}
/**
* Get the translation key for the current bind mode.
* @param stack The bind ItemStack
* @return the i18n key for the mode
*/
public static String getBindModeTranslationKey(ItemStack stack) {
return MODE_TRANSLATION_KEYS.getOrDefault(getBindModeId(stack), "tiedup.bindmode.full");
}
/**
* Called when player right-clicks in air with bind item.
* Sneak+click cycles the bind mode.
*/
@Override
public InteractionResultHolder<ItemStack> use(
Level level,
Player player,
InteractionHand hand
) {
ItemStack stack = player.getItemInHand(hand);
// Sneak+click in air cycles bind mode
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
String newModeId = cycleBindModeId(stack);
// Play feedback sound
player.playSound(SoundEvents.CHAIN_STEP, 0.5f, 1.2f);
// Show action bar message
player.displayClientMessage(
Component.translatable(
"tiedup.message.bindmode_changed",
Component.translatable(getBindModeTranslationKey(stack))
),
true
);
TiedUpMod.LOGGER.debug(
"[ItemBind] {} cycled bind mode to {}",
player.getName().getString(),
newModeId
);
}
return InteractionResultHolder.sidedSuccess(
stack,
level.isClientSide
);
}
return super.use(level, player, hand);
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
// Show bind mode
tooltip.add(
Component.translatable(
"item.tiedup.tooltip.bindmode",
Component.translatable(getBindModeTranslationKey(stack))
).withStyle(ChatFormatting.GRAY)
);
// Show lock status
if (isLockable(stack)) {
if (isLocked(stack)) {
tooltip.add(
Component.translatable(
"item.tiedup.tooltip.locked"
).withStyle(ChatFormatting.RED)
);
} else {
tooltip.add(
Component.translatable(
"item.tiedup.tooltip.lockable"
).withStyle(ChatFormatting.GOLD)
);
}
}
}
/**
* Called when the bind is equipped on an entity.
* Applies movement speed reduction only if legs are bound.
*
* Phase 14.1.5: Refactored to support IBondageState (LivingEntity + NPCs)
* Leg Binding: Speed reduction conditional on mode
* Based on original ItemBind.onEquipped() (1.12.2)
*/
@Override
public void onEquipped(ItemStack stack, LivingEntity entity) {
String modeId = getBindModeId(stack);
// Only apply speed reduction if legs are bound
if (hasLegsBound(stack)) {
// H6 fix: For players, speed is handled exclusively by MovementStyleManager
// (V2 tick-based system) via MovementStyleResolver V1 fallback.
// Applying V1 RestraintEffectUtils here would cause double stacking (different
// UUIDs, ADDITION vs MULTIPLY_BASE) leading to quasi-immobility.
if (entity instanceof Player) {
TiedUpMod.LOGGER.debug(
"[ItemBind] Applied bind (mode={}, pose={}) to player {} - speed delegated to MovementStyleManager",
modeId,
getPoseType().getAnimationId(),
entity.getName().getString()
);
} else {
// NPCs: MovementStyleManager only handles ServerPlayer, so NPCs
// still need the legacy RestraintEffectUtils speed modifier.
PoseType poseType = getPoseType();
boolean fullImmobilization =
poseType == PoseType.WRAP || poseType == PoseType.LATEX_SACK;
RestraintEffectUtils.applyBindSpeedReduction(entity, fullImmobilization);
TiedUpMod.LOGGER.debug(
"[ItemBind] Applied bind (mode={}, pose={}) to NPC {} - speed reduced (full={})",
modeId,
poseType.getAnimationId(),
entity.getName().getString(),
fullImmobilization
);
}
} else {
TiedUpMod.LOGGER.debug(
"[ItemBind] Applied bind (mode={}) to {} - no speed reduction",
modeId,
entity.getName().getString()
);
}
}
/**
* Called when the bind is unequipped from an entity.
* Restores normal movement speed for all entities.
* Phase 7: Resets resistance for next use.
*
* Phase 14.1.5: Refactored to support IBondageState (LivingEntity + NPCs)
* Based on original ItemBind.onUnequipped() (1.12.2)
*/
@Override
public void onUnequipped(ItemStack stack, LivingEntity entity) {
// H6 fix: For players, speed cleanup is handled by MovementStyleManager
// (V2 tick-based system). On the next tick, the resolver will see the item
// is gone, deactivate the style, and remove the modifier automatically.
// NPCs still need the legacy RestraintEffectUtils cleanup.
if (!(entity instanceof Player)) {
RestraintEffectUtils.removeBindSpeedReduction(entity);
}
// Phase 7: Reset resistance for next use (uses IHasResistance default method)
IHasResistance.super.resetCurrentResistance(stack);
TiedUpMod.LOGGER.debug(
"[ItemBind] Removed bind from {} - speed {} resistance reset",
entity.getName().getString(),
entity instanceof Player ? "delegated to MovementStyleManager," : "restored,"
);
}
// ========== Phase 6: Tying Interaction ==========
/**
* Called when player right-clicks another entity with this bind item.
* Starts or continues a tying task to tie up the target entity.
*
* Phase 14.2: Unified to support IBondageState (Player + NPCs)
* - Players: Uses tying task with progress bar
* - NPCs: Instant bind (no tying mini-game)
*
* Based on original ItemBind.itemInteractionForEntity() (1.12.2)
*
* @param stack The item stack
* @param player The player using the item (kidnapper)
* @param target The entity being interacted with
* @param hand The hand holding the item
* @return SUCCESS if tying started/continued, PASS otherwise
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Only run on server side
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Phase 14.2: Use KidnappedHelper to support both Players and NPCs
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null) {
return InteractionResult.PASS; // Target cannot be restrained
}
// Get kidnapper state (player using the item)
IBondageState kidnapperState = KidnappedHelper.getKidnappedState(player);
if (kidnapperState == null) {
return InteractionResult.FAIL;
}
// Already tied - try to swap binds (if not locked)
// Check stack.isEmpty() first to prevent accidental unbinding when
// the original stack was consumed (e.g., rapid clicks after tying completes)
if (targetState.isTiedUp()) {
if (stack.isEmpty()) {
// No bind in hand - can't swap, just pass
return InteractionResult.PASS;
}
ItemStack oldBind = targetState.replaceEquipment(BodyRegionV2.ARMS, stack.copy(), false);
if (!oldBind.isEmpty()) {
stack.shrink(1);
targetState.kidnappedDropItem(oldBind);
TiedUpMod.LOGGER.debug(
"[ItemBind] Swapped bind on {} - dropped old bind",
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
// Locked or failed - can't swap
return InteractionResult.PASS;
}
// Phase 7 FIX: Can't tie others if you're tied yourself
if (kidnapperState.isTiedUp()) {
TiedUpMod.LOGGER.debug(
"[ItemBind] {} tried to tie but is tied themselves",
player.getName().getString()
);
return InteractionResult.PASS;
}
// ========================================
// SECURITY: Distance and line-of-sight validation (skip for self-tying)
// ========================================
boolean isSelfTying = player.equals(target);
if (!isSelfTying) {
double maxTieDistance = 4.0; // Max distance to tie (blocks)
double distance = player.distanceTo(target);
if (distance > maxTieDistance) {
TiedUpMod.LOGGER.warn(
"[ItemBind] {} tried to tie {} from too far away ({} blocks)",
player.getName().getString(),
target.getName().getString(),
String.format("%.1f", distance)
);
return InteractionResult.PASS;
}
// Check line-of-sight (must be able to see target)
if (!player.hasLineOfSight(target)) {
TiedUpMod.LOGGER.warn(
"[ItemBind] {} tried to tie {} without line of sight",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.PASS;
}
}
// Phase 14.2.6: Unified tying for both Players and NPCs
return handleTying(stack, player, target, targetState);
}
/**
* Handle tying any target entity (Player or NPC).
* Phase 14.2.6: Unified tying system for all IBondageState entities.
*
* Uses progress-based system:
* - update() marks the tick as active
* - tick() in RestraintTaskTickHandler.onPlayerTick() handles progress increment/decrement
*/
private InteractionResult handleTying(
ItemStack stack,
Player player,
LivingEntity target,
IBondageState targetState
) {
// Get kidnapper's state to track the tying task
PlayerBindState kidnapperState = PlayerBindState.getInstance(player);
if (kidnapperState == null) {
return InteractionResult.FAIL;
}
// Get tying duration from GameRule (default: 5 seconds)
int tyingSeconds = getTyingDuration(player);
// Get current tying task (if any)
TyingTask currentTask = kidnapperState.getCurrentTyingTask();
// Check if we should start a new task or continue existing one
if (
currentTask == null ||
!currentTask.isSameTarget(target) ||
currentTask.isStopped() ||
!ItemStack.matches(currentTask.getBind(), stack)
) {
// Create new tying task (works for both Players and NPCs)
TyingPlayerTask newTask = new TyingPlayerTask(
stack.copy(),
targetState,
target,
tyingSeconds,
player.level(),
player // Pass kidnapper for SystemMessage
);
// FIX: Store the inventory slot for consumption when task completes
// This prevents duplication AND allows refund if task is cancelled
int sourceSlot = player.getInventory().selected;
newTask.setSourceSlot(sourceSlot);
newTask.setSourcePlayer(player);
// Start new task
kidnapperState.setCurrentTyingTask(newTask);
newTask.setUpTargetState(); // Initialize target's restraint state (only for players)
newTask.start();
currentTask = newTask;
TiedUpMod.LOGGER.debug(
"[ItemBind] {} started tying {} ({} seconds, slot={})",
player.getName().getString(),
target.getName().getString(),
tyingSeconds,
sourceSlot
);
} else {
// Continue existing task - ensure kidnapper is set
if (currentTask instanceof TyingPlayerTask playerTask) {
playerTask.setKidnapper(player);
}
}
// Mark this tick as active (progress will increase in onPlayerTick)
// The tick() method in RestraintTaskTickHandler.onPlayerTick handles progress increment/decrement
currentTask.update();
return InteractionResult.SUCCESS;
}
/**
* Called when player right-clicks with the bind item (not targeting an entity).
* Cancels any ongoing tying task.
*
* Based on original ItemBind.onItemRightClick() (1.12.2)
*
* @param context The use context
* @return FAIL to cancel the action
*/
@Override
public InteractionResult useOn(UseOnContext context) {
// Only run on server side
if (context.getLevel().isClientSide) {
return InteractionResult.SUCCESS;
}
Player player = context.getPlayer();
if (player == null) {
return InteractionResult.FAIL;
}
// Cancel any ongoing tying task
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return InteractionResult.FAIL;
}
// Check for active tying task (unified for both players and NPCs)
TyingTask task = state.getCurrentTyingTask();
if (task != null) {
task.stop();
state.setCurrentTyingTask(null);
LivingEntity target = task.getTargetEntity();
String targetName =
target != null ? target.getName().getString() : "???";
String kidnapperName = player.getName().getString();
// Send cancellation packet to kidnapper
if (player instanceof ServerPlayer serverPlayer) {
PacketTying packet = new PacketTying(
-1,
task.getMaxSeconds(),
true,
targetName
);
ModNetwork.sendToPlayer(packet, serverPlayer);
}
// Send cancellation packet to target (if it's a player)
if (target instanceof ServerPlayer serverTarget) {
PacketTying packet = new PacketTying(
-1,
task.getMaxSeconds(),
false,
kidnapperName
);
ModNetwork.sendToPlayer(packet, serverTarget);
}
TiedUpMod.LOGGER.debug(
"[ItemBind] {} cancelled tying task",
player.getName().getString()
);
}
return InteractionResult.FAIL;
}
/**
* Get the tying duration in seconds from GameRule.
* Phase 6: Reads from custom GameRule "tyingPlayerTime"
*
* @param player The player (for accessing world/GameRules)
* @return Duration in seconds (default: 5)
*/
private int getTyingDuration(Player player) {
return SettingsAccessor.getTyingPlayerTime(player.level().getGameRules());
}
// ========== Phase 7: Resistance System (via IHasResistance) ==========
/**
* Get the item name for GameRule lookup.
* Each subclass must implement this to return its identifier (e.g., "rope", "chain", etc.)
*
* @return Item name for resistance GameRule lookup
*/
public abstract String getItemName();
// ========== Phase 15: Pose System ==========
/**
* Get the pose type for this bind item.
* Determines which animation/pose is applied when this item is equipped.
*
* Override in subclasses for special poses (straitjacket, wrap, latex_sack).
*
* @return PoseType for this bind (default: STANDARD)
*/
public PoseType getPoseType() {
return PoseType.STANDARD;
}
/**
* Implementation of IHasResistance.getResistanceId().
* Delegates to getItemName() for backward compatibility with subclasses.
*
* @return Item identifier for resistance lookup
*/
@Override
public String getResistanceId() {
return getItemName();
}
/**
* Called when the entity struggles against this bind.
* Plays struggle sound and shows message.
*
* Based on original ItemBind struggle notification (1.12.2)
*
* @param entity The entity struggling
*/
@Override
public void notifyStruggle(LivingEntity entity) {
// Play struggle sound
TiedUpSounds.playStruggleSound(entity);
// Log the struggle attempt
TiedUpMod.LOGGER.debug(
"[ItemBind] {} is struggling against bind",
entity.getName().getString()
);
// Notify nearby players if the entity is a player
if (entity instanceof ServerPlayer serverPlayer) {
serverPlayer.displayClientMessage(
Component.translatable("tiedup.message.struggling"),
true // Action bar
);
}
}
// ILockable implementation inherited from interface default methods
}

View File

@@ -0,0 +1,90 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.util.EquipmentInteractionHelper;
import java.util.List;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Base class for blindfold items (classic blindfold, mask, hood, etc.)
* These items obstruct a player's vision when equipped.
*
* Based on original ItemBlindfold from 1.12.2
*
* Phase 1: Basic implementation without rendering effects (added in Phase 5)
* Phase 8.5: Added interactLivingEntity for equipment on tied players
*/
public abstract class ItemBlindfold
extends Item
implements IBondageItem, IHasBlindingEffect, IAdjustable, ILockable
{
public ItemBlindfold(Properties properties) {
super(properties);
}
@Override
public BodyRegionV2 getBodyRegion() {
return BodyRegionV2.EYES;
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
appendLockTooltip(stack, tooltip);
}
/**
* All blindfolds can be adjusted to better fit player skins.
* @return true - blindfolds support position adjustment
*/
@Override
public boolean canBeAdjusted() {
return true;
}
/**
* Called when player right-clicks another entity with this blindfold.
* Allows putting blindfold on tied-up entities (Players and NPCs).
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
return EquipmentInteractionHelper.equipOnTarget(
stack,
player,
target,
state -> state.isBlindfolded(),
(state, item) -> state.equip(BodyRegionV2.EYES, item),
(state, item) -> state.replaceEquipment(BodyRegionV2.EYES, item, false),
(p, t) ->
SystemMessageManager.sendToTarget(
p,
t,
SystemMessageManager.MessageCategory.BLINDFOLDED
),
"ItemBlindfold"
);
}
// ILockable implementation inherited from interface default methods
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
package com.tiedup.remake.items.base;
import java.util.Random;
/**
* Standard colors for bondage items.
* Colors are stored in NBT and used for texture selection.
*
* Based on original mod color variants:
* - 16 standard colors (matching Minecraft dye colors)
* - 2 special variants for tape (caution, clear)
*/
public enum ItemColor {
// Standard 16 colors (modelId used for CustomModelData)
BLACK("black", 0x1D1D21, 1),
BLUE("blue", 0x3C44AA, 2),
BROWN("brown", 0x835432, 3),
CYAN("cyan", 0x169C9C, 4),
GRAY("gray", 0x474F52, 5),
GREEN("green", 0x5E7C16, 6),
LIGHT_BLUE("light_blue", 0x3AB3DA, 7),
LIME("lime", 0x80C71F, 8),
MAGENTA("magenta", 0xC74EBD, 9),
ORANGE("orange", 0xF9801D, 10),
PINK("pink", 0xF38BAA, 11),
PURPLE("purple", 0x8932B8, 12),
RED("red", 0xB02E26, 13),
SILVER("silver", 0x9D9D97, 14), // Also known as light_gray
WHITE("white", 0xF9FFFE, 15),
YELLOW("yellow", 0xFED83D, 16),
// Special variants (for duct_tape/tape_gag)
CAUTION("caution", 0xFFCC00, 17),
CLEAR("clear", 0xCCCCCC, 18);
private static final Random RANDOM = new Random();
/** Standard colors (excludes special variants like caution/clear) */
private static final ItemColor[] STANDARD_COLORS = {
BLACK,
BLUE,
BROWN,
CYAN,
GRAY,
GREEN,
LIGHT_BLUE,
LIME,
MAGENTA,
ORANGE,
PINK,
PURPLE,
RED,
SILVER,
WHITE,
YELLOW,
};
private final String name;
private final int hexColor;
private final int modelId;
ItemColor(String name, int hexColor, int modelId) {
this.name = name;
this.hexColor = hexColor;
this.modelId = modelId;
}
/**
* Get the name used in NBT and texture paths.
* Example: "red" -> textures/item/ropes_red.png
*/
public String getName() {
return name;
}
/**
* Get the hex color value for tinting or display.
*/
public int getHexColor() {
return hexColor;
}
/**
* Get the model ID used for CustomModelData.
* This allows item models to use overrides for different colors.
*/
public int getModelId() {
return modelId;
}
/**
* Get a random standard color (excludes caution/clear).
*/
public static ItemColor getRandomStandard() {
return STANDARD_COLORS[RANDOM.nextInt(STANDARD_COLORS.length)];
}
/**
* Get a random standard color using a specific Random instance.
*/
public static ItemColor getRandomStandard(Random random) {
return STANDARD_COLORS[random.nextInt(STANDARD_COLORS.length)];
}
/**
* Get a color by name (for NBT deserialization).
* @return The color, or null if not found
*/
public static ItemColor fromName(String name) {
if (name == null || name.isEmpty()) {
return null;
}
for (ItemColor color : values()) {
if (color.name.equals(name)) {
return color;
}
}
return null;
}
/**
* Check if this is a special color (caution/clear).
* Special colors are only used for tape items.
*/
public boolean isSpecial() {
return this == CAUTION || this == CLEAR;
}
/**
* Get the red component as a float (0-1).
*/
public float getRed() {
return ((hexColor >> 16) & 0xFF) / 255.0f;
}
/**
* Get the green component as a float (0-1).
*/
public float getGreen() {
return ((hexColor >> 8) & 0xFF) / 255.0f;
}
/**
* Get the blue component as a float (0-1).
*/
public float getBlue() {
return (hexColor & 0xFF) / 255.0f;
}
}

View File

@@ -0,0 +1,90 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.util.EquipmentInteractionHelper;
import com.tiedup.remake.util.TiedUpSounds;
import java.util.List;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Base class for earplug items.
* These items block or reduce sounds when equipped.
*
* Based on original ItemEarplugs from 1.12.2
*
* Phase 8.5: Basic implementation + equipment mechanics
* Phase future: Sound blocking effect
*/
public abstract class ItemEarplugs
extends Item
implements IBondageItem, ILockable
{
public ItemEarplugs(Properties properties) {
super(properties.stacksTo(16)); // Earplugs can stack to 16
}
@Override
public BodyRegionV2 getBodyRegion() {
return BodyRegionV2.EARS;
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
appendLockTooltip(stack, tooltip);
}
/**
* Called when player right-clicks another entity with earplugs.
* Allows putting earplugs on tied-up entities (Players and NPCs).
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
return EquipmentInteractionHelper.equipOnTarget(
stack,
player,
target,
state -> state.hasEarplugs(),
(state, item) -> state.equip(BodyRegionV2.EARS, item),
(state, item) -> state.replaceEquipment(BodyRegionV2.EARS, item, false),
(p, t) ->
SystemMessageManager.sendToTarget(
p,
t,
SystemMessageManager.MessageCategory.EARPLUGS_ON
),
"ItemEarplugs",
null, // No pre-equip hook
(s, p, t, state) -> TiedUpSounds.playEarplugsEquipSound(t), // Post-equip: play sound
null // No replace check
);
}
// Sound blocking implemented in:
// - client/events/EarplugSoundHandler.java (event interception)
// - client/MuffledSoundInstance.java (volume/pitch wrapper)
// - Configurable via ModConfig.CLIENT.earplugVolumeMultiplier
// ILockable implementation inherited from interface default methods
}

View File

@@ -0,0 +1,95 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.util.EquipmentInteractionHelper;
import com.tiedup.remake.util.GagMaterial;
import java.util.List;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Base class for gag items (ball gag, cloth gag, tape, etc.)
* These items prevent or muffle a player's speech when equipped.
*
* Based on original ItemGag from 1.12.2
*
* Phase 1: Basic implementation without gag talk or adjustment (added later)
* Phase 8.5: Added interactLivingEntity for equipment on tied players
* Phase 12: Added GagTalk material system
*/
public abstract class ItemGag
extends Item
implements IBondageItem, IHasGaggingEffect, IAdjustable, ILockable
{
private final GagMaterial material;
public ItemGag(Properties properties, GagMaterial material) {
super(properties);
this.material = material;
}
public GagMaterial getGagMaterial() {
return this.material;
}
@Override
public BodyRegionV2 getBodyRegion() {
return BodyRegionV2.MOUTH;
}
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
appendLockTooltip(stack, tooltip);
}
/**
* All gags can be adjusted to better fit player skins.
* @return true - gags support position adjustment
*/
@Override
public boolean canBeAdjusted() {
return true;
}
/**
* Called when player right-clicks another entity with this gag.
* Allows putting gag on tied-up entities (Players and NPCs).
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
return EquipmentInteractionHelper.equipOnTarget(
stack,
player,
target,
state -> state.isGagged(),
(state, item) -> state.equip(BodyRegionV2.MOUTH, item),
(state, item) -> state.replaceEquipment(BodyRegionV2.MOUTH, item, false),
SystemMessageManager::sendGagged,
"ItemGag"
);
}
// ILockable implementation inherited from interface default methods
}

View File

@@ -0,0 +1,72 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.util.EquipmentInteractionHelper;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
/**
* Base class for mittens items.
* These items block hand interactions (mining, placing, using items) when equipped.
*
* Phase 14.4: Mittens system
*
* Restrictions when wearing mittens:
* - Cannot mine/break blocks
* - Cannot place blocks
* - Cannot use items
* - Cannot attack (0 damage punch allowed)
*
* Allowed:
* - Push buttons/levers
* - Open doors
*/
public abstract class ItemMittens
extends Item
implements IBondageItem, ILockable
{
public ItemMittens(Properties properties) {
super(properties);
}
@Override
public BodyRegionV2 getBodyRegion() {
return BodyRegionV2.HANDS;
}
/**
* Called when player right-clicks another entity with these mittens.
* Allows putting mittens on tied-up entities (Players and NPCs).
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
return EquipmentInteractionHelper.equipOnTarget(
stack,
player,
target,
state -> state.hasMittens(),
(state, item) -> state.equip(BodyRegionV2.HANDS, item),
(state, item) -> state.replaceEquipment(BodyRegionV2.HANDS, item, false),
(p, t) ->
SystemMessageManager.sendToTarget(
p,
t,
SystemMessageManager.MessageCategory.MITTENS_ON
),
"ItemMittens"
);
}
// ILockable implementation inherited from interface default methods
}

View File

@@ -0,0 +1,203 @@
package com.tiedup.remake.items.base;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Base class for items that maintain a relationship between an owner and a target.
* This is used for "pairing" mechanics like shocker controllers and GPS locators.
*
* <p>Data is stored directly in the ItemStack's NBT tag using UUIDs for reliability
* and Names for display in tooltips.</p>
*/
public abstract class ItemOwnerTarget extends Item {
public ItemOwnerTarget(Properties properties) {
super(properties);
}
/**
* Links this item to a specific owner.
* @param stack The item instance
* @param owner The player who now owns this tool
*/
public void setOwner(ItemStack stack, Player owner) {
if (owner != null) {
CompoundTag nbt = stack.getOrCreateTag();
nbt.putUUID("ownerId", owner.getUUID());
nbt.putString("ownerName", owner.getName().getString());
}
}
/**
* Directly sets the owner UUID without a player instance.
*/
public void setOwnerId(ItemStack stack, UUID uuid) {
if (uuid != null) {
stack.getOrCreateTag().putUUID("ownerId", uuid);
}
}
/**
* Clears all ownership data from the item.
*/
public void removeOwner(ItemStack stack) {
CompoundTag nbt = stack.getTag();
if (nbt != null) {
nbt.remove("ownerId");
nbt.remove("ownerName");
}
}
/**
* Links this tool to a specific target (victim/slave).
* Used for TARGETED mode in shockers and locators.
*/
public void setTarget(ItemStack stack, Entity target) {
if (target != null) {
CompoundTag nbt = stack.getOrCreateTag();
nbt.putUUID("targetId", target.getUUID());
nbt.putString("targetName", target.getName().getString());
}
}
/**
* Retrieves the stored owner UUID.
* @return UUID or null if unclaimed
*/
public UUID getOwnerId(ItemStack stack) {
CompoundTag nbt = stack.getTag();
if (nbt != null && nbt.hasUUID("ownerId")) {
return nbt.getUUID("ownerId");
}
return null;
}
/**
* Retrieves the stored owner name for display purposes.
*/
public String getOwnerName(ItemStack stack) {
CompoundTag nbt = stack.getTag();
if (nbt != null && nbt.contains("ownerName")) {
return nbt.getString("ownerName");
}
return null;
}
/**
* Retrieves the stored target UUID.
*/
public UUID getTargetId(ItemStack stack) {
CompoundTag nbt = stack.getTag();
if (nbt != null && nbt.hasUUID("targetId")) {
return nbt.getUUID("targetId");
}
return null;
}
/**
* Retrieves the stored target name.
*/
public String getTargetName(ItemStack stack) {
CompoundTag nbt = stack.getTag();
if (nbt != null && nbt.contains("targetName")) {
return nbt.getString("targetName");
}
return null;
}
public boolean hasOwner(ItemStack stack) {
return getOwnerId(stack) != null;
}
public boolean hasTarget(ItemStack stack) {
return getTargetId(stack) != null;
}
/**
* Verification if the current player matches the item's stored owner.
*/
public boolean isOwner(ItemStack stack, Player player) {
return player != null && isOwner(stack, player.getUUID());
}
public boolean isOwner(ItemStack stack, UUID uuid) {
if (uuid != null && stack != null) {
UUID ownerUUID = getOwnerId(stack);
return uuid.equals(ownerUUID);
}
return false;
}
/**
* Check if a player instance matches the item's current target.
*/
// =====================================================
// TOOLTIP HELPERS
// =====================================================
/**
* Appends the "Owner: ..." or "Unclaimed (...)" tooltip line.
* @param unclaimedHint text shown when unclaimed (e.g. "Right-click a player")
*/
protected void appendOwnerTooltip(ItemStack stack, List<Component> tooltip, String unclaimedHint) {
if (hasOwner(stack)) {
tooltip.add(
Component.literal("Owner: ")
.withStyle(ChatFormatting.GOLD)
.append(Component.literal(getOwnerName(stack)).withStyle(ChatFormatting.WHITE))
);
} else {
tooltip.add(
Component.literal("Unclaimed (" + unclaimedHint + ")").withStyle(ChatFormatting.GRAY)
);
}
}
/**
* Resolves the display name for the current target, enriching it with
* the collar nickname if available.
* @return display name, possibly "Nickname (RealName)" if collar has a nickname
*/
protected String resolveTargetDisplayName(ItemStack stack, @Nullable Level level) {
String displayName = getTargetName(stack);
if (level != null && hasTarget(stack)) {
Player target = level.getPlayerByUUID(getTargetId(stack));
if (target != null) {
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState != null && targetState.hasCollar()) {
ItemStack collar = targetState.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem && collarItem.hasNickname(collar)) {
displayName = collarItem.getNickname(collar) + " (" + displayName + ")";
}
}
}
}
return displayName;
}
public boolean isTarget(ItemStack stack, Player potentialTarget) {
if (potentialTarget != null && stack != null) {
UUID playerUUID = potentialTarget.getUUID();
UUID targetUUID = getTargetId(stack);
return (
playerUUID != null &&
targetUUID != null &&
playerUUID.equals(targetUUID)
);
}
return false;
}
}

View File

@@ -0,0 +1,61 @@
package com.tiedup.remake.items.base;
/**
* Enum defining all knife variants with their properties.
* Used by GenericKnife to create knife items via factory pattern.
*
* Each tier has its own cutting speed and durability:
* - Stone: slow, low capacity (emergency tool)
* - Iron: medium speed, reliable capacity
* - Golden: fast, high capacity (can cut through a padlock)
*
* Durability consumed per second = cuttingSpeed (1 durability = 1 resistance).
*/
public enum KnifeVariant {
STONE("stone_knife", 100, 5), // 100 dura, 5 res/s → 20s, 100 total resistance
IRON("iron_knife", 160, 8), // 160 dura, 8 res/s → 20s, 160 total resistance
GOLDEN("golden_knife", 300, 12); // 300 dura, 12 res/s → 25s, 300 total resistance
private final String registryName;
private final int durability;
private final int cuttingSpeed;
KnifeVariant(String registryName, int durability, int cuttingSpeed) {
this.registryName = registryName;
this.durability = durability;
this.cuttingSpeed = cuttingSpeed;
}
public String getRegistryName() {
return registryName;
}
/**
* Get the durability (max damage) of this knife.
* Total resistance a knife can cut = durability (1:1 ratio with cuttingSpeed drain).
*
* @return Durability value
*/
public int getDurability() {
return durability;
}
/**
* Get the cutting speed (resistance removed per second).
* Also the durability consumed per second.
*
* @return Cutting speed in resistance/second
*/
public int getCuttingSpeed() {
return cuttingSpeed;
}
/**
* Get max cutting time in seconds.
*
* @return Max cutting time (durability / cuttingSpeed)
*/
public int getMaxCuttingTimeSeconds() {
return durability / cuttingSpeed;
}
}

View File

@@ -0,0 +1,35 @@
package com.tiedup.remake.items.base;
/**
* Enum defining all mittens variants.
* Used by GenericMittens to create mittens items via factory pattern.
*
* <p>Phase 14.4: Mittens system - blocks hand interactions when equipped.
*
* <p><b>Issue #12 fix:</b> Added textureSubfolder to eliminate string checks in renderers.
*/
public enum MittensVariant {
LEATHER("mittens", "mittens");
private final String registryName;
private final String textureSubfolder;
MittensVariant(String registryName, String textureSubfolder) {
this.registryName = registryName;
this.textureSubfolder = textureSubfolder;
}
public String getRegistryName() {
return registryName;
}
/**
* Get the texture subfolder for this mittens variant.
* Used by renderers to locate texture files.
*
* @return Subfolder path under textures/entity/bondage/ (e.g., "mittens")
*/
public String getTextureSubfolder() {
return textureSubfolder;
}
}

View File

@@ -0,0 +1,55 @@
package com.tiedup.remake.items.base;
/**
* Enum defining the different pose types for restrained entities.
* Each pose type has a corresponding animation file for players
* and pose method in BondagePoseHelper for NPCs.
*
* Phase 15: Pose system for different bind types
*/
public enum PoseType {
/** Standard tied pose - arms behind back, legs frozen */
STANDARD("tied_up_basic", "basic"),
/** Straitjacket pose - arms crossed in front */
STRAITJACKET("straitjacket", "straitjacket"),
/** Wrap pose - arms at sides, body wrapped */
WRAP("wrap", "wrap"),
/** Latex sack pose - full enclosure, legs together */
LATEX_SACK("latex_sack", "latex_sack"),
/** Dog pose - on all fours (crawling) */
DOG("tied_up_dog", "dog"),
/** Human chair pose - on all fours with straight limbs (table/furniture) */
HUMAN_CHAIR("human_chair", "human_chair");
private final String animationId;
private final String bindTypeName;
PoseType(String animationId, String bindTypeName) {
this.animationId = animationId;
this.bindTypeName = bindTypeName;
}
/**
* Get the animation file ID for this pose type.
* Used by PlayerAnimationManager to load the correct animation.
*
* @return Animation resource name (without path or extension)
*/
public String getAnimationId() {
return animationId;
}
/**
* Get the bind type name used in SIT/KNEEL animation IDs.
*
* @return Bind type name (e.g., "basic", "straitjacket", "wrap")
*/
public String getBindTypeName() {
return bindTypeName;
}
}

View File

@@ -0,0 +1,13 @@
package com.tiedup.remake.items.bondage3d;
/**
* Interface for items that have a 3D model configuration.
* Implement this to provide custom position, scale, and rotation for 3D rendering.
*/
public interface IHas3DModelConfig {
/**
* Get the 3D model configuration for rendering.
* @return The Model3DConfig with position, scale, and rotation offsets
*/
Model3DConfig getModelConfig();
}

View File

@@ -0,0 +1,67 @@
package com.tiedup.remake.items.bondage3d;
import java.util.Set;
/**
* Configuration immutable for a 3D item.
* Contains all parameters necessary for rendering.
*/
public record Model3DConfig(
String objPath, // "tiedup:models/obj/ball_gag/model.obj"
String texturePath, // "tiedup:models/obj/ball_gag/texture.png" (or null = use MTL)
float posOffsetX, // Horizontal offset
float posOffsetY, // Vertical offset (negative = lower)
float posOffsetZ, // Depth offset (positive = forward)
float scale, // Scale (1.0 = normal size)
float rotOffsetX, // Additional X rotation
float rotOffsetY, // Additional Y rotation
float rotOffsetZ, // Additional Z rotation
Set<String> tintMaterials // Material names to apply color tint (e.g., "Ball")
) {
/** Config without tinting */
public static Model3DConfig simple(String objPath, String texturePath) {
return new Model3DConfig(
objPath,
texturePath,
0,
0,
0,
1.0f,
0,
0,
0,
Set.of()
);
}
/** Config with position/rotation but no tinting */
public Model3DConfig(
String objPath,
String texturePath,
float posOffsetX,
float posOffsetY,
float posOffsetZ,
float scale,
float rotOffsetX,
float rotOffsetY,
float rotOffsetZ
) {
this(
objPath,
texturePath,
posOffsetX,
posOffsetY,
posOffsetZ,
scale,
rotOffsetX,
rotOffsetY,
rotOffsetZ,
Set.of()
);
}
/** Check if this item supports color tinting */
public boolean supportsTinting() {
return tintMaterials != null && !tintMaterials.isEmpty();
}
}

View File

@@ -0,0 +1,78 @@
package com.tiedup.remake.items.bondage3d.gags;
import com.tiedup.remake.items.base.ItemGag;
import com.tiedup.remake.items.bondage3d.IHas3DModelConfig;
import com.tiedup.remake.items.bondage3d.Model3DConfig;
import com.tiedup.remake.util.GagMaterial;
import java.util.Set;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import org.jetbrains.annotations.Nullable;
/**
* Ball Gag 3D - Extends ItemGag with 3D OBJ model rendering.
* All 3D configuration is defined here.
* Supports color variants via tinting the "Ball" material.
*/
public class ItemBallGag3D extends ItemGag implements IHas3DModelConfig {
// 3D config with "Ball" material tintable for color variants
private static final Model3DConfig CONFIG = new Model3DConfig(
"tiedup:models/obj/ball_gag/model.obj", // OBJ
"tiedup:models/obj/ball_gag/texture.png", // Explicit texture
0.0f, // posX
1.55f, // posY
0.0f, // posZ
1.0f, // scale
0.0f,
0.0f,
180.0f, // rotation
Set.of("Ball") // Tintable materials (for color variants)
);
public ItemBallGag3D() {
super(new Item.Properties().stacksTo(16), GagMaterial.BALL);
}
// ===== 3D Model Support =====
@Override
public boolean uses3DModel() {
return true;
}
@Override
@Nullable
public ResourceLocation get3DModelLocation() {
return ResourceLocation.tryParse(CONFIG.objPath());
}
/**
* Returns the complete 3D configuration for the renderer.
*/
@Override
public Model3DConfig getModelConfig() {
return CONFIG;
}
/**
* Explicit texture (if non-null, overrides MTL map_Kd).
*/
@Nullable
public ResourceLocation getExplicitTexture() {
String path = CONFIG.texturePath();
return path != null ? ResourceLocation.tryParse(path) : null;
}
// ===== Gag Properties =====
@Override
public String getTextureSubfolder() {
return "ballgags/normal"; // Fallback if 3D fails
}
@Override
public boolean canAttachPadlock() {
return true;
}
}

View File

@@ -0,0 +1,152 @@
package com.tiedup.remake.items.clothes;
import java.util.EnumSet;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Immutable snapshot of clothes properties for rendering.
* Extracted from ItemStack NBT for efficient access during render.
*
* <p>This record is created once per render frame to avoid repeated NBT lookups.
*/
public record ClothesProperties(
@Nullable String dynamicTextureUrl,
boolean fullSkin,
boolean smallArms,
boolean keepHead,
EnumSet<LayerPart> visibleLayers
) {
/**
* Enum representing the six body layer parts that can be hidden.
*/
public enum LayerPart {
HEAD,
BODY,
LEFT_ARM,
RIGHT_ARM,
LEFT_LEG,
RIGHT_LEG,
}
/**
* Create a ClothesProperties from an ItemStack.
*
* @param stack The clothes ItemStack
* @return ClothesProperties, or null if not a GenericClothes item
*/
@Nullable
public static ClothesProperties fromStack(ItemStack stack) {
if (
stack.isEmpty() ||
!(stack.getItem() instanceof GenericClothes clothes)
) {
return null;
}
String url = clothes.getDynamicTextureUrl(stack);
boolean fullSkin = clothes.isFullSkinEnabled(stack);
boolean smallArms = clothes.shouldForceSmallArms(stack);
boolean keepHead = clothes.isKeepHeadEnabled(stack);
EnumSet<LayerPart> visible = EnumSet.noneOf(LayerPart.class);
if (
clothes.isLayerEnabled(stack, GenericClothes.LAYER_HEAD)
) visible.add(LayerPart.HEAD);
if (
clothes.isLayerEnabled(stack, GenericClothes.LAYER_BODY)
) visible.add(LayerPart.BODY);
if (
clothes.isLayerEnabled(stack, GenericClothes.LAYER_LEFT_ARM)
) visible.add(LayerPart.LEFT_ARM);
if (
clothes.isLayerEnabled(stack, GenericClothes.LAYER_RIGHT_ARM)
) visible.add(LayerPart.RIGHT_ARM);
if (
clothes.isLayerEnabled(stack, GenericClothes.LAYER_LEFT_LEG)
) visible.add(LayerPart.LEFT_LEG);
if (
clothes.isLayerEnabled(stack, GenericClothes.LAYER_RIGHT_LEG)
) visible.add(LayerPart.RIGHT_LEG);
return new ClothesProperties(
url,
fullSkin,
smallArms,
keepHead,
visible
);
}
/**
* Check if a dynamic texture URL is set.
*
* @return true if a URL is available
*/
public boolean hasUrl() {
return dynamicTextureUrl != null && !dynamicTextureUrl.isEmpty();
}
/**
* Check if all layers are visible (default state).
*
* @return true if all 6 layers are visible
*/
public boolean allLayersVisible() {
return visibleLayers.size() == 6;
}
/**
* Check if a specific layer is visible.
*
* @param part The layer part to check
* @return true if visible
*/
public boolean isLayerVisible(LayerPart part) {
return visibleLayers.contains(part);
}
/**
* Encode layer visibility as a byte bitfield.
* Used for network packets.
*
* <p>Bit positions:
* <ul>
* <li>0: HEAD</li>
* <li>1: BODY</li>
* <li>2: LEFT_ARM</li>
* <li>3: RIGHT_ARM</li>
* <li>4: LEFT_LEG</li>
* <li>5: RIGHT_LEG</li>
* </ul>
*
* @return Bitfield byte (0b111111 = all visible)
*/
public byte encodeLayerVisibility() {
byte bits = 0;
if (visibleLayers.contains(LayerPart.HEAD)) bits |= 0b000001;
if (visibleLayers.contains(LayerPart.BODY)) bits |= 0b000010;
if (visibleLayers.contains(LayerPart.LEFT_ARM)) bits |= 0b000100;
if (visibleLayers.contains(LayerPart.RIGHT_ARM)) bits |= 0b001000;
if (visibleLayers.contains(LayerPart.LEFT_LEG)) bits |= 0b010000;
if (visibleLayers.contains(LayerPart.RIGHT_LEG)) bits |= 0b100000;
return bits;
}
/**
* Decode layer visibility from a byte bitfield.
*
* @param bits The bitfield byte
* @return EnumSet of visible layers
*/
public static EnumSet<LayerPart> decodeLayerVisibility(byte bits) {
EnumSet<LayerPart> visible = EnumSet.noneOf(LayerPart.class);
if ((bits & 0b000001) != 0) visible.add(LayerPart.HEAD);
if ((bits & 0b000010) != 0) visible.add(LayerPart.BODY);
if ((bits & 0b000100) != 0) visible.add(LayerPart.LEFT_ARM);
if ((bits & 0b001000) != 0) visible.add(LayerPart.RIGHT_ARM);
if ((bits & 0b010000) != 0) visible.add(LayerPart.LEFT_LEG);
if ((bits & 0b100000) != 0) visible.add(LayerPart.RIGHT_LEG);
return visible;
}
}

View File

@@ -0,0 +1,526 @@
package com.tiedup.remake.items.clothes;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.network.sync.SyncManager;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Generic clothes item with full NBT-based configuration.
*
* <p>Clothes are cosmetic items that can:
* <ul>
* <li>Use dynamic textures from URLs</li>
* <li>Replace the entire player skin (full-skin mode)</li>
* <li>Force slim arm model</li>
* <li>Control visibility of wearer's body parts</li>
* <li>Be locked with padlocks</li>
* </ul>
*
* <p>Unlike other bondage items, clothes have NO gameplay effects - they are purely visual.
*/
public class GenericClothes extends Item implements ILockable, IV2BondageItem {
// ========== NBT KEYS ==========
public static final String NBT_DYNAMIC_TEXTURE = "dynamicTexture";
public static final String NBT_FULL_SKIN = "fullSkin";
public static final String NBT_SMALL_ARMS = "smallArms";
public static final String NBT_KEEP_HEAD = "keepHead";
public static final String NBT_LAYER_VISIBILITY = "layerVisibility";
public static final String NBT_LOCKED = "locked";
public static final String NBT_LOCKABLE = "lockable";
public static final String NBT_LOCKED_BY_KEY_UUID = "lockedByKeyUUID";
// Layer visibility keys
public static final String LAYER_HEAD = "head";
public static final String LAYER_BODY = "body";
public static final String LAYER_LEFT_ARM = "leftArm";
public static final String LAYER_RIGHT_ARM = "rightArm";
public static final String LAYER_LEFT_LEG = "leftLeg";
public static final String LAYER_RIGHT_LEG = "rightLeg";
public GenericClothes() {
super(new Item.Properties().stacksTo(16));
}
// ========== Lifecycle Hooks ==========
@Override
public void onEquipped(ItemStack stack, LivingEntity entity) {
// Clothes have no special equip effects - purely cosmetic
}
@Override
public void onUnequipped(ItemStack stack, LivingEntity entity) {
// Clothes have no special unequip effects
}
/**
* Called when player right-clicks another entity with clothes.
* Allows putting clothes on tied-up entities (Players and NPCs).
*
* Unlike other bondage items, clothes can also be put on non-tied players
* if game rules allow it (roleplay scenarios).
*
* @param stack The item stack
* @param player The player using the item
* @param target The entity being interacted with
* @param hand The hand holding the item
* @return SUCCESS if clothes equipped/replaced, PASS otherwise
*/
@Override
public InteractionResult interactLivingEntity(
ItemStack stack,
Player player,
LivingEntity target,
InteractionHand hand
) {
// Server-side only
if (player.level().isClientSide) {
return InteractionResult.SUCCESS;
}
// Check if target can wear clothes (Player, EntityDamsel, EntityKidnapper)
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
if (targetState == null) {
return InteractionResult.PASS; // Entity cannot wear clothes
}
// Unlike gags/blindfolds, clothes can be put on non-tied players too
// But if tied, always allowed. If not tied, check if target allows it.
if (!targetState.isTiedUp() && !targetState.canChangeClothes(player)) {
return InteractionResult.PASS;
}
// Case 1: No clothes yet - equip new one
if (!targetState.hasClothes()) {
ItemStack clothesCopy = stack.copyWithCount(1);
targetState.equip(BodyRegionV2.TORSO, clothesCopy);
stack.shrink(1);
// Sync equipment to all tracking clients
if (target instanceof ServerPlayer serverPlayer) {
SyncManager.syncInventory(serverPlayer);
SyncManager.syncClothesConfig(serverPlayer);
}
TiedUpMod.LOGGER.info(
"[GenericClothes] {} put clothes on {}",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
// Case 2: Already has clothes - replace them
else {
ItemStack clothesCopy = stack.copyWithCount(1);
ItemStack oldClothes = targetState.replaceEquipment(
BodyRegionV2.TORSO, clothesCopy, false
);
if (!oldClothes.isEmpty()) {
stack.shrink(1);
targetState.kidnappedDropItem(oldClothes);
// Sync equipment to all tracking clients
if (target instanceof ServerPlayer serverPlayer) {
SyncManager.syncInventory(serverPlayer);
SyncManager.syncClothesConfig(serverPlayer);
}
TiedUpMod.LOGGER.info(
"[GenericClothes] {} replaced clothes on {}",
player.getName().getString(),
target.getName().getString()
);
return InteractionResult.SUCCESS;
}
}
return InteractionResult.PASS;
}
// ========== Dynamic Texture Methods ==========
/**
* Get the dynamic texture URL from this clothes item.
*
* @param stack The ItemStack to check
* @return The URL string, or null if not set
*/
@Nullable
public String getDynamicTextureUrl(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_DYNAMIC_TEXTURE)) {
String url = tag.getString(NBT_DYNAMIC_TEXTURE);
return url.isEmpty() ? null : url;
}
return null;
}
/**
* Set the dynamic texture URL for this clothes item.
*
* @param stack The ItemStack to modify
* @param url The URL to set
*/
public void setDynamicTextureUrl(ItemStack stack, String url) {
if (url != null && !url.isEmpty()) {
stack.getOrCreateTag().putString(NBT_DYNAMIC_TEXTURE, url);
}
}
/**
* Remove the dynamic texture URL from this clothes item.
*
* @param stack The ItemStack to modify
*/
public void removeDynamicTextureUrl(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null) {
tag.remove(NBT_DYNAMIC_TEXTURE);
}
}
/**
* Check if this clothes item has a dynamic texture URL set.
*
* @param stack The ItemStack to check
* @return true if a URL is set
*/
public boolean hasDynamicTextureUrl(ItemStack stack) {
return getDynamicTextureUrl(stack) != null;
}
// ========== Full Skin / Small Arms Methods ==========
/**
* Check if full-skin mode is enabled.
* In full-skin mode, the clothes texture replaces the entire player skin.
*
* @param stack The ItemStack to check
* @return true if full-skin mode is enabled
*/
public boolean isFullSkinEnabled(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_FULL_SKIN);
}
/**
* Set full-skin mode.
*
* @param stack The ItemStack to modify
* @param enabled true to enable full-skin mode
*/
public void setFullSkinEnabled(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_FULL_SKIN, enabled);
}
/**
* Check if small arms (slim model) should be forced.
*
* @param stack The ItemStack to check
* @return true if small arms should be forced
*/
public boolean shouldForceSmallArms(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_SMALL_ARMS);
}
/**
* Set whether small arms (slim model) should be forced.
*
* @param stack The ItemStack to modify
* @param enabled true to force small arms
*/
public void setForceSmallArms(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_SMALL_ARMS, enabled);
}
/**
* Check if keep head mode is enabled.
* When enabled, the wearer's head/hat layers are preserved instead of being
* replaced by the clothes texture. Useful for keeping the original face.
*
* @param stack The ItemStack to check
* @return true if keep head mode is enabled
*/
public boolean isKeepHeadEnabled(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_KEEP_HEAD);
}
/**
* Set keep head mode.
* When enabled, the wearer's head/hat layers are preserved.
*
* @param stack The ItemStack to modify
* @param enabled true to keep the wearer's head
*/
public void setKeepHeadEnabled(ItemStack stack, boolean enabled) {
stack.getOrCreateTag().putBoolean(NBT_KEEP_HEAD, enabled);
}
// ========== Layer Visibility Methods ==========
/**
* Check if a specific body layer is enabled (visible on wearer).
* Defaults to true (visible) if not set.
*
* @param stack The ItemStack to check
* @param layer The layer key (use LAYER_* constants)
* @return true if the layer is visible
*/
public boolean isLayerEnabled(ItemStack stack, String layer) {
CompoundTag tag = stack.getTag();
if (tag == null || !tag.contains(NBT_LAYER_VISIBILITY)) {
return true; // Default: all layers visible
}
CompoundTag layers = tag.getCompound(NBT_LAYER_VISIBILITY);
// If not specified, default to visible
return !layers.contains(layer) || layers.getBoolean(layer);
}
/**
* Set the visibility of a specific body layer on the wearer.
*
* @param stack The ItemStack to modify
* @param layer The layer key (use LAYER_* constants)
* @param enabled true to show the layer, false to hide it
*/
public void setLayerEnabled(
ItemStack stack,
String layer,
boolean enabled
) {
CompoundTag tag = stack.getOrCreateTag();
CompoundTag layers = tag.contains(NBT_LAYER_VISIBILITY)
? tag.getCompound(NBT_LAYER_VISIBILITY)
: new CompoundTag();
layers.putBoolean(layer, enabled);
tag.put(NBT_LAYER_VISIBILITY, layers);
}
/**
* Get all layer visibility settings as a compound tag.
*
* @param stack The ItemStack to check
* @return The layer visibility compound, or null if not set
*/
@Nullable
public CompoundTag getLayerVisibility(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.contains(NBT_LAYER_VISIBILITY)) {
return tag.getCompound(NBT_LAYER_VISIBILITY);
}
return null;
}
// ========== IV2BondageItem Implementation ==========
private static final Set<BodyRegionV2> REGIONS =
Collections.unmodifiableSet(EnumSet.of(BodyRegionV2.TORSO));
@Override
public Set<BodyRegionV2> getOccupiedRegions() {
return REGIONS;
}
@Override
@Nullable
public ResourceLocation getModelLocation() {
return null; // Clothes use URL-texture rendering, not GLB models
}
@Override
public int getPosePriority() {
return 0; // Cosmetic item, never forces a pose
}
@Override
public int getEscapeDifficulty() {
return 0; // Cosmetic, no struggle resistance
}
@Override
public boolean supportsColor() {
return false; // Color is handled via URL texture, not variant system
}
@Override
public boolean supportsSlimModel() {
return false; // Slim/wide is handled via NBT smallArms flag, not model variants
}
@Override
public boolean canEquip(ItemStack stack, LivingEntity entity) {
return true;
}
@Override
public boolean canUnequip(ItemStack stack, LivingEntity entity) {
return true;
}
// ========== ILockable Implementation ==========
@Override
public ItemStack setLocked(ItemStack stack, boolean state) {
stack.getOrCreateTag().putBoolean(NBT_LOCKED, state);
if (!state) {
// When unlocking, clear lock-related data
clearLockResistance(stack);
setJammed(stack, false);
}
return stack;
}
@Override
public boolean isLocked(ItemStack stack) {
CompoundTag tag = stack.getTag();
return tag != null && tag.getBoolean(NBT_LOCKED);
}
@Override
public ItemStack setLockable(ItemStack stack, boolean state) {
stack.getOrCreateTag().putBoolean(NBT_LOCKABLE, state);
return stack;
}
@Override
public boolean isLockable(ItemStack stack) {
CompoundTag tag = stack.getTag();
// Default to true if not set
return (
tag == null ||
!tag.contains(NBT_LOCKABLE) ||
tag.getBoolean(NBT_LOCKABLE)
);
}
@Override
@Nullable
public UUID getLockedByKeyUUID(ItemStack stack) {
CompoundTag tag = stack.getTag();
if (tag != null && tag.hasUUID(NBT_LOCKED_BY_KEY_UUID)) {
return tag.getUUID(NBT_LOCKED_BY_KEY_UUID);
}
return null;
}
@Override
public void setLockedByKeyUUID(ItemStack stack, @Nullable UUID keyUUID) {
CompoundTag tag = stack.getOrCreateTag();
if (keyUUID != null) {
tag.putUUID(NBT_LOCKED_BY_KEY_UUID, keyUUID);
setLocked(stack, true);
initializeLockResistance(stack);
} else {
tag.remove(NBT_LOCKED_BY_KEY_UUID);
setLocked(stack, false);
}
}
// ========== Tooltip ==========
@Override
public void appendHoverText(
ItemStack stack,
@Nullable Level level,
List<Component> tooltip,
TooltipFlag flag
) {
super.appendHoverText(stack, level, tooltip, flag);
// Dynamic texture info
String url = getDynamicTextureUrl(stack);
if (url != null) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.has_url"
).withStyle(ChatFormatting.GREEN)
);
if (isFullSkinEnabled(stack)) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.full_skin"
).withStyle(ChatFormatting.AQUA)
);
}
if (shouldForceSmallArms(stack)) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.small_arms"
).withStyle(ChatFormatting.AQUA)
);
}
} else {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.no_url"
).withStyle(ChatFormatting.GRAY)
);
}
// Layer visibility info
CompoundTag layers = getLayerVisibility(stack);
if (layers != null) {
StringBuilder disabled = new StringBuilder();
if (!isLayerEnabled(stack, LAYER_HEAD)) disabled.append("head ");
if (!isLayerEnabled(stack, LAYER_BODY)) disabled.append("body ");
if (!isLayerEnabled(stack, LAYER_LEFT_ARM)) disabled.append(
"L.arm "
);
if (!isLayerEnabled(stack, LAYER_RIGHT_ARM)) disabled.append(
"R.arm "
);
if (!isLayerEnabled(stack, LAYER_LEFT_LEG)) disabled.append(
"L.leg "
);
if (!isLayerEnabled(stack, LAYER_RIGHT_LEG)) disabled.append(
"R.leg "
);
if (!disabled.isEmpty()) {
tooltip.add(
Component.translatable(
"item.tiedup.clothes.tooltip.layers_disabled",
disabled.toString().trim()
).withStyle(ChatFormatting.YELLOW)
);
}
}
// Lock info
if (isLocked(stack)) {
tooltip.add(
Component.translatable("item.tiedup.locked").withStyle(
ChatFormatting.RED
)
);
}
}
}