16 Commits

Author SHA1 Message Date
NotEvil
530b86a9a7 fix(D-01/B): hood missing MOUTH block + organic items lockable:false
- Hood: add MOUTH to blocked_regions — prevents double gag stacking
- 8 organic items (slime, vine, web, tape): set lockable:false at top
  level for consistency with can_attach_padlock:false
2026-04-14 17:59:27 +02:00
NotEvil
258223bf68 feat(D-01/B): 47 data-driven item definitions + cleanup test files
16 binds: ropes, armbinder, dogbinder, chain, ribbon, slime, vine_seed,
  web_bind, shibari, leather_straps, medical_straps, beam_cuffs,
  duct_tape, straitjacket, wrap, latex_sack

19 gags: cloth_gag, ropes_gag, cleave_gag, ribbon_gag, ball_gag,
  ball_gag_strap, tape_gag, wrap_gag, slime_gag, vine_gag, web_gag,
  panel_gag, beam_panel_gag, chain_panel_gag, latex_gag, tube_gag,
  bite_gag, sponge_gag, baguette_gag

2 blindfolds, 1 earplugs, 1 mittens
5 collars (classic, shock, shock_auto, gps, choke) with ownership component
3 combos (hood, medical_gag, ball_gag_3d)

All items config-driven via ResistanceComponent (id) and GaggingComponent
(material). Organic items have can_attach_padlock: false.

