12 Commits

Author SHA1 Message Date
NotEvil
df56ebb6bc fix(D-01/A): adversarial review fixes — 4 logic bugs
1. NECK region explicitly blocked in interactLivingEntity() — prevents
   V2 collars equipping without ownership setup (latent, no JSONs yet)

2. Swap rollback safety: if re-equip fails after swap failure, drop the
   old bind as item entity instead of losing it silently

3. GaggingComponent: cache GagMaterial at construction time — eliminates
   valueOf() log spam on every chat message with misconfigured material

4. Dual-bind prevention: check both V1 isTiedUp() AND V2 region occupied
   in TyingInteractionHelper and PacketSelfBondage to prevent equipping
   V2 bind on top of V1 bind
2026-04-14 16:48:50 +02:00
NotEvil
b97bdf367e fix(D-01/A): V2 bind/collar resistance completely broken (CRITICAL)
PlayerEquipment.getCurrentBindResistance/setCurrentBindResistance and
getCurrentCollarResistance/setCurrentCollarResistance all checked
instanceof ItemBind/ItemCollar — V2 DataDrivenBondageItem silently
returned 0, making V2 items escapable in 1 struggle roll.

Fix: use instanceof IHasResistance which both V1 and V2 implement.

Also fix StruggleCollar.tighten() to read ResistanceComponent directly
for V2 collars instead of IHasResistance.getBaseResistance(entity)
which triggers the singleton MAX-scan across all equipped items.

Note: isItemLocked() dead code in StruggleState is a PRE-EXISTING bug
(x10 locked penalty never applied) — tracked for separate fix.
2026-04-14 16:44:59 +02:00
NotEvil
eb7f06bfc8 fix(D-01/A): 3 review bugs + null guards (BUG-001, BUG-002, BUG-003, RISK-003)
BUG-001: TyingInteractionHelper swap now checks V2EquipResult — on failure,
rolls back by re-equipping the old bind instead of leaving target untied

BUG-002: OwnershipComponent.onUnequipped no longer double-calls
unregisterWearer — onCollarRemoved already handles it. Suppressed-alert
path calls unregister directly since onCollarRemoved is skipped.

BUG-003: PacketSelfBondage handleV2SelfBind now clears completed tying
task from PlayerBindState to prevent blocking future tying interactions

RISK-003: StruggleCollar get/setResistanceState null-guard on player
2026-04-14 16:38:09 +02:00
NotEvil
5c4e4c2352 fix(D-01/A): double item consumption + unchecked cast in TyingInteractionHelper
QA-001: add instanceof V2TyingPlayerTask guard before cast to prevent
ClassCastException when a V1 TyingPlayerTask was still active

QA-002: remove stack.shrink(1) after tying completion — V2TyingPlayerTask
.onComplete() already consumes the held item via heldStack.shrink(1)
2026-04-14 16:35:05 +02:00
NotEvil
eee4825aba feat(D-01/A): NPC speed reduction for V2 items (A12)
- onEquipped: apply RestraintEffectUtils speed reduction for non-Player
  entities with ARMS region and legs bound. Full immobilization for
  WRAP/LATEX_SACK pose types.
- onUnequipped: remove speed reduction for non-Player entities
- Players use MovementStyleManager (V2 tick-based), not this legacy path
2026-04-14 16:07:41 +02:00
NotEvil
b359c6be35 feat(D-01/A): self-bondage region routing (A11)
- handleV2SelfBondage: split into region-based routing
  - NECK → blocked (cannot self-collar)
  - ARMS → handleV2SelfBind (tying task with progress bar)
  - Other → handleV2SelfAccessory (instant equip)
- handleV2SelfAccessory: arms-bound check via BindModeHelper,
  locked check for swap, V2EquipmentHelper for conflict resolution
2026-04-14 16:06:01 +02:00
NotEvil
19cc69985d feat(D-01/A): V2-aware struggle system (A9)
StruggleBinds:
- canStruggle(): instanceof ItemBind → BindModeHelper.isBindItem()
- isItemLocked(): instanceof ItemBind → instanceof ILockable (fixes R4)
- onAttempt(): instanceof ItemShockCollar → CollarHelper.canShock() (fixes R5)
- tighten(): reads ResistanceComponent directly for V2, avoids MAX scan bug

