Merge pull request 'feature/d01-branch-a-bridge' (#6) from feature/d01-branch-a-bridge into develop
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -14,8 +14,10 @@ import com.tiedup.remake.tasks.TyingTask;
|
|||||||
import com.tiedup.remake.tasks.V2TyingPlayerTask;
|
import com.tiedup.remake.tasks.V2TyingPlayerTask;
|
||||||
import com.tiedup.remake.util.KidnappedHelper;
|
import com.tiedup.remake.util.KidnappedHelper;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.BindModeHelper;
|
||||||
import com.tiedup.remake.v2.bondage.IV2BondageItem;
|
import com.tiedup.remake.v2.bondage.IV2BondageItem;
|
||||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||||
|
import com.tiedup.remake.v2.bondage.V2EquipResult;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import net.minecraft.network.FriendlyByteBuf;
|
import net.minecraft.network.FriendlyByteBuf;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
@@ -222,69 +224,116 @@ public class PacketSelfBondage {
|
|||||||
IV2BondageItem v2Item,
|
IV2BondageItem v2Item,
|
||||||
IBondageState state
|
IBondageState state
|
||||||
) {
|
) {
|
||||||
|
java.util.Set<BodyRegionV2> regions = v2Item.getOccupiedRegions(stack);
|
||||||
|
|
||||||
|
// Cannot self-collar
|
||||||
|
if (regions.contains(BodyRegionV2.NECK)) {
|
||||||
|
TiedUpMod.LOGGER.debug("[SelfBondage] {} tried to self-collar — blocked", player.getName().getString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ARMS: tying task with progress bar
|
||||||
|
if (regions.contains(BodyRegionV2.ARMS)) {
|
||||||
|
handleV2SelfBind(player, stack, v2Item, state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accessories (MOUTH, EYES, EARS, HANDS): instant equip
|
||||||
|
handleV2SelfAccessory(player, stack, v2Item, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleV2SelfBind(
|
||||||
|
ServerPlayer player,
|
||||||
|
ItemStack stack,
|
||||||
|
IV2BondageItem v2Item,
|
||||||
|
IBondageState state
|
||||||
|
) {
|
||||||
|
// Can't self-tie if already tied (check both V1 state and V2 region to prevent dual-bind)
|
||||||
|
if (state.isTiedUp() || V2EquipmentHelper.isRegionOccupied(player, BodyRegionV2.ARMS)) {
|
||||||
|
TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-tie but already tied", player.getName().getString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if all target regions are already occupied or blocked
|
// Check if all target regions are already occupied or blocked
|
||||||
boolean allBlocked = true;
|
boolean allBlocked = true;
|
||||||
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
|
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
|
||||||
if (
|
if (!V2EquipmentHelper.isRegionOccupied(player, region)
|
||||||
!V2EquipmentHelper.isRegionOccupied(player, region) &&
|
&& !V2EquipmentHelper.isRegionBlocked(player, region)) {
|
||||||
!V2EquipmentHelper.isRegionBlocked(player, region)
|
|
||||||
) {
|
|
||||||
allBlocked = false;
|
allBlocked = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (allBlocked) {
|
if (allBlocked) {
|
||||||
TiedUpMod.LOGGER.debug(
|
TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-equip but all regions occupied", player.getName().getString());
|
||||||
"[SelfBondage] {} tried V2 self-equip but all regions occupied",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerBindState playerState = PlayerBindState.getInstance(player);
|
PlayerBindState playerState = PlayerBindState.getInstance(player);
|
||||||
if (playerState == null) return;
|
if (playerState == null) return;
|
||||||
|
|
||||||
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(
|
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules());
|
||||||
player.level().getGameRules()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create V2 tying task (uses V2EquipmentHelper on completion, NOT putBindOn)
|
|
||||||
V2TyingPlayerTask newTask = new V2TyingPlayerTask(
|
V2TyingPlayerTask newTask = new V2TyingPlayerTask(
|
||||||
stack.copy(), // copy for display/matching
|
stack.copy(), stack, state, player, tyingSeconds, player.level(), player
|
||||||
stack, // live reference for consumption
|
|
||||||
state,
|
|
||||||
player, // target is self
|
|
||||||
tyingSeconds,
|
|
||||||
player.level(),
|
|
||||||
player // kidnapper is also self
|
|
||||||
);
|
);
|
||||||
|
|
||||||
TyingTask currentTask = playerState.getCurrentTyingTask();
|
TyingTask currentTask = playerState.getCurrentTyingTask();
|
||||||
|
if (currentTask == null
|
||||||
if (
|
|| !currentTask.isSameTarget(player)
|
||||||
currentTask == null ||
|
|| currentTask.isOutdated()
|
||||||
!currentTask.isSameTarget(player) ||
|
|| !ItemStack.matches(currentTask.getBind(), stack)) {
|
||||||
currentTask.isOutdated() ||
|
|
||||||
!ItemStack.matches(currentTask.getBind(), stack)
|
|
||||||
) {
|
|
||||||
// Start new task
|
|
||||||
playerState.setCurrentTyingTask(newTask);
|
playerState.setCurrentTyingTask(newTask);
|
||||||
newTask.start();
|
newTask.start();
|
||||||
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[SelfBondage] {} started V2 self-tying ({} seconds)",
|
|
||||||
player.getName().getString(),
|
|
||||||
tyingSeconds
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Continue existing task — just mark active
|
|
||||||
currentTask.update();
|
currentTask.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we started a new task, mark it active too
|
|
||||||
if (playerState.getCurrentTyingTask() == newTask) {
|
if (playerState.getCurrentTyingTask() == newTask) {
|
||||||
newTask.update();
|
newTask.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear completed task to prevent blocking future tying interactions
|
||||||
|
TyingTask activeTask = playerState.getCurrentTyingTask();
|
||||||
|
if (activeTask != null && activeTask.isStopped()) {
|
||||||
|
playerState.setCurrentTyingTask(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleV2SelfAccessory(
|
||||||
|
ServerPlayer player,
|
||||||
|
ItemStack stack,
|
||||||
|
IV2BondageItem v2Item,
|
||||||
|
IBondageState state
|
||||||
|
) {
|
||||||
|
// Can't equip accessories if arms are fully bound
|
||||||
|
ItemStack currentBind = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
|
||||||
|
if (!currentBind.isEmpty() && BindModeHelper.hasArmsBound(currentBind)) {
|
||||||
|
TiedUpMod.LOGGER.debug("[SelfBondage] {} can't self-accessory — arms bound", player.getName().getString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if region is occupied — try to swap
|
||||||
|
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
|
||||||
|
if (V2EquipmentHelper.isRegionOccupied(player, region)) {
|
||||||
|
ItemStack existing = V2EquipmentHelper.getInRegion(player, region);
|
||||||
|
// Can't swap if locked
|
||||||
|
if (existing.getItem() instanceof ILockable lockable && lockable.isLocked(existing)) {
|
||||||
|
TiedUpMod.LOGGER.debug("[SelfBondage] {} can't swap — current is locked", player.getName().getString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equip via V2EquipmentHelper (handles conflict resolution, displaced items)
|
||||||
|
V2EquipResult result = V2EquipmentHelper.equipItem(player, stack.copy());
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
for (ItemStack displaced : result.displaced()) {
|
||||||
|
player.spawnAtLocation(displaced);
|
||||||
|
}
|
||||||
|
stack.shrink(1);
|
||||||
|
SyncManager.syncInventory(player);
|
||||||
|
TiedUpMod.LOGGER.info("[SelfBondage] {} self-equipped V2 accessory", player.getName().getString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -320,10 +320,12 @@ public class PlayerEquipment {
|
|||||||
player,
|
player,
|
||||||
BodyRegionV2.ARMS
|
BodyRegionV2.ARMS
|
||||||
);
|
);
|
||||||
if (
|
if (stack.isEmpty()) return 0;
|
||||||
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
|
// V1 and V2 both implement IHasResistance
|
||||||
) return 0;
|
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) {
|
||||||
return bind.getCurrentResistance(stack, player);
|
return resistance.getCurrentResistance(stack, player);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -334,10 +336,11 @@ public class PlayerEquipment {
|
|||||||
player,
|
player,
|
||||||
BodyRegionV2.ARMS
|
BodyRegionV2.ARMS
|
||||||
);
|
);
|
||||||
if (
|
if (stack.isEmpty()) return;
|
||||||
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
|
// V1 and V2 both implement IHasResistance
|
||||||
) return;
|
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) {
|
||||||
bind.setCurrentResistance(stack, resistance);
|
resistanceItem.setCurrentResistance(stack, resistance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -348,10 +351,12 @@ public class PlayerEquipment {
|
|||||||
player,
|
player,
|
||||||
BodyRegionV2.NECK
|
BodyRegionV2.NECK
|
||||||
);
|
);
|
||||||
if (
|
if (stack.isEmpty()) return 0;
|
||||||
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
|
// V1 and V2 both implement IHasResistance
|
||||||
) return 0;
|
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) {
|
||||||
return collar.getCurrentResistance(stack, player);
|
return resistance.getCurrentResistance(stack, player);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -362,10 +367,11 @@ public class PlayerEquipment {
|
|||||||
player,
|
player,
|
||||||
BodyRegionV2.NECK
|
BodyRegionV2.NECK
|
||||||
);
|
);
|
||||||
if (
|
if (stack.isEmpty()) return;
|
||||||
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
|
// V1 and V2 both implement IHasResistance
|
||||||
) return;
|
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) {
|
||||||
collar.setCurrentResistance(stack, resistance);
|
resistanceItem.setCurrentResistance(stack, resistance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Helper Methods ==========
|
// ========== Helper Methods ==========
|
||||||
|
|||||||
@@ -4,12 +4,18 @@ import com.tiedup.remake.core.SettingsAccessor;
|
|||||||
import com.tiedup.remake.core.SystemMessageManager;
|
import com.tiedup.remake.core.SystemMessageManager;
|
||||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||||
import com.tiedup.remake.core.TiedUpMod;
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.items.base.IHasResistance;
|
||||||
import com.tiedup.remake.items.base.ILockable;
|
import com.tiedup.remake.items.base.ILockable;
|
||||||
import com.tiedup.remake.items.base.ItemBind;
|
import com.tiedup.remake.items.base.ItemBind;
|
||||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||||
import com.tiedup.remake.state.PlayerBindState;
|
import com.tiedup.remake.state.PlayerBindState;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.BindModeHelper;
|
||||||
|
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ResistanceComponent;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
|
||||||
import net.minecraft.world.entity.item.ItemEntity;
|
import net.minecraft.world.entity.item.ItemEntity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
@@ -75,16 +81,17 @@ public class StruggleBinds extends StruggleState {
|
|||||||
player,
|
player,
|
||||||
BodyRegionV2.ARMS
|
BodyRegionV2.ARMS
|
||||||
);
|
);
|
||||||
if (
|
if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) {
|
||||||
bindStack.isEmpty() ||
|
|
||||||
!(bindStack.getItem() instanceof ItemBind bind)
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The locked check has been moved to struggle() where decrease is reduced
|
// The locked check has been moved to struggle() where decrease is reduced
|
||||||
|
|
||||||
return bind.canBeStruggledOut(bindStack);
|
// Check canBeStruggledOut — works for both V1 and V2 via IHasResistance
|
||||||
|
if (bindStack.getItem() instanceof IHasResistance resistance) {
|
||||||
|
return resistance.canBeStruggledOut(bindStack);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,14 +110,13 @@ public class StruggleBinds extends StruggleState {
|
|||||||
player,
|
player,
|
||||||
BodyRegionV2.ARMS
|
BodyRegionV2.ARMS
|
||||||
);
|
);
|
||||||
if (
|
if (bindStack.isEmpty()) return false;
|
||||||
bindStack.isEmpty() ||
|
|
||||||
!(bindStack.getItem() instanceof ItemBind bind)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return bind.isLocked(bindStack);
|
// Works for both V1 (ItemBind) and V2 (DataDrivenBondageItem) via ILockable
|
||||||
|
if (bindStack.getItem() instanceof ILockable lockable) {
|
||||||
|
return lockable.isLocked(bindStack);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,14 +154,18 @@ public class StruggleBinds extends StruggleState {
|
|||||||
BodyRegionV2.NECK
|
BodyRegionV2.NECK
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!collar.isEmpty() && CollarHelper.canShock(collar)) {
|
||||||
!collar.isEmpty() &&
|
// V1 shock collar
|
||||||
collar.getItem() instanceof
|
if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) {
|
||||||
com.tiedup.remake.items.ItemShockCollar shockCollar
|
return shockCollar.notifyStruggle(player, collar);
|
||||||
) {
|
}
|
||||||
return shockCollar.notifyStruggle(player, collar);
|
// V2 shock collar — notify via IHasResistance if available
|
||||||
|
if (collar.getItem() instanceof IHasResistance resistance) {
|
||||||
|
resistance.notifyStruggle(player);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true; // No collar, proceed normally
|
return true; // No shock collar, proceed normally
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,18 +327,23 @@ public class StruggleBinds extends StruggleState {
|
|||||||
target,
|
target,
|
||||||
BodyRegionV2.ARMS
|
BodyRegionV2.ARMS
|
||||||
);
|
);
|
||||||
if (
|
if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) {
|
||||||
bindStack.isEmpty() ||
|
|
||||||
!(bindStack.getItem() instanceof ItemBind bind)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get base resistance from config (BUG-003 fix: was using ModGameRules which
|
// Get base resistance: V2 reads from ResistanceComponent directly,
|
||||||
// only knew 4 types and returned hardcoded 100 for the other 10)
|
// V1 reads from SettingsAccessor via item name (BUG-003 fix)
|
||||||
int baseResistance = SettingsAccessor.getBindResistance(
|
int baseResistance;
|
||||||
bind.getItemName()
|
ResistanceComponent comp = DataDrivenBondageItem.getComponent(
|
||||||
|
bindStack, ComponentType.RESISTANCE, ResistanceComponent.class
|
||||||
);
|
);
|
||||||
|
if (comp != null) {
|
||||||
|
baseResistance = comp.getBaseResistance();
|
||||||
|
} else if (bindStack.getItem() instanceof ItemBind bind) {
|
||||||
|
baseResistance = SettingsAccessor.getBindResistance(bind.getItemName());
|
||||||
|
} else {
|
||||||
|
baseResistance = 100;
|
||||||
|
}
|
||||||
|
|
||||||
// Set current resistance to base (full restore)
|
// Set current resistance to base (full restore)
|
||||||
setResistanceState(state, baseResistance);
|
setResistanceState(state, baseResistance);
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package com.tiedup.remake.state.struggle;
|
|||||||
|
|
||||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||||
import com.tiedup.remake.core.TiedUpMod;
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.items.base.IHasResistance;
|
||||||
|
import com.tiedup.remake.items.base.ILockable;
|
||||||
import com.tiedup.remake.items.base.ItemCollar;
|
import com.tiedup.remake.items.base.ItemCollar;
|
||||||
import com.tiedup.remake.state.PlayerBindState;
|
import com.tiedup.remake.state.PlayerBindState;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ResistanceComponent;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
@@ -44,19 +50,15 @@ public class StruggleCollar extends StruggleState {
|
|||||||
@Override
|
@Override
|
||||||
protected int getResistanceState(PlayerBindState state) {
|
protected int getResistanceState(PlayerBindState state) {
|
||||||
Player player = state.getPlayer();
|
Player player = state.getPlayer();
|
||||||
ItemStack collar = V2EquipmentHelper.getInRegion(
|
if (player == null) return 0;
|
||||||
player,
|
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||||
BodyRegionV2.NECK
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return 0;
|
||||||
collar.isEmpty() ||
|
|
||||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
if (collar.getItem() instanceof IHasResistance resistance) {
|
||||||
) {
|
return resistance.getCurrentResistance(collar, player);
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
return collarItem.getCurrentResistance(collar, player);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,19 +70,14 @@ public class StruggleCollar extends StruggleState {
|
|||||||
@Override
|
@Override
|
||||||
protected void setResistanceState(PlayerBindState state, int resistance) {
|
protected void setResistanceState(PlayerBindState state, int resistance) {
|
||||||
Player player = state.getPlayer();
|
Player player = state.getPlayer();
|
||||||
ItemStack collar = V2EquipmentHelper.getInRegion(
|
if (player == null) return;
|
||||||
player,
|
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||||
BodyRegionV2.NECK
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return;
|
||||||
collar.isEmpty() ||
|
|
||||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
if (collar.getItem() instanceof IHasResistance resistanceItem) {
|
||||||
) {
|
resistanceItem.setCurrentResistance(collar, resistance);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collarItem.setCurrentResistance(collar, resistance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,31 +101,29 @@ public class StruggleCollar extends StruggleState {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack collar = V2EquipmentHelper.getInRegion(
|
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||||
player,
|
|
||||||
BodyRegionV2.NECK
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
|
||||||
collar.isEmpty() ||
|
|
||||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
|
||||||
) {
|
|
||||||
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar equipped");
|
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar equipped");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if locked
|
// Check if locked (works for V1 and V2 via ILockable)
|
||||||
if (!collarItem.isLocked(collar)) {
|
if (collar.getItem() instanceof ILockable lockable) {
|
||||||
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked");
|
if (!lockable.isLocked(collar)) {
|
||||||
|
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if struggle is enabled
|
// Check if struggle is enabled (works for V1 and V2 via IHasResistance)
|
||||||
if (!collarItem.canBeStruggledOut(collar)) {
|
if (collar.getItem() instanceof IHasResistance resistance) {
|
||||||
TiedUpMod.LOGGER.debug(
|
if (!resistance.canBeStruggledOut(collar)) {
|
||||||
"[StruggleCollar] Collar struggle is disabled"
|
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar struggle is disabled");
|
||||||
);
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -141,17 +136,17 @@ public class StruggleCollar extends StruggleState {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean onAttempt(PlayerBindState state) {
|
protected boolean onAttempt(PlayerBindState state) {
|
||||||
Player player = state.getPlayer();
|
Player player = state.getPlayer();
|
||||||
ItemStack collar = V2EquipmentHelper.getInRegion(
|
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||||
player,
|
|
||||||
BodyRegionV2.NECK
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (!collar.isEmpty() && CollarHelper.canShock(collar)) {
|
||||||
!collar.isEmpty() &&
|
// V1 shock collar
|
||||||
collar.getItem() instanceof
|
if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) {
|
||||||
com.tiedup.remake.items.ItemShockCollar shockCollar
|
return shockCollar.notifyStruggle(player, collar);
|
||||||
) {
|
}
|
||||||
return shockCollar.notifyStruggle(player, collar);
|
// V2 shock collar — notify via IHasResistance
|
||||||
|
if (collar.getItem() instanceof IHasResistance resistance) {
|
||||||
|
resistance.notifyStruggle(player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -167,31 +162,19 @@ public class StruggleCollar extends StruggleState {
|
|||||||
@Override
|
@Override
|
||||||
protected void successAction(PlayerBindState state) {
|
protected void successAction(PlayerBindState state) {
|
||||||
Player player = state.getPlayer();
|
Player player = state.getPlayer();
|
||||||
ItemStack collar = V2EquipmentHelper.getInRegion(
|
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||||
player,
|
|
||||||
BodyRegionV2.NECK
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
|
||||||
collar.isEmpty() ||
|
TiedUpMod.LOGGER.warn("[StruggleCollar] successAction called but no collar equipped");
|
||||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
|
||||||
) {
|
|
||||||
TiedUpMod.LOGGER.warn(
|
|
||||||
"[StruggleCollar] successAction called but no collar equipped"
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlock the collar
|
// Unlock the collar (works for V1 and V2 via ILockable)
|
||||||
collarItem.setLocked(collar, false);
|
if (collar.getItem() instanceof ILockable lockable) {
|
||||||
|
lockable.setLocked(collar, false);
|
||||||
|
}
|
||||||
|
|
||||||
TiedUpMod.LOGGER.info(
|
TiedUpMod.LOGGER.info("[StruggleCollar] {} unlocked their collar!", player.getName().getString());
|
||||||
"[StruggleCollar] {} unlocked their collar!",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: Collar is NOT removed, just unlocked
|
|
||||||
// Player can now manually remove it
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -230,30 +213,36 @@ public class StruggleCollar extends StruggleState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemStack collar = V2EquipmentHelper.getInRegion(
|
ItemStack collar = V2EquipmentHelper.getInRegion(target, BodyRegionV2.NECK);
|
||||||
target,
|
|
||||||
BodyRegionV2.NECK
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
|
||||||
collar.isEmpty() ||
|
|
||||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
|
||||||
) {
|
|
||||||
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar to tighten");
|
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar to tighten");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if collar is locked
|
// Check if collar is locked (V1 and V2 via ILockable)
|
||||||
if (!collarItem.isLocked(collar)) {
|
if (collar.getItem() instanceof ILockable lockable) {
|
||||||
TiedUpMod.LOGGER.debug(
|
if (!lockable.isLocked(collar)) {
|
||||||
"[StruggleCollar] Collar must be locked to tighten"
|
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar must be locked to tighten");
|
||||||
);
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get base resistance from GameRules
|
// Get resistance — V2: read ResistanceComponent directly (avoids MAX-scan bug),
|
||||||
int baseResistance = collarItem.getBaseResistance(target);
|
// V1: use IHasResistance.getBaseResistance()
|
||||||
int currentResistance = collarItem.getCurrentResistance(collar, target);
|
if (!(collar.getItem() instanceof IHasResistance resistanceItem)) return;
|
||||||
|
int baseResistance;
|
||||||
|
ResistanceComponent comp = DataDrivenBondageItem.getComponent(
|
||||||
|
collar, ComponentType.RESISTANCE, ResistanceComponent.class
|
||||||
|
);
|
||||||
|
if (comp != null) {
|
||||||
|
baseResistance = comp.getBaseResistance();
|
||||||
|
} else {
|
||||||
|
baseResistance = resistanceItem.getBaseResistance(target);
|
||||||
|
}
|
||||||
|
int currentResistance = resistanceItem.getCurrentResistance(collar, target);
|
||||||
|
|
||||||
// Only tighten if current resistance is lower than base
|
// Only tighten if current resistance is lower than base
|
||||||
if (currentResistance >= baseResistance) {
|
if (currentResistance >= baseResistance) {
|
||||||
@@ -264,7 +253,7 @@ public class StruggleCollar extends StruggleState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore to base resistance
|
// Restore to base resistance
|
||||||
collarItem.setCurrentResistance(collar, baseResistance);
|
resistanceItem.setCurrentResistance(collar, baseResistance);
|
||||||
|
|
||||||
TiedUpMod.LOGGER.info(
|
TiedUpMod.LOGGER.info(
|
||||||
"[StruggleCollar] {} tightened {}'s collar (resistance {} -> {})",
|
"[StruggleCollar] {} tightened {}'s collar (resistance {} -> {})",
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.tiedup.remake.v2.bondage;
|
||||||
|
|
||||||
|
import com.tiedup.remake.items.base.ItemBind;
|
||||||
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static utilities for bind mode operations on any bondage item stack (V1 or V2).
|
||||||
|
*
|
||||||
|
* <p>Bind mode determines whether a bind restrains arms, legs, or both.
|
||||||
|
* The mode is stored in the stack's NBT tag {@code "bindMode"}.</p>
|
||||||
|
*/
|
||||||
|
public final class BindModeHelper {
|
||||||
|
|
||||||
|
private BindModeHelper() {}
|
||||||
|
|
||||||
|
private static final String NBT_BIND_MODE = "bindMode";
|
||||||
|
|
||||||
|
public static final String MODE_FULL = "full";
|
||||||
|
public static final String MODE_ARMS = "arms";
|
||||||
|
public static final String MODE_LEGS = "legs";
|
||||||
|
|
||||||
|
private static final String[] MODE_CYCLE = { MODE_FULL, MODE_ARMS, MODE_LEGS };
|
||||||
|
|
||||||
|
private static final Map<String, String> MODE_TRANSLATION_KEYS = Map.of(
|
||||||
|
MODE_FULL, "tiedup.bindmode.full",
|
||||||
|
MODE_ARMS, "tiedup.bindmode.arms",
|
||||||
|
MODE_LEGS, "tiedup.bindmode.legs"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given stack is a bind item (V2 ARMS-region item or V1 ItemBind).
|
||||||
|
*/
|
||||||
|
public static boolean isBindItem(ItemStack stack) {
|
||||||
|
if (stack.isEmpty()) return false;
|
||||||
|
// V2: check data-driven definition
|
||||||
|
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||||
|
if (def != null) {
|
||||||
|
return def.occupiedRegions().contains(BodyRegionV2.ARMS);
|
||||||
|
}
|
||||||
|
// V1 fallback
|
||||||
|
return stack.getItem() instanceof ItemBind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the bind mode ID from the stack's NBT.
|
||||||
|
* @return "full", "arms", or "legs" (defaults to "full")
|
||||||
|
*/
|
||||||
|
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_ARMS.equals(value) || MODE_LEGS.equals(value)) return value;
|
||||||
|
return MODE_FULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if arms are restrained (mode is "arms" or "full"). */
|
||||||
|
public static boolean hasArmsBound(ItemStack stack) {
|
||||||
|
String mode = getBindModeId(stack);
|
||||||
|
return MODE_ARMS.equals(mode) || MODE_FULL.equals(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if legs are restrained (mode is "legs" or "full"). */
|
||||||
|
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.
|
||||||
|
* @return the new mode ID
|
||||||
|
*/
|
||||||
|
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. */
|
||||||
|
public static String getBindModeTranslationKey(ItemStack stack) {
|
||||||
|
return MODE_TRANSLATION_KEYS.getOrDefault(getBindModeId(stack), "tiedup.bindmode.full");
|
||||||
|
}
|
||||||
|
}
|
||||||
371
src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java
Normal file
371
src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
package com.tiedup.remake.v2.bondage;
|
||||||
|
|
||||||
|
import com.tiedup.remake.items.ItemChokeCollar;
|
||||||
|
import com.tiedup.remake.items.ItemGpsCollar;
|
||||||
|
import com.tiedup.remake.items.ItemShockCollar;
|
||||||
|
import com.tiedup.remake.items.ItemShockCollarAuto;
|
||||||
|
import com.tiedup.remake.items.base.ItemCollar;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ChokingComponent;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.GpsComponent;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.OwnershipComponent;
|
||||||
|
import com.tiedup.remake.v2.bondage.component.ShockComponent;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static utility for collar operations bridging V1 (ItemCollar subclasses)
|
||||||
|
* and V2 (data-driven items with OwnershipComponent).
|
||||||
|
*/
|
||||||
|
public final class CollarHelper {
|
||||||
|
|
||||||
|
private CollarHelper() {}
|
||||||
|
|
||||||
|
// ===== DETECTION =====
|
||||||
|
|
||||||
|
// True if the stack is any kind of collar (V2 ownership component or V1 ItemCollar)
|
||||||
|
public static boolean isCollar(ItemStack stack) {
|
||||||
|
if (stack.isEmpty()) return false;
|
||||||
|
if (DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return stack.getItem() instanceof ItemCollar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== OWNERSHIP (NBT: "owners") =====
|
||||||
|
|
||||||
|
// Returns all owner UUIDs stored in the collar's "owners" ListTag
|
||||||
|
public static List<UUID> getOwners(ItemStack stack) {
|
||||||
|
return getListUUIDs(stack, "owners");
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if the given UUID is in the owners list
|
||||||
|
public static boolean isOwner(ItemStack stack, UUID uuid) {
|
||||||
|
return hasUUIDInList(stack, "owners", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if the given player is an owner
|
||||||
|
public static boolean isOwner(ItemStack stack, Player player) {
|
||||||
|
return isOwner(stack, player.getUUID());
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if the collar has at least one owner
|
||||||
|
public static boolean hasOwner(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains("owners", Tag.TAG_LIST)) return false;
|
||||||
|
return !tag.getList("owners", Tag.TAG_COMPOUND).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds an owner entry {uuid, name} to the "owners" ListTag
|
||||||
|
public static void addOwner(ItemStack stack, UUID uuid, String name) {
|
||||||
|
addToList(stack, "owners", uuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience: add a player as owner
|
||||||
|
public static void addOwner(ItemStack stack, Player player) {
|
||||||
|
addOwner(stack, player.getUUID(), player.getGameProfile().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes an owner by UUID
|
||||||
|
public static void removeOwner(ItemStack stack, UUID uuid) {
|
||||||
|
removeFromList(stack, "owners", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== BLACKLIST (NBT: "blacklist") =====
|
||||||
|
|
||||||
|
public static List<UUID> getBlacklist(ItemStack stack) {
|
||||||
|
return getListUUIDs(stack, "blacklist");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBlacklisted(ItemStack stack, UUID uuid) {
|
||||||
|
return hasUUIDInList(stack, "blacklist", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addToBlacklist(ItemStack stack, UUID uuid, String name) {
|
||||||
|
addToList(stack, "blacklist", uuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeFromBlacklist(ItemStack stack, UUID uuid) {
|
||||||
|
removeFromList(stack, "blacklist", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== WHITELIST (NBT: "whitelist") =====
|
||||||
|
|
||||||
|
public static List<UUID> getWhitelist(ItemStack stack) {
|
||||||
|
return getListUUIDs(stack, "whitelist");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isWhitelisted(ItemStack stack, UUID uuid) {
|
||||||
|
return hasUUIDInList(stack, "whitelist", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addToWhitelist(ItemStack stack, UUID uuid, String name) {
|
||||||
|
addToList(stack, "whitelist", uuid, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeFromWhitelist(ItemStack stack, UUID uuid) {
|
||||||
|
removeFromList(stack, "whitelist", uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== LIST INTERNALS =====
|
||||||
|
|
||||||
|
private static List<UUID> getListUUIDs(ItemStack stack, String listKey) {
|
||||||
|
List<UUID> result = new ArrayList<>();
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return result;
|
||||||
|
ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND);
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
CompoundTag entry = list.getCompound(i);
|
||||||
|
if (entry.contains("uuid")) {
|
||||||
|
try {
|
||||||
|
result.add(UUID.fromString(entry.getString("uuid")));
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
// Malformed UUID in NBT, skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasUUIDInList(ItemStack stack, String listKey, UUID uuid) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return false;
|
||||||
|
String uuidStr = uuid.toString();
|
||||||
|
ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND);
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
if (uuidStr.equals(list.getCompound(i).getString("uuid"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addToList(ItemStack stack, String listKey, UUID uuid, String name) {
|
||||||
|
CompoundTag tag = stack.getOrCreateTag();
|
||||||
|
ListTag list = tag.contains(listKey, Tag.TAG_LIST)
|
||||||
|
? tag.getList(listKey, Tag.TAG_COMPOUND)
|
||||||
|
: new ListTag();
|
||||||
|
// Prevent duplicates
|
||||||
|
String uuidStr = uuid.toString();
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
if (uuidStr.equals(list.getCompound(i).getString("uuid"))) return;
|
||||||
|
}
|
||||||
|
CompoundTag entry = new CompoundTag();
|
||||||
|
entry.putString("uuid", uuidStr);
|
||||||
|
entry.putString("name", name);
|
||||||
|
list.add(entry);
|
||||||
|
tag.put(listKey, list);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeFromList(ItemStack stack, String listKey, UUID uuid) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return;
|
||||||
|
String uuidStr = uuid.toString();
|
||||||
|
ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND);
|
||||||
|
list.removeIf(element ->
|
||||||
|
uuidStr.equals(((CompoundTag) element).getString("uuid"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== FEATURES =====
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String getNickname(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains("nickname")) return null;
|
||||||
|
return tag.getString("nickname");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setNickname(ItemStack stack, String nickname) {
|
||||||
|
stack.getOrCreateTag().putString("nickname", nickname);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasNickname(ItemStack stack) {
|
||||||
|
return stack.hasTag() && stack.getTag().contains("nickname");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static UUID getCellId(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains("cellId")) return null;
|
||||||
|
try {
|
||||||
|
return UUID.fromString(tag.getString("cellId"));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setCellId(ItemStack stack, UUID cellId) {
|
||||||
|
stack.getOrCreateTag().putString("cellId", cellId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasCellAssigned(ItemStack stack) {
|
||||||
|
return getCellId(stack) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isKidnappingModeEnabled(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
return tag != null && tag.getBoolean("kidnappingMode");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setKidnappingModeEnabled(ItemStack stack, boolean enabled) {
|
||||||
|
stack.getOrCreateTag().putBoolean("kidnappingMode", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kidnapping mode is ready when enabled AND a cell is assigned
|
||||||
|
public static boolean isKidnappingModeReady(ItemStack stack) {
|
||||||
|
return isKidnappingModeEnabled(stack) && hasCellAssigned(stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldTieToPole(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
return tag != null && tag.getBoolean("tieToPole");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setShouldTieToPole(ItemStack stack, boolean value) {
|
||||||
|
stack.getOrCreateTag().putBoolean("tieToPole", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default true when tag is absent or key is missing
|
||||||
|
public static boolean shouldWarnMasters(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains("warnMasters")) return true;
|
||||||
|
return tag.getBoolean("warnMasters");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setShouldWarnMasters(ItemStack stack, boolean value) {
|
||||||
|
stack.getOrCreateTag().putBoolean("warnMasters", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBondageServiceEnabled(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
return tag != null && tag.getBoolean("bondageservice");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setBondageServiceEnabled(ItemStack stack, boolean enabled) {
|
||||||
|
stack.getOrCreateTag().putBoolean("bondageservice", enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static String getServiceSentence(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains("servicesentence")) return null;
|
||||||
|
return tag.getString("servicesentence");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setServiceSentence(ItemStack stack, String sentence) {
|
||||||
|
stack.getOrCreateTag().putString("servicesentence", sentence);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== SHOCK =====
|
||||||
|
|
||||||
|
// True if the collar can shock (V2 ShockComponent or V1 ItemShockCollar)
|
||||||
|
public static boolean canShock(ItemStack stack) {
|
||||||
|
if (stack.isEmpty()) return false;
|
||||||
|
if (DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return stack.getItem() instanceof ItemShockCollar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPublicShock(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
return tag != null && tag.getBoolean("public_mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPublicShock(ItemStack stack, boolean publicMode) {
|
||||||
|
stack.getOrCreateTag().putBoolean("public_mode", publicMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// V2: from ShockComponent auto interval, V1: from ItemShockCollarAuto field, else 0
|
||||||
|
public static int getShockInterval(ItemStack stack) {
|
||||||
|
ShockComponent comp = DataDrivenBondageItem.getComponent(
|
||||||
|
stack, ComponentType.SHOCK, ShockComponent.class
|
||||||
|
);
|
||||||
|
if (comp != null) return comp.getAutoInterval();
|
||||||
|
if (stack.getItem() instanceof ItemShockCollarAuto auto) {
|
||||||
|
return auto.getInterval();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== GPS =====
|
||||||
|
|
||||||
|
// True if the collar has GPS capabilities
|
||||||
|
public static boolean hasGPS(ItemStack stack) {
|
||||||
|
if (stack.isEmpty()) return false;
|
||||||
|
if (DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return stack.getItem() instanceof ItemGpsCollar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasPublicTracking(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
return tag != null && tag.getBoolean("publicTracking");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPublicTracking(ItemStack stack, boolean publicTracking) {
|
||||||
|
stack.getOrCreateTag().putBoolean("publicTracking", publicTracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPS active defaults to true when absent
|
||||||
|
public static boolean isActive(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
if (tag == null || !tag.contains("gpsActive")) return true;
|
||||||
|
return tag.getBoolean("gpsActive");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setActive(ItemStack stack, boolean active) {
|
||||||
|
stack.getOrCreateTag().putBoolean("gpsActive", active);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== CHOKE =====
|
||||||
|
|
||||||
|
// True if the collar is a choke collar
|
||||||
|
public static boolean isChokeCollar(ItemStack stack) {
|
||||||
|
if (stack.isEmpty()) return false;
|
||||||
|
if (DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return stack.getItem() instanceof ItemChokeCollar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isChoking(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
return tag != null && tag.getBoolean("choking");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setChoking(ItemStack stack, boolean choking) {
|
||||||
|
stack.getOrCreateTag().putBoolean("choking", choking);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPetPlayMode(ItemStack stack) {
|
||||||
|
CompoundTag tag = stack.getTag();
|
||||||
|
return tag != null && tag.getBoolean("petPlayMode");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPetPlayMode(ItemStack stack, boolean petPlay) {
|
||||||
|
stack.getOrCreateTag().putBoolean("petPlayMode", petPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ALERT SUPPRESSION =====
|
||||||
|
|
||||||
|
// Executes the action with collar removal alerts suppressed
|
||||||
|
public static void runWithSuppressedAlert(Runnable action) {
|
||||||
|
ItemCollar.runWithSuppressedAlert(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// True if removal alerts are currently suppressed (ThreadLocal state)
|
||||||
|
public static boolean isRemovalAlertSuppressed() {
|
||||||
|
return ItemCollar.isRemovalAlertSuppressed();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.tiedup.remake.v2.bondage;
|
||||||
|
|
||||||
|
import com.tiedup.remake.items.base.ItemBind;
|
||||||
|
import com.tiedup.remake.items.base.PoseType;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the {@link PoseType} for any bondage item stack (V1 or V2).
|
||||||
|
*
|
||||||
|
* <p>V2 items read from the data-driven definition's {@code pose_type} field.
|
||||||
|
* V1 items fall back to {@code ItemBind.getPoseType()}.</p>
|
||||||
|
*/
|
||||||
|
public final class PoseTypeHelper {
|
||||||
|
|
||||||
|
private PoseTypeHelper() {}
|
||||||
|
|
||||||
|
public static PoseType getPoseType(ItemStack stack) {
|
||||||
|
// V2: read from data-driven definition
|
||||||
|
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||||
|
if (def != null && def.poseType() != null) {
|
||||||
|
try {
|
||||||
|
return PoseType.valueOf(def.poseType().toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return PoseType.STANDARD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// V1 fallback
|
||||||
|
if (stack.getItem() instanceof ItemBind bind) {
|
||||||
|
return bind.getPoseType();
|
||||||
|
}
|
||||||
|
return PoseType.STANDARD;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.tiedup.remake.v2.bondage;
|
||||||
|
|
||||||
|
import com.tiedup.remake.core.SettingsAccessor;
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.state.IBondageState;
|
||||||
|
import com.tiedup.remake.state.PlayerBindState;
|
||||||
|
import com.tiedup.remake.tasks.TyingTask;
|
||||||
|
import com.tiedup.remake.tasks.V2TyingPlayerTask;
|
||||||
|
import com.tiedup.remake.util.KidnappedHelper;
|
||||||
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||||
|
import com.tiedup.remake.v2.bondage.V2EquipResult;
|
||||||
|
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.ItemStack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the tying interaction flow for V2 data-driven ARMS items.
|
||||||
|
*
|
||||||
|
* <p>Extracted from {@code ItemBind.interactLivingEntity()} to support
|
||||||
|
* the same tying task flow for data-driven items.</p>
|
||||||
|
*/
|
||||||
|
public final class TyingInteractionHelper {
|
||||||
|
|
||||||
|
private TyingInteractionHelper() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle right-click tying of a target entity with a V2 ARMS item.
|
||||||
|
* Creates/continues a V2TyingPlayerTask.
|
||||||
|
*/
|
||||||
|
public static InteractionResult handleTying(
|
||||||
|
ServerPlayer player,
|
||||||
|
LivingEntity target,
|
||||||
|
ItemStack stack,
|
||||||
|
InteractionHand hand
|
||||||
|
) {
|
||||||
|
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
|
||||||
|
if (targetState == null) return InteractionResult.PASS;
|
||||||
|
|
||||||
|
// Kidnapper can't be tied themselves
|
||||||
|
IBondageState kidnapperState = KidnappedHelper.getKidnappedState(player);
|
||||||
|
if (kidnapperState != null && kidnapperState.isTiedUp()) {
|
||||||
|
return InteractionResult.PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already tied — try to swap (check both V1 state and V2 region to prevent dual-bind)
|
||||||
|
if (targetState.isTiedUp() || V2EquipmentHelper.isRegionOccupied(target, BodyRegionV2.ARMS)) {
|
||||||
|
if (stack.isEmpty()) return InteractionResult.PASS;
|
||||||
|
ItemStack oldBind = V2EquipmentHelper.unequipFromRegion(target, BodyRegionV2.ARMS);
|
||||||
|
if (!oldBind.isEmpty()) {
|
||||||
|
V2EquipResult result = V2EquipmentHelper.equipItem(target, stack.copy());
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
stack.shrink(1);
|
||||||
|
target.spawnAtLocation(oldBind);
|
||||||
|
TiedUpMod.LOGGER.debug("[TyingInteraction] Swapped bind on {}", target.getName().getString());
|
||||||
|
return InteractionResult.SUCCESS;
|
||||||
|
} else {
|
||||||
|
// Equip failed — rollback: re-equip old bind
|
||||||
|
V2EquipResult rollback = V2EquipmentHelper.equipItem(target, oldBind);
|
||||||
|
if (!rollback.isSuccess()) {
|
||||||
|
// Rollback also failed — drop old bind as safety net
|
||||||
|
target.spawnAtLocation(oldBind);
|
||||||
|
TiedUpMod.LOGGER.warn("[TyingInteraction] Swap AND rollback failed, dropped old bind for {}", target.getName().getString());
|
||||||
|
} else {
|
||||||
|
TiedUpMod.LOGGER.debug("[TyingInteraction] Swap failed, rolled back old bind on {}", target.getName().getString());
|
||||||
|
}
|
||||||
|
return InteractionResult.PASS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return InteractionResult.PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance + line-of-sight (skip for self-tying)
|
||||||
|
boolean isSelfTying = player.equals(target);
|
||||||
|
if (!isSelfTying) {
|
||||||
|
if (player.distanceTo(target) > 4.0 || !player.hasLineOfSight(target)) {
|
||||||
|
return InteractionResult.PASS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create/continue tying task
|
||||||
|
PlayerBindState playerState = PlayerBindState.getInstance(player);
|
||||||
|
if (playerState == null) return InteractionResult.PASS;
|
||||||
|
|
||||||
|
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules());
|
||||||
|
|
||||||
|
V2TyingPlayerTask newTask = new V2TyingPlayerTask(
|
||||||
|
stack.copy(),
|
||||||
|
stack,
|
||||||
|
targetState,
|
||||||
|
target,
|
||||||
|
tyingSeconds,
|
||||||
|
player.level(),
|
||||||
|
player
|
||||||
|
);
|
||||||
|
|
||||||
|
TyingTask currentTask = playerState.getCurrentTyingTask();
|
||||||
|
if (currentTask == null
|
||||||
|
|| !(currentTask instanceof V2TyingPlayerTask)
|
||||||
|
|| !currentTask.isSameTarget(target)
|
||||||
|
|| currentTask.isOutdated()
|
||||||
|
|| !ItemStack.matches(currentTask.getBind(), stack)) {
|
||||||
|
// Start new task (also handles case where existing task is V1 TyingPlayerTask)
|
||||||
|
playerState.setCurrentTyingTask(newTask);
|
||||||
|
newTask.start();
|
||||||
|
} else {
|
||||||
|
newTask = (V2TyingPlayerTask) currentTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
newTask.update();
|
||||||
|
|
||||||
|
if (newTask.isStopped()) {
|
||||||
|
// Item already consumed by V2TyingPlayerTask.onComplete() — don't shrink again
|
||||||
|
playerState.setCurrentTyingTask(null);
|
||||||
|
TiedUpMod.LOGGER.info("[TyingInteraction] {} tied {}", player.getName().getString(), target.getName().getString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return InteractionResult.SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.tiedup.remake.v2.bondage.component;
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component: choking effect for data-driven items.
|
* Component: choking effect for data-driven items.
|
||||||
@@ -43,4 +50,9 @@ public class ChokingComponent implements IItemComponent {
|
|||||||
public boolean isNonLethalForMaster() {
|
public boolean isNonLethalForMaster() {
|
||||||
return nonLethalForMaster;
|
return nonLethalForMaster;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.choking").withStyle(ChatFormatting.DARK_PURPLE));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ package com.tiedup.remake.v2.bondage.component;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public final class ComponentHolder {
|
public final class ComponentHolder {
|
||||||
@@ -58,6 +62,12 @@ public final class ComponentHolder {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
for (IItemComponent c : components.values()) {
|
||||||
|
c.appendTooltip(stack, level, tooltip, flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return components.isEmpty();
|
return components.isEmpty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ public enum ComponentType {
|
|||||||
SHOCK("shock", ShockComponent::fromJson),
|
SHOCK("shock", ShockComponent::fromJson),
|
||||||
GPS("gps", GpsComponent::fromJson),
|
GPS("gps", GpsComponent::fromJson),
|
||||||
CHOKING("choking", ChokingComponent::fromJson),
|
CHOKING("choking", ChokingComponent::fromJson),
|
||||||
ADJUSTABLE("adjustable", AdjustableComponent::fromJson);
|
ADJUSTABLE("adjustable", AdjustableComponent::fromJson),
|
||||||
|
OWNERSHIP("ownership", OwnershipComponent::fromJson);
|
||||||
|
|
||||||
private final String jsonKey;
|
private final String jsonKey;
|
||||||
private final Function<JsonObject, IItemComponent> factory;
|
private final Function<JsonObject, IItemComponent> factory;
|
||||||
|
|||||||
@@ -1,45 +1,100 @@
|
|||||||
package com.tiedup.remake.v2.bondage.component;
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.util.GagMaterial;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component: gagging behavior for data-driven items.
|
* Component: gagging behavior for data-driven items.
|
||||||
*
|
*
|
||||||
* JSON config: {@code "gagging": {"comprehension": 0.2, "range": 10.0}}
|
* <p>Config-driven: {@code "gagging": {"material": "ball"}} delegates to
|
||||||
|
* {@link GagMaterial} for comprehension/range from ModConfig at runtime.</p>
|
||||||
|
*
|
||||||
|
* <p>Override: {@code "gagging": {"comprehension": 0.15, "range": 8.0}} uses
|
||||||
|
* explicit values that take priority over the material lookup.</p>
|
||||||
*/
|
*/
|
||||||
public class GaggingComponent implements IItemComponent {
|
public class GaggingComponent implements IItemComponent {
|
||||||
|
|
||||||
private final double comprehension;
|
private final @Nullable String material;
|
||||||
private final double range;
|
private final @Nullable GagMaterial cachedMaterial;
|
||||||
|
private final double comprehensionOverride;
|
||||||
|
private final double rangeOverride;
|
||||||
|
|
||||||
private GaggingComponent(double comprehension, double range) {
|
private GaggingComponent(@Nullable String material, @Nullable GagMaterial cachedMaterial,
|
||||||
this.comprehension = comprehension;
|
double comprehensionOverride, double rangeOverride) {
|
||||||
this.range = range;
|
this.material = material;
|
||||||
|
this.cachedMaterial = cachedMaterial;
|
||||||
|
this.comprehensionOverride = comprehensionOverride;
|
||||||
|
this.rangeOverride = rangeOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IItemComponent fromJson(JsonObject config) {
|
public static IItemComponent fromJson(JsonObject config) {
|
||||||
double comprehension = 0.2;
|
String material = null;
|
||||||
double range = 10.0;
|
double comprehension = -1;
|
||||||
|
double range = -1;
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
|
if (config.has("material")) {
|
||||||
|
material = config.get("material").getAsString();
|
||||||
|
}
|
||||||
if (config.has("comprehension")) {
|
if (config.has("comprehension")) {
|
||||||
comprehension = config.get("comprehension").getAsDouble();
|
comprehension = Math.max(0.0, Math.min(1.0, config.get("comprehension").getAsDouble()));
|
||||||
}
|
}
|
||||||
if (config.has("range")) {
|
if (config.has("range")) {
|
||||||
range = config.get("range").getAsDouble();
|
range = Math.max(0.0, config.get("range").getAsDouble());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
comprehension = Math.max(0.0, Math.min(1.0, comprehension));
|
// Resolve and cache GagMaterial at load time to avoid valueOf() on every chat message
|
||||||
range = Math.max(0.0, range);
|
GagMaterial resolved = null;
|
||||||
return new GaggingComponent(comprehension, range);
|
if (material != null) {
|
||||||
|
try {
|
||||||
|
resolved = GagMaterial.valueOf(material.toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
TiedUpMod.LOGGER.warn("[GaggingComponent] Unknown gag material '{}' — using defaults", material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new GaggingComponent(material, resolved, comprehension, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full). */
|
/** How much of the gagged speech is comprehensible (0.0 = nothing, 1.0 = full). */
|
||||||
public double getComprehension() {
|
public double getComprehension() {
|
||||||
return comprehension;
|
if (comprehensionOverride >= 0) return comprehensionOverride;
|
||||||
|
GagMaterial gag = getMaterial();
|
||||||
|
if (gag != null) return gag.getComprehension();
|
||||||
|
return 0.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maximum range in blocks where muffled speech can be heard. */
|
/** Maximum range in blocks where muffled speech can be heard. */
|
||||||
public double getRange() {
|
public double getRange() {
|
||||||
return range;
|
if (rangeOverride >= 0) return rangeOverride;
|
||||||
|
GagMaterial gag = getMaterial();
|
||||||
|
if (gag != null) return gag.getTalkRange();
|
||||||
|
return 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The gag material enum, or null if not configured or invalid. Cached at load time. */
|
||||||
|
public @Nullable GagMaterial getMaterial() {
|
||||||
|
return cachedMaterial;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The raw material string from JSON, or null. */
|
||||||
|
public @Nullable String getMaterialName() {
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
if (material != null) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.gag_material", material)
|
||||||
|
.withStyle(ChatFormatting.RED));
|
||||||
|
} else {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.gagging").withStyle(ChatFormatting.RED));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.tiedup.remake.v2.bondage.component;
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component: GPS tracking and safe zone for data-driven items.
|
* Component: GPS tracking and safe zone for data-driven items.
|
||||||
@@ -42,4 +49,14 @@ public class GpsComponent implements IItemComponent {
|
|||||||
public boolean isPublicTracking() {
|
public boolean isPublicTracking() {
|
||||||
return publicTracking;
|
return publicTracking;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.gps_tracking")
|
||||||
|
.withStyle(ChatFormatting.AQUA));
|
||||||
|
if (safeZoneRadius > 0) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.gps_zone_radius", safeZoneRadius)
|
||||||
|
.withStyle(ChatFormatting.DARK_AQUA));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package com.tiedup.remake.v2.bondage.component;
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable behavior module for data-driven bondage items.
|
* A reusable behavior module for data-driven bondage items.
|
||||||
@@ -16,4 +21,6 @@ public interface IItemComponent {
|
|||||||
default boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
|
default boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
default void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.tiedup.remake.v2.bondage.component;
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component: lockable behavior for data-driven items.
|
* Component: lockable behavior for data-driven items.
|
||||||
@@ -41,4 +48,13 @@ public class LockableComponent implements IItemComponent {
|
|||||||
public int getLockResistance() {
|
public int getLockResistance() {
|
||||||
return lockResistance;
|
return lockResistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.lockable").withStyle(ChatFormatting.GOLD));
|
||||||
|
if (flag.isAdvanced()) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.lock_resistance", lockResistance)
|
||||||
|
.withStyle(ChatFormatting.DARK_GRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.items.base.ItemCollar;
|
||||||
|
import com.tiedup.remake.state.CollarRegistry;
|
||||||
|
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: collar ownership behavior for data-driven items.
|
||||||
|
*
|
||||||
|
* <p>Marks an item as a collar with ownership capabilities.
|
||||||
|
* Lifecycle hooks handle CollarRegistry registration/unregistration.</p>
|
||||||
|
*
|
||||||
|
* <p>JSON config: {@code "ownership": {}}</p>
|
||||||
|
*/
|
||||||
|
public class OwnershipComponent implements IItemComponent {
|
||||||
|
|
||||||
|
private OwnershipComponent() {}
|
||||||
|
|
||||||
|
public static IItemComponent fromJson(JsonObject config) {
|
||||||
|
return new OwnershipComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEquipped(ItemStack stack, LivingEntity entity) {
|
||||||
|
if (entity.level().isClientSide()) return;
|
||||||
|
if (!(entity.level() instanceof ServerLevel serverLevel)) return;
|
||||||
|
|
||||||
|
List<UUID> owners = CollarHelper.getOwners(stack);
|
||||||
|
if (owners.isEmpty()) {
|
||||||
|
TiedUpMod.LOGGER.debug(
|
||||||
|
"[OwnershipComponent] Collar equipped on {} with no owners in NBT — skipping registry",
|
||||||
|
entity.getName().getString()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
CollarRegistry registry = CollarRegistry.get(serverLevel);
|
||||||
|
registry.registerCollar(entity.getUUID(), new HashSet<>(owners));
|
||||||
|
TiedUpMod.LOGGER.debug(
|
||||||
|
"[OwnershipComponent] Registered collar for {} with {} owner(s)",
|
||||||
|
entity.getName().getString(), owners.size()
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
TiedUpMod.LOGGER.warn(
|
||||||
|
"[OwnershipComponent] Failed to register collar for {}: {}",
|
||||||
|
entity.getName().getString(), e.getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUnequipped(ItemStack stack, LivingEntity entity) {
|
||||||
|
if (entity.level().isClientSide()) return;
|
||||||
|
if (!(entity.level() instanceof ServerLevel serverLevel)) return;
|
||||||
|
|
||||||
|
// Alert kidnappers + unregister from CollarRegistry
|
||||||
|
// onCollarRemoved handles both the alert AND the unregister call internally,
|
||||||
|
// so we do NOT call registry.unregisterWearer() separately to avoid double unregister.
|
||||||
|
if (!CollarHelper.isRemovalAlertSuppressed()) {
|
||||||
|
ItemCollar.onCollarRemoved(entity, true);
|
||||||
|
} else {
|
||||||
|
// Suppressed alert path: still need to unregister, just skip the alert
|
||||||
|
try {
|
||||||
|
CollarRegistry registry = CollarRegistry.get(serverLevel);
|
||||||
|
registry.unregisterWearer(entity.getUUID());
|
||||||
|
TiedUpMod.LOGGER.debug(
|
||||||
|
"[OwnershipComponent] Unregistered collar for {} (alert suppressed)",
|
||||||
|
entity.getName().getString()
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
TiedUpMod.LOGGER.warn(
|
||||||
|
"[OwnershipComponent] Failed to unregister collar for {}: {}",
|
||||||
|
entity.getName().getString(), e.getMessage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
String nickname = CollarHelper.getNickname(stack);
|
||||||
|
if (nickname != null && !nickname.isEmpty()) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.nickname", nickname)
|
||||||
|
.withStyle(ChatFormatting.LIGHT_PURPLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UUID> owners = CollarHelper.getOwners(stack);
|
||||||
|
if (!owners.isEmpty()) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.owners", owners.size())
|
||||||
|
.withStyle(ChatFormatting.GOLD));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollarHelper.canShock(stack)) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.shock_capable")
|
||||||
|
.withStyle(ChatFormatting.DARK_RED));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollarHelper.hasGPS(stack)) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.gps_capable")
|
||||||
|
.withStyle(ChatFormatting.AQUA));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CollarHelper.isChokeCollar(stack)) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.choke_capable")
|
||||||
|
.withStyle(ChatFormatting.DARK_PURPLE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,70 @@
|
|||||||
package com.tiedup.remake.v2.bondage.component;
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import com.tiedup.remake.core.SettingsAccessor;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component: struggle resistance for data-driven items.
|
* Component: struggle resistance for data-driven items.
|
||||||
*
|
*
|
||||||
* JSON config: {@code "resistance": {"base": 150}}
|
* <p>Config-driven: {@code "resistance": {"id": "rope"}} delegates to
|
||||||
|
* {@link SettingsAccessor#getBindResistance(String)} at runtime.</p>
|
||||||
|
*
|
||||||
|
* <p>Legacy/override: {@code "resistance": {"base": 150}} uses a hardcoded value.</p>
|
||||||
*/
|
*/
|
||||||
public class ResistanceComponent implements IItemComponent {
|
public class ResistanceComponent implements IItemComponent {
|
||||||
|
|
||||||
private final int baseResistance;
|
private final @Nullable String resistanceId;
|
||||||
|
private final int fallbackBase;
|
||||||
|
|
||||||
private ResistanceComponent(int baseResistance) {
|
private ResistanceComponent(@Nullable String resistanceId, int fallbackBase) {
|
||||||
this.baseResistance = baseResistance;
|
this.resistanceId = resistanceId;
|
||||||
|
this.fallbackBase = fallbackBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IItemComponent fromJson(JsonObject config) {
|
public static IItemComponent fromJson(JsonObject config) {
|
||||||
|
String id = null;
|
||||||
int base = 100;
|
int base = 100;
|
||||||
if (config != null && config.has("base")) {
|
if (config != null) {
|
||||||
base = config.get("base").getAsInt();
|
if (config.has("id")) {
|
||||||
|
id = config.get("id").getAsString();
|
||||||
|
}
|
||||||
|
if (config.has("base")) {
|
||||||
|
base = config.get("base").getAsInt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
base = Math.max(0, base);
|
base = Math.max(0, base);
|
||||||
return new ResistanceComponent(base);
|
return new ResistanceComponent(id, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the base resistance for this item.
|
* Get the base resistance for this item.
|
||||||
|
* If a {@code resistanceId} is configured, delegates to server config at runtime.
|
||||||
|
* Otherwise returns the hardcoded fallback value.
|
||||||
*/
|
*/
|
||||||
public int getBaseResistance() {
|
public int getBaseResistance() {
|
||||||
return baseResistance;
|
if (resistanceId != null) {
|
||||||
|
return SettingsAccessor.getBindResistance(resistanceId);
|
||||||
|
}
|
||||||
|
return fallbackBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The config key used for runtime resistance lookup, or null if hardcoded. */
|
||||||
|
public @Nullable String getResistanceId() {
|
||||||
|
return resistanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
if (flag.isAdvanced()) {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.resistance", getBaseResistance())
|
||||||
|
.withStyle(ChatFormatting.DARK_GRAY));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
package com.tiedup.remake.v2.bondage.component;
|
package com.tiedup.remake.v2.bondage.component;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component: shock collar behavior for data-driven items.
|
* Component: shock collar behavior for data-driven items.
|
||||||
@@ -49,4 +56,16 @@ public class ShockComponent implements IItemComponent {
|
|||||||
public boolean hasAutoShock() {
|
public boolean hasAutoShock() {
|
||||||
return autoInterval > 0;
|
return autoInterval > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||||
|
if (hasAutoShock()) {
|
||||||
|
float seconds = autoInterval / 20.0f;
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.shock_auto", String.format("%.1f", seconds))
|
||||||
|
.withStyle(ChatFormatting.DARK_RED));
|
||||||
|
} else {
|
||||||
|
tooltip.add(Component.translatable("item.tiedup.tooltip.shock_manual")
|
||||||
|
.withStyle(ChatFormatting.DARK_RED));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.tiedup.remake.v2.bondage.datadriven;
|
package com.tiedup.remake.v2.bondage.datadriven;
|
||||||
|
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.BindModeHelper;
|
||||||
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
||||||
|
import com.tiedup.remake.v2.bondage.TyingInteractionHelper;
|
||||||
import com.tiedup.remake.v2.bondage.V2BondageItems;
|
import com.tiedup.remake.v2.bondage.V2BondageItems;
|
||||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||||
import com.tiedup.remake.v2.bondage.component.ComponentHolder;
|
import com.tiedup.remake.v2.bondage.component.ComponentHolder;
|
||||||
@@ -17,7 +20,13 @@ import java.util.stream.Collectors;
|
|||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
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.LivingEntity;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.TooltipFlag;
|
import net.minecraft.world.item.TooltipFlag;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
@@ -116,6 +125,60 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
|||||||
return def != null && def.supportsColor();
|
return def != null && def.supportsColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== INTERACTION ROUTING =====
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
|
||||||
|
ItemStack stack = player.getItemInHand(hand);
|
||||||
|
Set<BodyRegionV2> regions = getOccupiedRegions(stack);
|
||||||
|
|
||||||
|
// ARMS items: shift+click cycles bind mode
|
||||||
|
if (regions.contains(BodyRegionV2.ARMS) && player.isShiftKeyDown() && !level.isClientSide) {
|
||||||
|
BindModeHelper.cycleBindModeId(stack);
|
||||||
|
player.playSound(SoundEvents.CHAIN_STEP, 0.5f, 1.2f);
|
||||||
|
player.displayClientMessage(
|
||||||
|
Component.translatable(
|
||||||
|
"tiedup.message.bindmode_changed",
|
||||||
|
Component.translatable(BindModeHelper.getBindModeTranslationKey(stack))
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return InteractionResultHolder.success(stack);
|
||||||
|
}
|
||||||
|
return super.use(level, player, hand);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InteractionResult interactLivingEntity(
|
||||||
|
ItemStack stack, Player player, LivingEntity target, InteractionHand hand
|
||||||
|
) {
|
||||||
|
// Client: arm swing
|
||||||
|
if (player.level().isClientSide) {
|
||||||
|
return InteractionResult.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||||
|
if (def == null) return InteractionResult.PASS;
|
||||||
|
|
||||||
|
Set<BodyRegionV2> regions = def.occupiedRegions();
|
||||||
|
|
||||||
|
// ARMS: tying flow (do NOT call super — avoids double equip)
|
||||||
|
if (regions.contains(BodyRegionV2.ARMS) && player instanceof ServerPlayer serverPlayer) {
|
||||||
|
return TyingInteractionHelper.handleTying(serverPlayer, target, stack, hand);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NECK: blocked until Branch C wires the full collar equip flow
|
||||||
|
// (add owner to NBT, register in CollarRegistry, play sound, sync).
|
||||||
|
// Without this, V2 collars equip without ownership — breaking GPS, shock, alerts.
|
||||||
|
if (regions.contains(BodyRegionV2.NECK)) {
|
||||||
|
TiedUpMod.LOGGER.debug("[DataDrivenBondageItem] NECK equip blocked — collar flow not wired yet");
|
||||||
|
return InteractionResult.PASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other regions (MOUTH, EYES, EARS, HANDS): instant equip via parent
|
||||||
|
return super.interactLivingEntity(stack, player, target, hand);
|
||||||
|
}
|
||||||
|
|
||||||
// ===== IHasResistance IMPLEMENTATION =====
|
// ===== IHasResistance IMPLEMENTATION =====
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -246,6 +309,12 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Component tooltips
|
||||||
|
ComponentHolder holder = DataDrivenItemRegistry.getComponents(stack);
|
||||||
|
if (holder != null) {
|
||||||
|
holder.appendTooltip(stack, level, tooltip, flag);
|
||||||
|
}
|
||||||
|
|
||||||
// Lock status + escape difficulty (from AbstractV2BondageItem)
|
// Lock status + escape difficulty (from AbstractV2BondageItem)
|
||||||
super.appendHoverText(stack, level, tooltip, flag);
|
super.appendHoverText(stack, level, tooltip, flag);
|
||||||
|
|
||||||
@@ -285,6 +354,17 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
|||||||
if (holder != null) {
|
if (holder != null) {
|
||||||
holder.onEquipped(stack, entity);
|
holder.onEquipped(stack, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NPC speed reduction (players use MovementStyleManager, not this legacy path)
|
||||||
|
if (!(entity instanceof Player)) {
|
||||||
|
Set<BodyRegionV2> regions = getOccupiedRegions(stack);
|
||||||
|
if (regions.contains(BodyRegionV2.ARMS) && BindModeHelper.hasLegsBound(stack)) {
|
||||||
|
com.tiedup.remake.items.base.PoseType pose = com.tiedup.remake.v2.bondage.PoseTypeHelper.getPoseType(stack);
|
||||||
|
boolean fullImmobilization = pose == com.tiedup.remake.items.base.PoseType.WRAP
|
||||||
|
|| pose == com.tiedup.remake.items.base.PoseType.LATEX_SACK;
|
||||||
|
com.tiedup.remake.util.RestraintEffectUtils.applyBindSpeedReduction(entity, fullImmobilization);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -293,6 +373,14 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
|
|||||||
if (holder != null) {
|
if (holder != null) {
|
||||||
holder.onUnequipped(stack, entity);
|
holder.onUnequipped(stack, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NPC speed cleanup
|
||||||
|
if (!(entity instanceof Player)) {
|
||||||
|
Set<BodyRegionV2> regions = getOccupiedRegions(stack);
|
||||||
|
if (regions.contains(BodyRegionV2.ARMS)) {
|
||||||
|
com.tiedup.remake.util.RestraintEffectUtils.removeBindSpeedReduction(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ public record DataDrivenItemDefinition(
|
|||||||
/** Body regions this item blocks. Defaults to occupiedRegions if not specified. */
|
/** Body regions this item blocks. Defaults to occupiedRegions if not specified. */
|
||||||
Set<BodyRegionV2> blockedRegions,
|
Set<BodyRegionV2> blockedRegions,
|
||||||
|
|
||||||
|
/** Optional pose type identifier (e.g., "STANDARD", "STRAITJACKET", "DOG"). */
|
||||||
|
@Nullable String poseType,
|
||||||
|
|
||||||
/** Pose priority for conflict resolution. Higher wins. */
|
/** Pose priority for conflict resolution. Higher wins. */
|
||||||
int posePriority,
|
int posePriority,
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,9 @@ public final class DataDrivenItemParser {
|
|||||||
blockedRegions = occupiedRegions;
|
blockedRegions = occupiedRegions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional: pose_type (e.g., "STANDARD", "STRAITJACKET", "DOG")
|
||||||
|
String poseType = getStringOrNull(root, "pose_type");
|
||||||
|
|
||||||
// Optional: pose_priority (default 0)
|
// Optional: pose_priority (default 0)
|
||||||
int posePriority = getIntOrDefault(root, "pose_priority", 0);
|
int posePriority = getIntOrDefault(root, "pose_priority", 0);
|
||||||
|
|
||||||
@@ -328,6 +331,7 @@ public final class DataDrivenItemParser {
|
|||||||
animationSource,
|
animationSource,
|
||||||
occupiedRegions,
|
occupiedRegions,
|
||||||
blockedRegions,
|
blockedRegions,
|
||||||
|
poseType,
|
||||||
posePriority,
|
posePriority,
|
||||||
escapeDifficulty,
|
escapeDifficulty,
|
||||||
lockable,
|
lockable,
|
||||||
|
|||||||
Reference in New Issue
Block a user