Removed 4 test files (test_component_gag, test_handcuffs, test_leg_cuffs).
2026-04-14 17:52:19 +02:00
NotEvil
679d7033f9 feat(D-01/B): add can_attach_padlock field to data-driven items (B0)
- DataDrivenItemDefinition: add canAttachPadlock boolean (default true)
- DataDrivenItemParser: parse "can_attach_padlock" from JSON
- DataDrivenBondageItem: add static canAttachPadlockTo(stack) method
- AnvilEventHandler: check V2 definition before allowing padlock attach
2026-04-14 17:47:32 +02:00
8bfd97ba57 Merge pull request 'feature/d01-branch-a-bridge' (#6) from feature/d01-branch-a-bridge into develop
Reviewed-on: #6
2026-04-14 15:19:20 +00:00
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
120 changed files with 3995 additions and 260 deletions

View File

@@ -40,7 +40,11 @@ public class AnvilEventHandler {
// Check if item can have a padlock attached (tape, slime, vine, web cannot)
if (!lockable.canAttachPadlock()) {
return; // Item type cannot have padlock
return; // Item type cannot have padlock (V1)
}
// V2 data-driven items: check definition's can_attach_padlock field
if (!com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem.canAttachPadlockTo(left)) {
return;
}
// Item must not already have a padlock attached

View File

@@ -14,8 +14,10 @@ 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.BindModeHelper;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.v2.bondage.V2EquipResult;
import java.util.function.Supplier;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
@@ -222,69 +224,116 @@ public class PacketSelfBondage {
IV2BondageItem v2Item,
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
boolean allBlocked = true;
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
if (
!V2EquipmentHelper.isRegionOccupied(player, region) &&
!V2EquipmentHelper.isRegionBlocked(player, region)
) {
if (!V2EquipmentHelper.isRegionOccupied(player, region)
&& !V2EquipmentHelper.isRegionBlocked(player, region)) {
allBlocked = false;
break;
}
}
if (allBlocked) {
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} tried V2 self-equip but all regions occupied",
player.getName().getString()
);
TiedUpMod.LOGGER.debug("[SelfBondage] {} tried V2 self-equip but all regions occupied", player.getName().getString());
return;
}
PlayerBindState playerState = PlayerBindState.getInstance(player);
if (playerState == null) return;
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(
player.level().getGameRules()
);
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules());
// Create V2 tying task (uses V2EquipmentHelper on completion, NOT putBindOn)
V2TyingPlayerTask newTask = new V2TyingPlayerTask(
stack.copy(), // copy for display/matching
stack, // live reference for consumption
state,
player, // target is self
tyingSeconds,
player.level(),
player // kidnapper is also self
stack.copy(), stack, state, player, tyingSeconds, player.level(), player
);
TyingTask currentTask = playerState.getCurrentTyingTask();
if (
currentTask == null ||
!currentTask.isSameTarget(player) ||
currentTask.isOutdated() ||
!ItemStack.matches(currentTask.getBind(), stack)
) {
// Start new task
if (currentTask == null
|| !currentTask.isSameTarget(player)
|| currentTask.isOutdated()
|| !ItemStack.matches(currentTask.getBind(), stack)) {
playerState.setCurrentTyingTask(newTask);
newTask.start();
TiedUpMod.LOGGER.debug(
"[SelfBondage] {} started V2 self-tying ({} seconds)",
player.getName().getString(),
tyingSeconds
);
} else {
// Continue existing task — just mark active
currentTask.update();
}
// If we started a new task, mark it active too
if (playerState.getCurrentTyingTask() == newTask) {
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,
BodyRegionV2.ARMS
);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
) return 0;
return bind.getCurrentResistance(stack, player);
if (stack.isEmpty()) return 0;
// V1 and V2 both implement IHasResistance
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) {
return resistance.getCurrentResistance(stack, player);
}
return 0;
}
/**
@@ -334,10 +336,11 @@ public class PlayerEquipment {
player,
BodyRegionV2.ARMS
);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
) return;
bind.setCurrentResistance(stack, resistance);
if (stack.isEmpty()) return;
// V1 and V2 both implement IHasResistance
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) {
resistanceItem.setCurrentResistance(stack, resistance);
}
}
/**
@@ -348,10 +351,12 @@ public class PlayerEquipment {
player,
BodyRegionV2.NECK
);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
) return 0;
return collar.getCurrentResistance(stack, player);
if (stack.isEmpty()) return 0;
// V1 and V2 both implement IHasResistance
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistance) {
return resistance.getCurrentResistance(stack, player);
}
return 0;
}
/**
@@ -362,10 +367,11 @@ public class PlayerEquipment {
player,
BodyRegionV2.NECK
);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
) return;
collar.setCurrentResistance(stack, resistance);
if (stack.isEmpty()) return;
// V1 and V2 both implement IHasResistance
if (stack.getItem() instanceof com.tiedup.remake.items.base.IHasResistance resistanceItem) {
resistanceItem.setCurrentResistance(stack, resistance);
}
}
// ========== 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.MessageCategory;
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.ItemBind;
import com.tiedup.remake.state.IPlayerLeashAccess;
import com.tiedup.remake.state.PlayerBindState;
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.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.player.Player;
import net.minecraft.world.item.ItemStack;
@@ -75,16 +81,17 @@ public class StruggleBinds extends StruggleState {
player,
BodyRegionV2.ARMS
);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) {
return false;
}
// 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,
BodyRegionV2.ARMS
);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
return false;
}
if (bindStack.isEmpty()) 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
);
if (
!collar.isEmpty() &&
collar.getItem() instanceof
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
if (!collar.isEmpty() && CollarHelper.canShock(collar)) {
// V1 shock collar
if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) {
return shockCollar.notifyStruggle(player, collar);
}
return true; // No collar, proceed normally
// V2 shock collar — notify via IHasResistance if available
if (collar.getItem() instanceof IHasResistance resistance) {
resistance.notifyStruggle(player);
}
return true;
}
return true; // No shock collar, proceed normally
}
/**
@@ -317,18 +327,23 @@ public class StruggleBinds extends StruggleState {
target,
BodyRegionV2.ARMS
);
if (
bindStack.isEmpty() ||
!(bindStack.getItem() instanceof ItemBind bind)
) {
if (bindStack.isEmpty() || !BindModeHelper.isBindItem(bindStack)) {
return;
}
// Get base resistance from config (BUG-003 fix: was using ModGameRules which
// only knew 4 types and returned hardcoded 100 for the other 10)
int baseResistance = SettingsAccessor.getBindResistance(
bind.getItemName()
// Get base resistance: V2 reads from ResistanceComponent directly,
// V1 reads from SettingsAccessor via item name (BUG-003 fix)
int baseResistance;
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)
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.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.state.PlayerBindState;
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.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.item.ItemStack;
@@ -44,19 +50,15 @@ public class StruggleCollar extends StruggleState {
@Override
protected int getResistanceState(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
if (player == null) return 0;
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
return 0;
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return 0;
if (collar.getItem() instanceof IHasResistance resistance) {
return resistance.getCurrentResistance(collar, player);
}
return collarItem.getCurrentResistance(collar, player);
return 0;
}
/**
@@ -68,19 +70,14 @@ public class StruggleCollar extends StruggleState {
@Override
protected void setResistanceState(PlayerBindState state, int resistance) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
if (player == null) return;
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
return;
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) return;
if (collar.getItem() instanceof IHasResistance resistanceItem) {
resistanceItem.setCurrentResistance(collar, resistance);
}
collarItem.setCurrentResistance(collar, resistance);
}
/**
@@ -104,33 +101,31 @@ public class StruggleCollar extends StruggleState {
return false;
}
ItemStack collar = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar equipped");
return false;
}
// Check if locked
if (!collarItem.isLocked(collar)) {
// Check if locked (works for V1 and V2 via ILockable)
if (collar.getItem() instanceof ILockable lockable) {
if (!lockable.isLocked(collar)) {
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked");
return false;
}
// Check if struggle is enabled
if (!collarItem.canBeStruggledOut(collar)) {
TiedUpMod.LOGGER.debug(
"[StruggleCollar] Collar struggle is disabled"
);
} else {
return false;
}
// Check if struggle is enabled (works for V1 and V2 via IHasResistance)
if (collar.getItem() instanceof IHasResistance resistance) {
if (!resistance.canBeStruggledOut(collar)) {
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar struggle is disabled");
return false;
}
}
return true;
}
@@ -141,18 +136,18 @@ public class StruggleCollar extends StruggleState {
@Override
protected boolean onAttempt(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
!collar.isEmpty() &&
collar.getItem() instanceof
com.tiedup.remake.items.ItemShockCollar shockCollar
) {
if (!collar.isEmpty() && CollarHelper.canShock(collar)) {
// V1 shock collar
if (collar.getItem() instanceof com.tiedup.remake.items.ItemShockCollar shockCollar) {
return shockCollar.notifyStruggle(player, collar);
}
// V2 shock collar — notify via IHasResistance
if (collar.getItem() instanceof IHasResistance resistance) {
resistance.notifyStruggle(player);
}
}
return true;
}
@@ -167,31 +162,19 @@ public class StruggleCollar extends StruggleState {
@Override
protected void successAction(PlayerBindState state) {
Player player = state.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(
player,
BodyRegionV2.NECK
);
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
TiedUpMod.LOGGER.warn(
"[StruggleCollar] successAction called but no collar equipped"
);
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
TiedUpMod.LOGGER.warn("[StruggleCollar] successAction called but no collar equipped");
return;
}
// Unlock the collar
collarItem.setLocked(collar, false);
// Unlock the collar (works for V1 and V2 via ILockable)
if (collar.getItem() instanceof ILockable lockable) {
lockable.setLocked(collar, false);
}
TiedUpMod.LOGGER.info(
"[StruggleCollar] {} unlocked their collar!",
player.getName().getString()
);
// Note: Collar is NOT removed, just unlocked
// Player can now manually remove it
TiedUpMod.LOGGER.info("[StruggleCollar] {} unlocked their collar!", player.getName().getString());
}
@Override
@@ -230,30 +213,36 @@ public class StruggleCollar extends StruggleState {
return;
}
ItemStack collar = V2EquipmentHelper.getInRegion(
target,
BodyRegionV2.NECK
);
ItemStack collar = V2EquipmentHelper.getInRegion(target, BodyRegionV2.NECK);
if (
collar.isEmpty() ||
!(collar.getItem() instanceof ItemCollar collarItem)
) {
if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar to tighten");
return;
}
// Check if collar is locked
if (!collarItem.isLocked(collar)) {
TiedUpMod.LOGGER.debug(
"[StruggleCollar] Collar must be locked to tighten"
);
// Check if collar is locked (V1 and V2 via ILockable)
if (collar.getItem() instanceof ILockable lockable) {
if (!lockable.isLocked(collar)) {
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar must be locked to tighten");
return;
}
} else {
return;
}
// Get base resistance from GameRules
int baseResistance = collarItem.getBaseResistance(target);
int currentResistance = collarItem.getCurrentResistance(collar, target);
// Get resistance — V2: read ResistanceComponent directly (avoids MAX-scan bug),
// V1: use IHasResistance.getBaseResistance()
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
if (currentResistance >= baseResistance) {
@@ -264,7 +253,7 @@ public class StruggleCollar extends StruggleState {
}
// Restore to base resistance
collarItem.setCurrentResistance(collar, baseResistance);
resistanceItem.setCurrentResistance(collar, baseResistance);
TiedUpMod.LOGGER.info(
"[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;
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.
@@ -43,4 +50,9 @@ public class ChokingComponent implements IItemComponent {
public boolean isNonLethalForMaster() {
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.EnumMap;
import java.util.List;
import java.util.Map;
import net.minecraft.network.chat.Component;
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;
public final class ComponentHolder {
@@ -58,6 +62,12 @@ public final class ComponentHolder {
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() {
return components.isEmpty();
}

View File

@@ -15,7 +15,8 @@ public enum ComponentType {
SHOCK("shock", ShockComponent::fromJson),
GPS("gps", GpsComponent::fromJson),
CHOKING("choking", ChokingComponent::fromJson),
ADJUSTABLE("adjustable", AdjustableComponent::fromJson);
ADJUSTABLE("adjustable", AdjustableComponent::fromJson),
OWNERSHIP("ownership", OwnershipComponent::fromJson);
private final String jsonKey;
private final Function<JsonObject, IItemComponent> factory;

View File

@@ -1,45 +1,100 @@
package com.tiedup.remake.v2.bondage.component;
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.
*
* 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 {
private final double comprehension;
private final double range;
private final @Nullable String material;
private final @Nullable GagMaterial cachedMaterial;
private final double comprehensionOverride;
private final double rangeOverride;
private GaggingComponent(double comprehension, double range) {
this.comprehension = comprehension;
this.range = range;
private GaggingComponent(@Nullable String material, @Nullable GagMaterial cachedMaterial,
double comprehensionOverride, double rangeOverride) {
this.material = material;
this.cachedMaterial = cachedMaterial;
this.comprehensionOverride = comprehensionOverride;
this.rangeOverride = rangeOverride;
}
public static IItemComponent fromJson(JsonObject config) {
double comprehension = 0.2;
double range = 10.0;
String material = null;
double comprehension = -1;
double range = -1;
if (config != null) {
if (config.has("material")) {
material = config.get("material").getAsString();
}
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")) {
range = config.get("range").getAsDouble();
range = Math.max(0.0, config.get("range").getAsDouble());
}
}
comprehension = Math.max(0.0, Math.min(1.0, comprehension));
range = Math.max(0.0, range);
return new GaggingComponent(comprehension, range);
// Resolve and cache GagMaterial at load time to avoid valueOf() on every chat message
GagMaterial resolved = null;
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). */
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. */
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;
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.
@@ -42,4 +49,14 @@ public class GpsComponent implements IItemComponent {
public boolean isPublicTracking() {
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;
import java.util.List;
import net.minecraft.network.chat.Component;
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;
/**
* A reusable behavior module for data-driven bondage items.
@@ -16,4 +21,6 @@ public interface IItemComponent {
default boolean blocksUnequip(ItemStack stack, LivingEntity entity) {
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;
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.
@@ -41,4 +48,13 @@ public class LockableComponent implements IItemComponent {
public int getLockResistance() {
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;
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.
*
* 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 {
private final int baseResistance;
private final @Nullable String resistanceId;
private final int fallbackBase;
private ResistanceComponent(int baseResistance) {
this.baseResistance = baseResistance;
private ResistanceComponent(@Nullable String resistanceId, int fallbackBase) {
this.resistanceId = resistanceId;
this.fallbackBase = fallbackBase;
}
public static IItemComponent fromJson(JsonObject config) {
String id = null;
int base = 100;
if (config != null && config.has("base")) {
if (config != null) {
if (config.has("id")) {
id = config.get("id").getAsString();
}
if (config.has("base")) {
base = config.get("base").getAsInt();
}
}
base = Math.max(0, base);
return new ResistanceComponent(base);
return new ResistanceComponent(id, base);
}
/**
* 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() {
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;
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.
@@ -49,4 +56,16 @@ public class ShockComponent implements IItemComponent {
public boolean hasAutoShock() {
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;
import com.tiedup.remake.core.TiedUpMod;
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.TyingInteractionHelper;
import com.tiedup.remake.v2.bondage.V2BondageItems;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.v2.bondage.component.ComponentHolder;
@@ -17,7 +20,13 @@ import java.util.stream.Collectors;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
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.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
@@ -116,6 +125,60 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
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 =====
@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)
super.appendHoverText(stack, level, tooltip, flag);
@@ -285,6 +354,17 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
if (holder != null) {
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
@@ -293,6 +373,14 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
if (holder != null) {
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
@@ -348,6 +436,19 @@ public class DataDrivenBondageItem extends AbstractV2BondageItem {
);
}
/**
* Stack-aware padlock attachment check for data-driven items.
* Returns false for organic items (slime, vine, web, tape) that have
* {@code can_attach_padlock: false} in their JSON definition.
*/
public static boolean canAttachPadlockTo(ItemStack stack) {
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
if (def != null) {
return def.canAttachPadlock();
}
return true;
}
// ===== FACTORY =====
/**

View File

@@ -46,6 +46,9 @@ public record DataDrivenItemDefinition(
/** Body regions this item blocks. Defaults to occupiedRegions if not specified. */
Set<BodyRegionV2> blockedRegions,
/** Optional pose type identifier (e.g., "STANDARD", "STRAITJACKET", "DOG"). */
@Nullable String poseType,
/** Pose priority for conflict resolution. Higher wins. */
int posePriority,
@@ -55,6 +58,9 @@ public record DataDrivenItemDefinition(
/** Whether this item can be locked with a padlock. */
boolean lockable,
/** Whether a padlock can be attached to this item. False for organic items (slime, vine, web, tape). */
boolean canAttachPadlock,
/** Whether this item supports color variants. */
boolean supportsColor,

View File

@@ -188,6 +188,9 @@ public final class DataDrivenItemParser {
blockedRegions = occupiedRegions;
}
// Optional: pose_type (e.g., "STANDARD", "STRAITJACKET", "DOG")
String poseType = getStringOrNull(root, "pose_type");
// Optional: pose_priority (default 0)
int posePriority = getIntOrDefault(root, "pose_priority", 0);
@@ -197,6 +200,9 @@ public final class DataDrivenItemParser {
// Optional: lockable (default true)
boolean lockable = getBooleanOrDefault(root, "lockable", true);
// Optional: can_attach_padlock (default true). False for organic items.
boolean canAttachPadlock = getBooleanOrDefault(root, "can_attach_padlock", true);
// Optional: supports_color (default false)
boolean supportsColor = getBooleanOrDefault(
root,
@@ -328,9 +334,11 @@ public final class DataDrivenItemParser {
animationSource,
occupiedRegions,
blockedRegions,
poseType,
posePriority,
escapeDifficulty,
lockable,
canAttachPadlock,
supportsColor,
tintChannels,
icon,

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Armbinder",
"translation_key": "item.tiedup.armbinder",
"model": "tiedup:models/gltf/v2/binds/armbinder.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Baguette Gag",
"translation_key": "item.tiedup.baguette_gag",
"model": "tiedup:models/gltf/v2/gags/baguette_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "baguette"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ball Gag",
"translation_key": "item.tiedup.ball_gag",
"model": "tiedup:models/gltf/v2/gags/ball_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "ball"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ball Gag 3D",
"translation_key": "item.tiedup.ball_gag_3d",
"model": "tiedup:models/gltf/v2/combos/ball_gag_3d.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "ball"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ball Gag Strap",
"translation_key": "item.tiedup.ball_gag_strap",
"model": "tiedup:models/gltf/v2/gags/ball_gag_strap.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "ball"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Beam Cuffs",
"translation_key": "item.tiedup.beam_cuffs",
"model": "tiedup:models/gltf/v2/binds/beam_cuffs.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "chain"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Beam Panel Gag",
"translation_key": "item.tiedup.beam_panel_gag",
"model": "tiedup:models/gltf/v2/gags/beam_panel_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Bite Gag",
"translation_key": "item.tiedup.bite_gag",
"model": "tiedup:models/gltf/v2/gags/bite_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "bite"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,26 @@
{
"type": "tiedup:bondage_item",
"display_name": "Blindfold Mask",
"translation_key": "item.tiedup.blindfold_mask",
"model": "tiedup:models/gltf/v2/blindfolds/blindfold_mask.glb",
"regions": [
"EYES"
],
"pose_priority": 5,
"escape_difficulty": 30,
"lockable": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
},
"blinding": {},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Chains",
"translation_key": "item.tiedup.chain",
"model": "tiedup:models/gltf/v2/binds/chain.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "chain"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Chain Panel Gag",
"translation_key": "item.tiedup.chain_panel_gag",
"model": "tiedup:models/gltf/v2/gags/chain_panel_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,25 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "Choke Collar",
"translation_key": "item.tiedup.choke_collar",
"model": "tiedup:models/gltf/v2/collars/choke_collar.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
},
"choking": {}
}
}

View File

@@ -0,0 +1,26 @@
{
"type": "tiedup:bondage_item",
"display_name": "Blindfold",
"translation_key": "item.tiedup.classic_blindfold",
"model": "tiedup:models/gltf/v2/blindfolds/classic_blindfold.glb",
"regions": [
"EYES"
],
"pose_priority": 5,
"escape_difficulty": 30,
"lockable": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
},
"blinding": {},
"adjustable": {}
}
}

View File

@@ -0,0 +1,24 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "Classic Collar",
"translation_key": "item.tiedup.classic_collar",
"model": "tiedup:models/gltf/v2/collars/classic_collar.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"type": "tiedup:bondage_item",
"display_name": "Earplugs",
"translation_key": "item.tiedup.classic_earplugs",
"model": "tiedup:models/gltf/v2/earplugs/classic_earplugs.glb",
"regions": [
"EARS"
],
"pose_priority": 5,
"escape_difficulty": 20,
"lockable": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Cleave Gag",
"translation_key": "item.tiedup.cleave_gag",
"model": "tiedup:models/gltf/v2/gags/cleave_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "cloth"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Cloth Gag",
"translation_key": "item.tiedup.cloth_gag",
"model": "tiedup:models/gltf/v2/gags/cloth_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "cloth"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,32 @@
{
"type": "tiedup:bondage_item",
"display_name": "Dogbinder",
"translation_key": "item.tiedup.dogbinder",
"model": "tiedup:models/gltf/v2/binds/dogbinder.glb",
"regions": [
"ARMS"
],
"pose_type": "DOG",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
},
"movement_style": "CRAWL"
}