StruggleCollar:
- getResistanceState/setResistanceState: instanceof ItemCollar → IHasResistance
- canStruggle(): instanceof ItemCollar → CollarHelper.isCollar() + ILockable
- onAttempt(): shock check via CollarHelper.canShock()
- successAction(): unlock via ILockable
- tighten(): resistance via IHasResistance

All V1 items continue working through the same interfaces they already implement.
2026-04-14 15:47:20 +02:00
NotEvil
737a4fd59b feat(D-01/A): interaction routing + TyingInteractionHelper (A8)
- DataDrivenBondageItem.use(): shift+click cycles bind mode for ARMS items
- DataDrivenBondageItem.interactLivingEntity(): region-based routing
  - ARMS → TyingInteractionHelper (tying task with progress bar)
  - NECK → deferred to Branch C (no V2 collar JSONs yet)
  - Other regions → instant equip via parent AbstractV2BondageItem
- TyingInteractionHelper: extracted tying flow using V2TyingPlayerTask
  - Distance/LoS validation, swap if already tied, task lifecycle
2026-04-14 15:35:31 +02:00
NotEvil
b79225d684 feat(D-01/A): OwnershipComponent lifecycle hooks (A7)
- onEquipped: register collar owners in CollarRegistry (server-side only)
- onUnequipped: alert kidnappers + unregister from CollarRegistry
- Guards: client-side check, ServerLevel cast, empty owners skip, try-catch
- appendTooltip: nickname, owner count, shock/GPS/choke capabilities
- Delegates alert suppression to ItemCollar.isRemovalAlertSuppressed()
2026-04-14 15:29:31 +02:00
NotEvil
751bad418d feat(D-01/A): poseType, helpers, OWNERSHIP ComponentType (A4, A5, A6)
- DataDrivenItemDefinition: add poseType field, parsed from JSON "pose_type"
- PoseTypeHelper: resolves PoseType from V2 definition or V1 ItemBind fallback
- BindModeHelper: static bind mode NBT utilities (isBindItem, hasArmsBound,
  hasLegsBound, cycleBindModeId) — works for V1 and V2 items
- CollarHelper: complete static utility class for collar operations with
  dual-path V2/V1 dispatch (ownership, features, shock, GPS, choke, alert)
- ComponentType: add OWNERSHIP enum value
- OwnershipComponent: stub class (lifecycle hooks added in next commit)
2026-04-14 15:23:08 +02:00
NotEvil
b81d3eed95 feat(D-01/A): config-driven components + tooltip hook (A1, A2, A3)
- ResistanceComponent: resistanceId delegates to SettingsAccessor at runtime,
  fallback to hardcoded base for backward compat
- GaggingComponent: material field delegates to GagMaterial enum from ModConfig,
  explicit comprehension/range overrides take priority
- IItemComponent: add default appendTooltip() method
- ComponentHolder: iterate components for tooltip contribution
- 6 components implement appendTooltip (lockable, resistance, gagging, shock,
  gps, choking)
- DataDrivenBondageItem: call holder.appendTooltip() in appendHoverText()
2026-04-14 15:05:48 +02:00
fa4c332a10 Merge pull request 'feature/d01-component-system' (#5) from feature/d01-component-system into develop
Reviewed-on: #5
2026-04-14 00:54:16 +00:00
21 changed files with 1262 additions and 188 deletions

View File

@@ -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());
}
} }
/** /**

View File

@@ -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 ==========

View File

@@ -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 collarnotify 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);

View File

@@ -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 collarnotify 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 {} -> {})",

View File

@@ -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");
}
}

View 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
} }

View File

@@ -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();
} }

View File

@@ -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;

View File

@@ -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));
}
} }
} }

View File

@@ -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));
}
}
} }

View File

@@ -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) {}
} }

View File

@@ -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));
}
}
} }

View File

@@ -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));
}
}
}

View File

@@ -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));
}
} }
} }

View File

@@ -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));
}
}
} }

View File

@@ -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

View File

@@ -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,

View File

@@ -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,