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:
68
src/main/java/com/tiedup/remake/items/GenericBind.java
Normal file
68
src/main/java/com/tiedup/remake/items/GenericBind.java
Normal 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();
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/tiedup/remake/items/GenericBlindfold.java
Normal file
37
src/main/java/com/tiedup/remake/items/GenericBlindfold.java
Normal 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();
|
||||
}
|
||||
}
|
||||
37
src/main/java/com/tiedup/remake/items/GenericEarplugs.java
Normal file
37
src/main/java/com/tiedup/remake/items/GenericEarplugs.java
Normal 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();
|
||||
}
|
||||
}
|
||||
72
src/main/java/com/tiedup/remake/items/GenericGag.java
Normal file
72
src/main/java/com/tiedup/remake/items/GenericGag.java
Normal 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;
|
||||
}
|
||||
}
|
||||
504
src/main/java/com/tiedup/remake/items/GenericKnife.java
Normal file
504
src/main/java/com/tiedup/remake/items/GenericKnife.java
Normal 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;
|
||||
}
|
||||
}
|
||||
38
src/main/java/com/tiedup/remake/items/GenericMittens.java
Normal file
38
src/main/java/com/tiedup/remake/items/GenericMittens.java
Normal 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();
|
||||
}
|
||||
}
|
||||
768
src/main/java/com/tiedup/remake/items/ItemAdminWand.java
Normal file
768
src/main/java/com/tiedup/remake/items/ItemAdminWand.java
Normal 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
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/tiedup/remake/items/ItemCellKey.java
Normal file
55
src/main/java/com/tiedup/remake/items/ItemCellKey.java
Normal 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;
|
||||
}
|
||||
}
|
||||
110
src/main/java/com/tiedup/remake/items/ItemChloroformBottle.java
Normal file
110
src/main/java/com/tiedup/remake/items/ItemChloroformBottle.java
Normal 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);
|
||||
}
|
||||
}
|
||||
156
src/main/java/com/tiedup/remake/items/ItemChokeCollar.java
Normal file
156
src/main/java/com/tiedup/remake/items/ItemChokeCollar.java
Normal 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;
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/tiedup/remake/items/ItemClassicCollar.java
Normal file
22
src/main/java/com/tiedup/remake/items/ItemClassicCollar.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
537
src/main/java/com/tiedup/remake/items/ItemCommandWand.java
Normal file
537
src/main/java/com/tiedup/remake/items/ItemCommandWand.java
Normal 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);
|
||||
}
|
||||
}
|
||||
240
src/main/java/com/tiedup/remake/items/ItemDebugWand.java
Normal file
240
src/main/java/com/tiedup/remake/items/ItemDebugWand.java
Normal 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
|
||||
}
|
||||
}
|
||||
370
src/main/java/com/tiedup/remake/items/ItemGpsCollar.java
Normal file
370
src/main/java/com/tiedup/remake/items/ItemGpsCollar.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
245
src/main/java/com/tiedup/remake/items/ItemGpsLocator.java
Normal file
245
src/main/java/com/tiedup/remake/items/ItemGpsLocator.java
Normal 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("_", " ");
|
||||
};
|
||||
}
|
||||
}
|
||||
36
src/main/java/com/tiedup/remake/items/ItemHood.java
Normal file
36
src/main/java/com/tiedup/remake/items/ItemHood.java
Normal 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";
|
||||
}
|
||||
}
|
||||
313
src/main/java/com/tiedup/remake/items/ItemKey.java
Normal file
313
src/main/java/com/tiedup/remake/items/ItemKey.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
395
src/main/java/com/tiedup/remake/items/ItemLockpick.java
Normal file
395
src/main/java/com/tiedup/remake/items/ItemLockpick.java
Normal 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;
|
||||
}
|
||||
}
|
||||
244
src/main/java/com/tiedup/remake/items/ItemMasterKey.java
Normal file
244
src/main/java/com/tiedup/remake/items/ItemMasterKey.java
Normal 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);
|
||||
}
|
||||
}
|
||||
25
src/main/java/com/tiedup/remake/items/ItemMedicalGag.java
Normal file
25
src/main/java/com/tiedup/remake/items/ItemMedicalGag.java
Normal 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";
|
||||
}
|
||||
}
|
||||
90
src/main/java/com/tiedup/remake/items/ItemPaddle.java
Normal file
90
src/main/java/com/tiedup/remake/items/ItemPaddle.java
Normal 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;
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/tiedup/remake/items/ItemPadlock.java
Normal file
45
src/main/java/com/tiedup/remake/items/ItemPadlock.java
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
289
src/main/java/com/tiedup/remake/items/ItemRag.java
Normal file
289
src/main/java/com/tiedup/remake/items/ItemRag.java
Normal 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();
|
||||
}
|
||||
}
|
||||
44
src/main/java/com/tiedup/remake/items/ItemRopeArrow.java
Normal file
44
src/main/java/com/tiedup/remake/items/ItemRopeArrow.java
Normal 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);
|
||||
}
|
||||
}
|
||||
133
src/main/java/com/tiedup/remake/items/ItemShockCollar.java
Normal file
133
src/main/java/com/tiedup/remake/items/ItemShockCollar.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
398
src/main/java/com/tiedup/remake/items/ItemShockerController.java
Normal file
398
src/main/java/com/tiedup/remake/items/ItemShockerController.java
Normal 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;
|
||||
}
|
||||
}
|
||||
116
src/main/java/com/tiedup/remake/items/ItemTaser.java
Normal file
116
src/main/java/com/tiedup/remake/items/ItemTaser.java
Normal 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);
|
||||
}
|
||||
}
|
||||
86
src/main/java/com/tiedup/remake/items/ItemTiedUpGuide.java
Normal file
86
src/main/java/com/tiedup/remake/items/ItemTiedUpGuide.java
Normal 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);
|
||||
}
|
||||
}
|
||||
71
src/main/java/com/tiedup/remake/items/ItemToken.java
Normal file
71
src/main/java/com/tiedup/remake/items/ItemToken.java
Normal 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
|
||||
}
|
||||
}
|
||||
181
src/main/java/com/tiedup/remake/items/ItemWhip.java
Normal file
181
src/main/java/com/tiedup/remake/items/ItemWhip.java
Normal 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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
215
src/main/java/com/tiedup/remake/items/ModCreativeTabs.java
Normal file
215
src/main/java/com/tiedup/remake/items/ModCreativeTabs.java
Normal 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()
|
||||
);
|
||||
}
|
||||
411
src/main/java/com/tiedup/remake/items/ModItems.java
Normal file
411
src/main/java/com/tiedup/remake/items/ModItems.java
Normal 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();
|
||||
}
|
||||
}
|
||||
173
src/main/java/com/tiedup/remake/items/base/AdjustmentHelper.java
Normal file
173
src/main/java/com/tiedup/remake/items/base/AdjustmentHelper.java
Normal 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);
|
||||
}
|
||||
}
|
||||
88
src/main/java/com/tiedup/remake/items/base/BindVariant.java
Normal file
88
src/main/java/com/tiedup/remake/items/base/BindVariant.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
163
src/main/java/com/tiedup/remake/items/base/GagVariant.java
Normal file
163
src/main/java/com/tiedup/remake/items/base/GagVariant.java
Normal 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;
|
||||
}
|
||||
}
|
||||
49
src/main/java/com/tiedup/remake/items/base/IAdjustable.java
Normal file
49
src/main/java/com/tiedup/remake/items/base/IAdjustable.java
Normal 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;
|
||||
}
|
||||
}
|
||||
102
src/main/java/com/tiedup/remake/items/base/IBondageItem.java
Normal file
102
src/main/java/com/tiedup/remake/items/base/IBondageItem.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
235
src/main/java/com/tiedup/remake/items/base/IHasResistance.java
Normal file
235
src/main/java/com/tiedup/remake/items/base/IHasResistance.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/main/java/com/tiedup/remake/items/base/IKnife.java
Normal file
15
src/main/java/com/tiedup/remake/items/base/IKnife.java
Normal 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
|
||||
}
|
||||
350
src/main/java/com/tiedup/remake/items/base/ILockable.java
Normal file
350
src/main/java/com/tiedup/remake/items/base/ILockable.java
Normal 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);
|
||||
}
|
||||
}
|
||||
623
src/main/java/com/tiedup/remake/items/base/ItemBind.java
Normal file
623
src/main/java/com/tiedup/remake/items/base/ItemBind.java
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
1459
src/main/java/com/tiedup/remake/items/base/ItemCollar.java
Normal file
1459
src/main/java/com/tiedup/remake/items/base/ItemCollar.java
Normal file
File diff suppressed because it is too large
Load Diff
149
src/main/java/com/tiedup/remake/items/base/ItemColor.java
Normal file
149
src/main/java/com/tiedup/remake/items/base/ItemColor.java
Normal 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;
|
||||
}
|
||||
}
|
||||
90
src/main/java/com/tiedup/remake/items/base/ItemEarplugs.java
Normal file
90
src/main/java/com/tiedup/remake/items/base/ItemEarplugs.java
Normal 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
|
||||
}
|
||||
95
src/main/java/com/tiedup/remake/items/base/ItemGag.java
Normal file
95
src/main/java/com/tiedup/remake/items/base/ItemGag.java
Normal 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
|
||||
}
|
||||
72
src/main/java/com/tiedup/remake/items/base/ItemMittens.java
Normal file
72
src/main/java/com/tiedup/remake/items/base/ItemMittens.java
Normal 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
|
||||
}
|
||||
203
src/main/java/com/tiedup/remake/items/base/ItemOwnerTarget.java
Normal file
203
src/main/java/com/tiedup/remake/items/base/ItemOwnerTarget.java
Normal 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;
|
||||
}
|
||||
}
|
||||
61
src/main/java/com/tiedup/remake/items/base/KnifeVariant.java
Normal file
61
src/main/java/com/tiedup/remake/items/base/KnifeVariant.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
55
src/main/java/com/tiedup/remake/items/base/PoseType.java
Normal file
55
src/main/java/com/tiedup/remake/items/base/PoseType.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user