View File

@@ -0,0 +1,30 @@
{
"type": "tiedup:bondage_item",
"display_name": "Duct Tape",
"translation_key": "item.tiedup.duct_tape",
"model": "tiedup:models/gltf/v2/binds/duct_tape.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": false,
"can_attach_padlock": false,
"supports_color": true,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"resistance": {
"id": "tape"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "GPS Collar",
"translation_key": "item.tiedup.gps_collar",
"model": "tiedup:models/gltf/v2/collars/gps_collar.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
},
"gps": {
"safe_zone_radius": 50
}
}
}

View File

@@ -0,0 +1,32 @@
{
"type": "tiedup:bondage_item",
"display_name": "Hood",
"translation_key": "item.tiedup.hood",
"model": "tiedup:models/gltf/v2/combos/hood.glb",
"regions": [
"EYES"
],
"blocked_regions": [
"EYES",
"EARS",
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 40,
"lockable": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
},
"blinding": {},
"gagging": {
"material": "stuffed"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Latex Gag",
"translation_key": "item.tiedup.latex_gag",
"model": "tiedup:models/gltf/v2/gags/latex_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "latex"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Latex Sack",
"translation_key": "item.tiedup.latex_sack",
"model": "tiedup:models/gltf/v2/binds/latex_sack.glb",
"regions": [
"ARMS"
],
"pose_type": "LATEX_SACK",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "latex_sack"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"type": "tiedup:bondage_item",
"display_name": "Leather Mittens",
"translation_key": "item.tiedup.leather_mittens",
"model": "tiedup:models/gltf/v2/mittens/leather_mittens.glb",
"regions": [
"HANDS"
],
"pose_priority": 5,
"escape_difficulty": 20,
"lockable": true,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Leather Straps",
"translation_key": "item.tiedup.leather_straps",
"model": "tiedup:models/gltf/v2/binds/leather_straps.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"type": "tiedup:bondage_item",
"display_name": "Medical Gag",
"translation_key": "item.tiedup.medical_gag",
"model": "tiedup:models/gltf/v2/combos/medical_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"blinding": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Medical Straps",
"translation_key": "item.tiedup.medical_straps",
"model": "tiedup:models/gltf/v2/binds/medical_straps.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Panel Gag",
"translation_key": "item.tiedup.panel_gag",
"model": "tiedup:models/gltf/v2/gags/panel_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ribbon",
"translation_key": "item.tiedup.ribbon",
"model": "tiedup:models/gltf/v2/binds/ribbon.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "ribbon"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ribbon Gag",
"translation_key": "item.tiedup.ribbon_gag",
"model": "tiedup:models/gltf/v2/gags/ribbon_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "cloth"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ropes",
"translation_key": "item.tiedup.ropes",
"model": "tiedup:models/gltf/v2/binds/ropes.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "rope"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Rope Gag",
"translation_key": "item.tiedup.ropes_gag",
"model": "tiedup:models/gltf/v2/gags/ropes_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "cloth"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Shibari",
"translation_key": "item.tiedup.shibari",
"model": "tiedup:models/gltf/v2/binds/shibari.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "rope"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "Shock Collar",
"translation_key": "item.tiedup.shock_collar",
"model": "tiedup:models/gltf/v2/collars/shock_collar.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
},
"shock": {
"damage": 2.0
}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "Auto Shock Collar",
"translation_key": "item.tiedup.shock_collar_auto",
"model": "tiedup:models/gltf/v2/collars/shock_collar_auto.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
},
"shock": {
"damage": 2.0,
"auto_interval": 200
}
}
}

View File

@@ -0,0 +1,30 @@
{
"type": "tiedup:bondage_item",
"display_name": "Slime Bind",
"translation_key": "item.tiedup.slime",
"model": "tiedup:models/gltf/v2/binds/slime.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": false,
"can_attach_padlock": false,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"resistance": {
"id": "slime"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "tiedup:bondage_item",
"display_name": "Slime Gag",
"translation_key": "item.tiedup.slime_gag",
"model": "tiedup:models/gltf/v2/gags/slime_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": false,
"can_attach_padlock": false,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"resistance": {
"id": "gag"
},
"gagging": {
"material": "stuffed"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Sponge Gag",
"translation_key": "item.tiedup.sponge_gag",
"model": "tiedup:models/gltf/v2/gags/sponge_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "sponge"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Straitjacket",
"translation_key": "item.tiedup.straitjacket",
"model": "tiedup:models/gltf/v2/binds/straitjacket.glb",
"regions": [
"ARMS"
],
"pose_type": "STRAITJACKET",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "straitjacket"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "tiedup:bondage_item",
"display_name": "Tape Gag",
"translation_key": "item.tiedup.tape_gag",
"model": "tiedup:models/gltf/v2/gags/tape_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": false,
"can_attach_padlock": false,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"resistance": {
"id": "gag"
},
"gagging": {
"material": "tape"
},
"adjustable": {}
}
}

View File

@@ -1,15 +0,0 @@
{
"type": "tiedup:bondage_item",
"display_name": "Data-Driven Handcuffs",
"creator": "TiedUp! Team",
"model": "tiedup:models/gltf/v2/handcuffs/cuffs_prototype.glb",
"regions": ["ARMS"],
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"icon": "tiedup:item/beam_cuffs",
"animation_bones": {
"idle": ["rightArm", "leftArm"],
"struggle": ["rightArm", "leftArm"]
}
}

View File

@@ -1,18 +0,0 @@
{
"type": "tiedup:bondage_item",
"display_name": "Prototype Leg Cuffs",
"model": "tiedup:models/gltf/leg_cuffs_proto.glb",
"regions": ["LEGS"],
"pose_priority": 30,
"escape_difficulty": 5,
"lockable": true,
"movement_style": "shuffle",
"supports_color": true,
"tint_channels": {
"tintable_1": "#808080"
},
"animation_bones": {
"idle": ["rightLeg", "leftLeg"],
"struggle": ["rightLeg", "leftLeg"]
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Tube Gag",
"translation_key": "item.tiedup.tube_gag",
"model": "tiedup:models/gltf/v2/gags/tube_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "ring"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "tiedup:bondage_item",
"display_name": "Vine Gag",
"translation_key": "item.tiedup.vine_gag",
"model": "tiedup:models/gltf/v2/gags/vine_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": false,
"can_attach_padlock": false,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"resistance": {
"id": "gag"
},
"gagging": {
"material": "stuffed"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,30 @@
{
"type": "tiedup:bondage_item",
"display_name": "Vine Bind",
"translation_key": "item.tiedup.vine_seed",
"model": "tiedup:models/gltf/v2/binds/vine_seed.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": false,
"can_attach_padlock": false,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"resistance": {
"id": "vine"
}
}
}

View File

@@ -0,0 +1,30 @@
{
"type": "tiedup:bondage_item",
"display_name": "Web Bind",
"translation_key": "item.tiedup.web_bind",
"model": "tiedup:models/gltf/v2/binds/web_bind.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": false,
"can_attach_padlock": false,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"resistance": {
"id": "web"
}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "tiedup:bondage_item",
"display_name": "Web Gag",
"translation_key": "item.tiedup.web_gag",
"model": "tiedup:models/gltf/v2/gags/web_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": false,
"can_attach_padlock": false,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"resistance": {
"id": "gag"
},
"gagging": {
"material": "stuffed"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Wrap",
"translation_key": "item.tiedup.wrap",
"model": "tiedup:models/gltf/v2/binds/wrap.glb",
"regions": [
"ARMS"
],
"pose_type": "WRAP",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "wrap"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Wrap Gag",
"translation_key": "item.tiedup.wrap_gag",
"model": "tiedup:models/gltf/v2/gags/wrap_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "stuffed"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Armbinder",
"translation_key": "item.tiedup.armbinder",
"model": "tiedup:models/gltf/v2/binds/armbinder.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Baguette Gag",
"translation_key": "item.tiedup.baguette_gag",
"model": "tiedup:models/gltf/v2/gags/baguette_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "baguette"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ball Gag",
"translation_key": "item.tiedup.ball_gag",
"model": "tiedup:models/gltf/v2/gags/ball_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "ball"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,28 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ball Gag 3D",
"translation_key": "item.tiedup.ball_gag_3d",
"model": "tiedup:models/gltf/v2/combos/ball_gag_3d.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "ball"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ball Gag Strap",
"translation_key": "item.tiedup.ball_gag_strap",
"model": "tiedup:models/gltf/v2/gags/ball_gag_strap.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "ball"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Beam Cuffs",
"translation_key": "item.tiedup.beam_cuffs",
"model": "tiedup:models/gltf/v2/binds/beam_cuffs.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "chain"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Beam Panel Gag",
"translation_key": "item.tiedup.beam_panel_gag",
"model": "tiedup:models/gltf/v2/gags/beam_panel_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Bite Gag",
"translation_key": "item.tiedup.bite_gag",
"model": "tiedup:models/gltf/v2/gags/bite_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "bite"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,26 @@
{
"type": "tiedup:bondage_item",
"display_name": "Blindfold Mask",
"translation_key": "item.tiedup.blindfold_mask",
"model": "tiedup:models/gltf/v2/blindfolds/blindfold_mask.glb",
"regions": [
"EYES"
],
"pose_priority": 5,
"escape_difficulty": 30,
"lockable": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
},
"blinding": {},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Chains",
"translation_key": "item.tiedup.chain",
"model": "tiedup:models/gltf/v2/binds/chain.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "chain"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Chain Panel Gag",
"translation_key": "item.tiedup.chain_panel_gag",
"model": "tiedup:models/gltf/v2/gags/chain_panel_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,25 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "Choke Collar",
"translation_key": "item.tiedup.choke_collar",
"model": "tiedup:models/gltf/v2/collars/choke_collar.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
},
"choking": {}
}
}

View File

@@ -0,0 +1,26 @@
{
"type": "tiedup:bondage_item",
"display_name": "Blindfold",
"translation_key": "item.tiedup.classic_blindfold",
"model": "tiedup:models/gltf/v2/blindfolds/classic_blindfold.glb",
"regions": [
"EYES"
],
"pose_priority": 5,
"escape_difficulty": 30,
"lockable": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
},
"blinding": {},
"adjustable": {}
}
}

View File

@@ -0,0 +1,24 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "Classic Collar",
"translation_key": "item.tiedup.classic_collar",
"model": "tiedup:models/gltf/v2/collars/classic_collar.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
}
}
}

View File

@@ -0,0 +1,23 @@
{
"type": "tiedup:bondage_item",
"display_name": "Earplugs",
"translation_key": "item.tiedup.classic_earplugs",
"model": "tiedup:models/gltf/v2/earplugs/classic_earplugs.glb",
"regions": [
"EARS"
],
"pose_priority": 5,
"escape_difficulty": 20,
"lockable": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Cleave Gag",
"translation_key": "item.tiedup.cleave_gag",
"model": "tiedup:models/gltf/v2/gags/cleave_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "cloth"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Cloth Gag",
"translation_key": "item.tiedup.cloth_gag",
"model": "tiedup:models/gltf/v2/gags/cloth_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "cloth"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,32 @@
{
"type": "tiedup:bondage_item",
"display_name": "Dogbinder",
"translation_key": "item.tiedup.dogbinder",
"model": "tiedup:models/gltf/v2/binds/dogbinder.glb",
"regions": [
"ARMS"
],
"pose_type": "DOG",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
},
"movement_style": "CRAWL"
}

View File

@@ -0,0 +1,30 @@
{
"type": "tiedup:bondage_item",
"display_name": "Duct Tape",
"translation_key": "item.tiedup.duct_tape",
"model": "tiedup:models/gltf/v2/binds/duct_tape.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": false,
"can_attach_padlock": false,
"supports_color": true,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"resistance": {
"id": "tape"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"type": "tiedup:bondage_item",
"regions": [
"NECK"
],
"pose_priority": 5,
"escape_difficulty": 80,
"lockable": true,
"animation_bones": {
"idle": [
"body"
]
},
"display_name": "GPS Collar",
"translation_key": "item.tiedup.gps_collar",
"model": "tiedup:models/gltf/v2/collars/gps_collar.glb",
"components": {
"ownership": {},
"lockable": {},
"resistance": {
"id": "collar"
},
"gps": {
"safe_zone_radius": 50
}
}
}

View File

@@ -0,0 +1,32 @@
{
"type": "tiedup:bondage_item",
"display_name": "Hood",
"translation_key": "item.tiedup.hood",
"model": "tiedup:models/gltf/v2/combos/hood.glb",
"regions": [
"EYES"
],
"blocked_regions": [
"EYES",
"EARS",
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 40,
"lockable": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "blindfold"
},
"blinding": {},
"gagging": {
"material": "stuffed"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Latex Gag",
"translation_key": "item.tiedup.latex_gag",
"model": "tiedup:models/gltf/v2/gags/latex_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "latex"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Latex Sack",
"translation_key": "item.tiedup.latex_sack",
"model": "tiedup:models/gltf/v2/binds/latex_sack.glb",
"regions": [
"ARMS"
],
"pose_type": "LATEX_SACK",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "latex_sack"
}
}
}

View File

@@ -0,0 +1,21 @@
{
"type": "tiedup:bondage_item",
"display_name": "Leather Mittens",
"translation_key": "item.tiedup.leather_mittens",
"model": "tiedup:models/gltf/v2/mittens/leather_mittens.glb",
"regions": [
"HANDS"
],
"pose_priority": 5,
"escape_difficulty": 20,
"lockable": true,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Leather Straps",
"translation_key": "item.tiedup.leather_straps",
"model": "tiedup:models/gltf/v2/binds/leather_straps.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
}
}

View File

@@ -0,0 +1,27 @@
{
"type": "tiedup:bondage_item",
"display_name": "Medical Gag",
"translation_key": "item.tiedup.medical_gag",
"model": "tiedup:models/gltf/v2/combos/medical_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"blinding": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Medical Straps",
"translation_key": "item.tiedup.medical_straps",
"model": "tiedup:models/gltf/v2/binds/medical_straps.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "armbinder"
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "tiedup:bondage_item",
"display_name": "Panel Gag",
"translation_key": "item.tiedup.panel_gag",
"model": "tiedup:models/gltf/v2/gags/panel_gag.glb",
"regions": [
"MOUTH"
],
"pose_priority": 10,
"escape_difficulty": 50,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"head"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "gag"
},
"gagging": {
"material": "panel"
},
"adjustable": {}
}
}

View File

@@ -0,0 +1,31 @@
{
"type": "tiedup:bondage_item",
"display_name": "Ribbon",
"translation_key": "item.tiedup.ribbon",
"model": "tiedup:models/gltf/v2/binds/ribbon.glb",
"regions": [
"ARMS"
],
"pose_type": "STANDARD",
"pose_priority": 30,
"escape_difficulty": 100,
"lockable": true,
"can_attach_padlock": true,
"supports_color": false,
"animation_bones": {
"idle": [
"rightArm",
"leftArm"
],
"struggle": [
"rightArm",
"leftArm"
]
},
"components": {
"lockable": {},
"resistance": {
"id": "ribbon"
}
}
}

Some files were not shown because too many files have changed in this diff Show More