Merge pull request 'feature/d01-branch-a-bridge' (#6) from feature/d01-branch-a-bridge into develop
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
@@ -14,8 +14,10 @@ import com.tiedup.remake.tasks.TyingTask;
|
||||
import com.tiedup.remake.tasks.V2TyingPlayerTask;
|
||||
import com.tiedup.remake.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());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {} -> {})",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.tiedup.remake.v2.bondage;
|
||||
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||
import java.util.Map;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Static utilities for bind mode operations on any bondage item stack (V1 or V2).
|
||||
*
|
||||
* <p>Bind mode determines whether a bind restrains arms, legs, or both.
|
||||
* The mode is stored in the stack's NBT tag {@code "bindMode"}.</p>
|
||||
*/
|
||||
public final class BindModeHelper {
|
||||
|
||||
private BindModeHelper() {}
|
||||
|
||||
private static final String NBT_BIND_MODE = "bindMode";
|
||||
|
||||
public static final String MODE_FULL = "full";
|
||||
public static final String MODE_ARMS = "arms";
|
||||
public static final String MODE_LEGS = "legs";
|
||||
|
||||
private static final String[] MODE_CYCLE = { MODE_FULL, MODE_ARMS, MODE_LEGS };
|
||||
|
||||
private static final Map<String, String> MODE_TRANSLATION_KEYS = Map.of(
|
||||
MODE_FULL, "tiedup.bindmode.full",
|
||||
MODE_ARMS, "tiedup.bindmode.arms",
|
||||
MODE_LEGS, "tiedup.bindmode.legs"
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if the given stack is a bind item (V2 ARMS-region item or V1 ItemBind).
|
||||
*/
|
||||
public static boolean isBindItem(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
// V2: check data-driven definition
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||
if (def != null) {
|
||||
return def.occupiedRegions().contains(BodyRegionV2.ARMS);
|
||||
}
|
||||
// V1 fallback
|
||||
return stack.getItem() instanceof ItemBind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bind mode ID from the stack's NBT.
|
||||
* @return "full", "arms", or "legs" (defaults to "full")
|
||||
*/
|
||||
public static String getBindModeId(ItemStack stack) {
|
||||
if (stack.isEmpty()) return MODE_FULL;
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains(NBT_BIND_MODE)) return MODE_FULL;
|
||||
String value = tag.getString(NBT_BIND_MODE);
|
||||
if (MODE_ARMS.equals(value) || MODE_LEGS.equals(value)) return value;
|
||||
return MODE_FULL;
|
||||
}
|
||||
|
||||
/** True if arms are restrained (mode is "arms" or "full"). */
|
||||
public static boolean hasArmsBound(ItemStack stack) {
|
||||
String mode = getBindModeId(stack);
|
||||
return MODE_ARMS.equals(mode) || MODE_FULL.equals(mode);
|
||||
}
|
||||
|
||||
/** True if legs are restrained (mode is "legs" or "full"). */
|
||||
public static boolean hasLegsBound(ItemStack stack) {
|
||||
String mode = getBindModeId(stack);
|
||||
return MODE_LEGS.equals(mode) || MODE_FULL.equals(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle bind mode: full → arms → legs → full.
|
||||
* @return the new mode ID
|
||||
*/
|
||||
public static String cycleBindModeId(ItemStack stack) {
|
||||
String current = getBindModeId(stack);
|
||||
String next = MODE_FULL;
|
||||
for (int i = 0; i < MODE_CYCLE.length; i++) {
|
||||
if (MODE_CYCLE[i].equals(current)) {
|
||||
next = MODE_CYCLE[(i + 1) % MODE_CYCLE.length];
|
||||
break;
|
||||
}
|
||||
}
|
||||
stack.getOrCreateTag().putString(NBT_BIND_MODE, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Get the translation key for the current bind mode. */
|
||||
public static String getBindModeTranslationKey(ItemStack stack) {
|
||||
return MODE_TRANSLATION_KEYS.getOrDefault(getBindModeId(stack), "tiedup.bindmode.full");
|
||||
}
|
||||
}
|
||||
371
src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java
Normal file
371
src/main/java/com/tiedup/remake/v2/bondage/CollarHelper.java
Normal file
@@ -0,0 +1,371 @@
|
||||
package com.tiedup.remake.v2.bondage;
|
||||
|
||||
import com.tiedup.remake.items.ItemChokeCollar;
|
||||
import com.tiedup.remake.items.ItemGpsCollar;
|
||||
import com.tiedup.remake.items.ItemShockCollar;
|
||||
import com.tiedup.remake.items.ItemShockCollarAuto;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.v2.bondage.component.ChokingComponent;
|
||||
import com.tiedup.remake.v2.bondage.component.ComponentType;
|
||||
import com.tiedup.remake.v2.bondage.component.GpsComponent;
|
||||
import com.tiedup.remake.v2.bondage.component.OwnershipComponent;
|
||||
import com.tiedup.remake.v2.bondage.component.ShockComponent;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Static utility for collar operations bridging V1 (ItemCollar subclasses)
|
||||
* and V2 (data-driven items with OwnershipComponent).
|
||||
*/
|
||||
public final class CollarHelper {
|
||||
|
||||
private CollarHelper() {}
|
||||
|
||||
// ===== DETECTION =====
|
||||
|
||||
// True if the stack is any kind of collar (V2 ownership component or V1 ItemCollar)
|
||||
public static boolean isCollar(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
if (DataDrivenBondageItem.getComponent(stack, ComponentType.OWNERSHIP, OwnershipComponent.class) != null) {
|
||||
return true;
|
||||
}
|
||||
return stack.getItem() instanceof ItemCollar;
|
||||
}
|
||||
|
||||
// ===== OWNERSHIP (NBT: "owners") =====
|
||||
|
||||
// Returns all owner UUIDs stored in the collar's "owners" ListTag
|
||||
public static List<UUID> getOwners(ItemStack stack) {
|
||||
return getListUUIDs(stack, "owners");
|
||||
}
|
||||
|
||||
// True if the given UUID is in the owners list
|
||||
public static boolean isOwner(ItemStack stack, UUID uuid) {
|
||||
return hasUUIDInList(stack, "owners", uuid);
|
||||
}
|
||||
|
||||
// True if the given player is an owner
|
||||
public static boolean isOwner(ItemStack stack, Player player) {
|
||||
return isOwner(stack, player.getUUID());
|
||||
}
|
||||
|
||||
// True if the collar has at least one owner
|
||||
public static boolean hasOwner(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains("owners", Tag.TAG_LIST)) return false;
|
||||
return !tag.getList("owners", Tag.TAG_COMPOUND).isEmpty();
|
||||
}
|
||||
|
||||
// Adds an owner entry {uuid, name} to the "owners" ListTag
|
||||
public static void addOwner(ItemStack stack, UUID uuid, String name) {
|
||||
addToList(stack, "owners", uuid, name);
|
||||
}
|
||||
|
||||
// Convenience: add a player as owner
|
||||
public static void addOwner(ItemStack stack, Player player) {
|
||||
addOwner(stack, player.getUUID(), player.getGameProfile().getName());
|
||||
}
|
||||
|
||||
// Removes an owner by UUID
|
||||
public static void removeOwner(ItemStack stack, UUID uuid) {
|
||||
removeFromList(stack, "owners", uuid);
|
||||
}
|
||||
|
||||
// ===== BLACKLIST (NBT: "blacklist") =====
|
||||
|
||||
public static List<UUID> getBlacklist(ItemStack stack) {
|
||||
return getListUUIDs(stack, "blacklist");
|
||||
}
|
||||
|
||||
public static boolean isBlacklisted(ItemStack stack, UUID uuid) {
|
||||
return hasUUIDInList(stack, "blacklist", uuid);
|
||||
}
|
||||
|
||||
public static void addToBlacklist(ItemStack stack, UUID uuid, String name) {
|
||||
addToList(stack, "blacklist", uuid, name);
|
||||
}
|
||||
|
||||
public static void removeFromBlacklist(ItemStack stack, UUID uuid) {
|
||||
removeFromList(stack, "blacklist", uuid);
|
||||
}
|
||||
|
||||
// ===== WHITELIST (NBT: "whitelist") =====
|
||||
|
||||
public static List<UUID> getWhitelist(ItemStack stack) {
|
||||
return getListUUIDs(stack, "whitelist");
|
||||
}
|
||||
|
||||
public static boolean isWhitelisted(ItemStack stack, UUID uuid) {
|
||||
return hasUUIDInList(stack, "whitelist", uuid);
|
||||
}
|
||||
|
||||
public static void addToWhitelist(ItemStack stack, UUID uuid, String name) {
|
||||
addToList(stack, "whitelist", uuid, name);
|
||||
}
|
||||
|
||||
public static void removeFromWhitelist(ItemStack stack, UUID uuid) {
|
||||
removeFromList(stack, "whitelist", uuid);
|
||||
}
|
||||
|
||||
// ===== LIST INTERNALS =====
|
||||
|
||||
private static List<UUID> getListUUIDs(ItemStack stack, String listKey) {
|
||||
List<UUID> result = new ArrayList<>();
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return result;
|
||||
ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
CompoundTag entry = list.getCompound(i);
|
||||
if (entry.contains("uuid")) {
|
||||
try {
|
||||
result.add(UUID.fromString(entry.getString("uuid")));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// Malformed UUID in NBT, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean hasUUIDInList(ItemStack stack, String listKey, UUID uuid) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return false;
|
||||
String uuidStr = uuid.toString();
|
||||
ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (uuidStr.equals(list.getCompound(i).getString("uuid"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void addToList(ItemStack stack, String listKey, UUID uuid, String name) {
|
||||
CompoundTag tag = stack.getOrCreateTag();
|
||||
ListTag list = tag.contains(listKey, Tag.TAG_LIST)
|
||||
? tag.getList(listKey, Tag.TAG_COMPOUND)
|
||||
: new ListTag();
|
||||
// Prevent duplicates
|
||||
String uuidStr = uuid.toString();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (uuidStr.equals(list.getCompound(i).getString("uuid"))) return;
|
||||
}
|
||||
CompoundTag entry = new CompoundTag();
|
||||
entry.putString("uuid", uuidStr);
|
||||
entry.putString("name", name);
|
||||
list.add(entry);
|
||||
tag.put(listKey, list);
|
||||
}
|
||||
|
||||
private static void removeFromList(ItemStack stack, String listKey, UUID uuid) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains(listKey, Tag.TAG_LIST)) return;
|
||||
String uuidStr = uuid.toString();
|
||||
ListTag list = tag.getList(listKey, Tag.TAG_COMPOUND);
|
||||
list.removeIf(element ->
|
||||
uuidStr.equals(((CompoundTag) element).getString("uuid"))
|
||||
);
|
||||
}
|
||||
|
||||
// ===== FEATURES =====
|
||||
|
||||
@Nullable
|
||||
public static String getNickname(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains("nickname")) return null;
|
||||
return tag.getString("nickname");
|
||||
}
|
||||
|
||||
public static void setNickname(ItemStack stack, String nickname) {
|
||||
stack.getOrCreateTag().putString("nickname", nickname);
|
||||
}
|
||||
|
||||
public static boolean hasNickname(ItemStack stack) {
|
||||
return stack.hasTag() && stack.getTag().contains("nickname");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static UUID getCellId(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains("cellId")) return null;
|
||||
try {
|
||||
return UUID.fromString(tag.getString("cellId"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setCellId(ItemStack stack, UUID cellId) {
|
||||
stack.getOrCreateTag().putString("cellId", cellId.toString());
|
||||
}
|
||||
|
||||
public static boolean hasCellAssigned(ItemStack stack) {
|
||||
return getCellId(stack) != null;
|
||||
}
|
||||
|
||||
public static boolean isKidnappingModeEnabled(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean("kidnappingMode");
|
||||
}
|
||||
|
||||
public static void setKidnappingModeEnabled(ItemStack stack, boolean enabled) {
|
||||
stack.getOrCreateTag().putBoolean("kidnappingMode", enabled);
|
||||
}
|
||||
|
||||
// Kidnapping mode is ready when enabled AND a cell is assigned
|
||||
public static boolean isKidnappingModeReady(ItemStack stack) {
|
||||
return isKidnappingModeEnabled(stack) && hasCellAssigned(stack);
|
||||
}
|
||||
|
||||
public static boolean shouldTieToPole(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean("tieToPole");
|
||||
}
|
||||
|
||||
public static void setShouldTieToPole(ItemStack stack, boolean value) {
|
||||
stack.getOrCreateTag().putBoolean("tieToPole", value);
|
||||
}
|
||||
|
||||
// Default true when tag is absent or key is missing
|
||||
public static boolean shouldWarnMasters(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains("warnMasters")) return true;
|
||||
return tag.getBoolean("warnMasters");
|
||||
}
|
||||
|
||||
public static void setShouldWarnMasters(ItemStack stack, boolean value) {
|
||||
stack.getOrCreateTag().putBoolean("warnMasters", value);
|
||||
}
|
||||
|
||||
public static boolean isBondageServiceEnabled(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean("bondageservice");
|
||||
}
|
||||
|
||||
public static void setBondageServiceEnabled(ItemStack stack, boolean enabled) {
|
||||
stack.getOrCreateTag().putBoolean("bondageservice", enabled);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getServiceSentence(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains("servicesentence")) return null;
|
||||
return tag.getString("servicesentence");
|
||||
}
|
||||
|
||||
public static void setServiceSentence(ItemStack stack, String sentence) {
|
||||
stack.getOrCreateTag().putString("servicesentence", sentence);
|
||||
}
|
||||
|
||||
// ===== SHOCK =====
|
||||
|
||||
// True if the collar can shock (V2 ShockComponent or V1 ItemShockCollar)
|
||||
public static boolean canShock(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
if (DataDrivenBondageItem.getComponent(stack, ComponentType.SHOCK, ShockComponent.class) != null) {
|
||||
return true;
|
||||
}
|
||||
return stack.getItem() instanceof ItemShockCollar;
|
||||
}
|
||||
|
||||
public static boolean isPublicShock(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean("public_mode");
|
||||
}
|
||||
|
||||
public static void setPublicShock(ItemStack stack, boolean publicMode) {
|
||||
stack.getOrCreateTag().putBoolean("public_mode", publicMode);
|
||||
}
|
||||
|
||||
// V2: from ShockComponent auto interval, V1: from ItemShockCollarAuto field, else 0
|
||||
public static int getShockInterval(ItemStack stack) {
|
||||
ShockComponent comp = DataDrivenBondageItem.getComponent(
|
||||
stack, ComponentType.SHOCK, ShockComponent.class
|
||||
);
|
||||
if (comp != null) return comp.getAutoInterval();
|
||||
if (stack.getItem() instanceof ItemShockCollarAuto auto) {
|
||||
return auto.getInterval();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ===== GPS =====
|
||||
|
||||
// True if the collar has GPS capabilities
|
||||
public static boolean hasGPS(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
if (DataDrivenBondageItem.getComponent(stack, ComponentType.GPS, GpsComponent.class) != null) {
|
||||
return true;
|
||||
}
|
||||
return stack.getItem() instanceof ItemGpsCollar;
|
||||
}
|
||||
|
||||
public static boolean hasPublicTracking(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean("publicTracking");
|
||||
}
|
||||
|
||||
public static void setPublicTracking(ItemStack stack, boolean publicTracking) {
|
||||
stack.getOrCreateTag().putBoolean("publicTracking", publicTracking);
|
||||
}
|
||||
|
||||
// GPS active defaults to true when absent
|
||||
public static boolean isActive(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
if (tag == null || !tag.contains("gpsActive")) return true;
|
||||
return tag.getBoolean("gpsActive");
|
||||
}
|
||||
|
||||
public static void setActive(ItemStack stack, boolean active) {
|
||||
stack.getOrCreateTag().putBoolean("gpsActive", active);
|
||||
}
|
||||
|
||||
// ===== CHOKE =====
|
||||
|
||||
// True if the collar is a choke collar
|
||||
public static boolean isChokeCollar(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
if (DataDrivenBondageItem.getComponent(stack, ComponentType.CHOKING, ChokingComponent.class) != null) {
|
||||
return true;
|
||||
}
|
||||
return stack.getItem() instanceof ItemChokeCollar;
|
||||
}
|
||||
|
||||
public static boolean isChoking(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean("choking");
|
||||
}
|
||||
|
||||
public static void setChoking(ItemStack stack, boolean choking) {
|
||||
stack.getOrCreateTag().putBoolean("choking", choking);
|
||||
}
|
||||
|
||||
public static boolean isPetPlayMode(ItemStack stack) {
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean("petPlayMode");
|
||||
}
|
||||
|
||||
public static void setPetPlayMode(ItemStack stack, boolean petPlay) {
|
||||
stack.getOrCreateTag().putBoolean("petPlayMode", petPlay);
|
||||
}
|
||||
|
||||
// ===== ALERT SUPPRESSION =====
|
||||
|
||||
// Executes the action with collar removal alerts suppressed
|
||||
public static void runWithSuppressedAlert(Runnable action) {
|
||||
ItemCollar.runWithSuppressedAlert(action);
|
||||
}
|
||||
|
||||
// True if removal alerts are currently suppressed (ThreadLocal state)
|
||||
public static boolean isRemovalAlertSuppressed() {
|
||||
return ItemCollar.isRemovalAlertSuppressed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.tiedup.remake.v2.bondage;
|
||||
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.items.base.PoseType;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Resolves the {@link PoseType} for any bondage item stack (V1 or V2).
|
||||
*
|
||||
* <p>V2 items read from the data-driven definition's {@code pose_type} field.
|
||||
* V1 items fall back to {@code ItemBind.getPoseType()}.</p>
|
||||
*/
|
||||
public final class PoseTypeHelper {
|
||||
|
||||
private PoseTypeHelper() {}
|
||||
|
||||
public static PoseType getPoseType(ItemStack stack) {
|
||||
// V2: read from data-driven definition
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||
if (def != null && def.poseType() != null) {
|
||||
try {
|
||||
return PoseType.valueOf(def.poseType().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return PoseType.STANDARD;
|
||||
}
|
||||
}
|
||||
// V1 fallback
|
||||
if (stack.getItem() instanceof ItemBind bind) {
|
||||
return bind.getPoseType();
|
||||
}
|
||||
return PoseType.STANDARD;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.tiedup.remake.v2.bondage;
|
||||
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.tasks.TyingTask;
|
||||
import com.tiedup.remake.tasks.V2TyingPlayerTask;
|
||||
import com.tiedup.remake.util.KidnappedHelper;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import com.tiedup.remake.v2.bondage.V2EquipResult;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Handles the tying interaction flow for V2 data-driven ARMS items.
|
||||
*
|
||||
* <p>Extracted from {@code ItemBind.interactLivingEntity()} to support
|
||||
* the same tying task flow for data-driven items.</p>
|
||||
*/
|
||||
public final class TyingInteractionHelper {
|
||||
|
||||
private TyingInteractionHelper() {}
|
||||
|
||||
/**
|
||||
* Handle right-click tying of a target entity with a V2 ARMS item.
|
||||
* Creates/continues a V2TyingPlayerTask.
|
||||
*/
|
||||
public static InteractionResult handleTying(
|
||||
ServerPlayer player,
|
||||
LivingEntity target,
|
||||
ItemStack stack,
|
||||
InteractionHand hand
|
||||
) {
|
||||
IBondageState targetState = KidnappedHelper.getKidnappedState(target);
|
||||
if (targetState == null) return InteractionResult.PASS;
|
||||
|
||||
// Kidnapper can't be tied themselves
|
||||
IBondageState kidnapperState = KidnappedHelper.getKidnappedState(player);
|
||||
if (kidnapperState != null && kidnapperState.isTiedUp()) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
// Already tied — try to swap (check both V1 state and V2 region to prevent dual-bind)
|
||||
if (targetState.isTiedUp() || V2EquipmentHelper.isRegionOccupied(target, BodyRegionV2.ARMS)) {
|
||||
if (stack.isEmpty()) return InteractionResult.PASS;
|
||||
ItemStack oldBind = V2EquipmentHelper.unequipFromRegion(target, BodyRegionV2.ARMS);
|
||||
if (!oldBind.isEmpty()) {
|
||||
V2EquipResult result = V2EquipmentHelper.equipItem(target, stack.copy());
|
||||
if (result.isSuccess()) {
|
||||
stack.shrink(1);
|
||||
target.spawnAtLocation(oldBind);
|
||||
TiedUpMod.LOGGER.debug("[TyingInteraction] Swapped bind on {}", target.getName().getString());
|
||||
return InteractionResult.SUCCESS;
|
||||
} else {
|
||||
// Equip failed — rollback: re-equip old bind
|
||||
V2EquipResult rollback = V2EquipmentHelper.equipItem(target, oldBind);
|
||||
if (!rollback.isSuccess()) {
|
||||
// Rollback also failed — drop old bind as safety net
|
||||
target.spawnAtLocation(oldBind);
|
||||
TiedUpMod.LOGGER.warn("[TyingInteraction] Swap AND rollback failed, dropped old bind for {}", target.getName().getString());
|
||||
} else {
|
||||
TiedUpMod.LOGGER.debug("[TyingInteraction] Swap failed, rolled back old bind on {}", target.getName().getString());
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
}
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
|
||||
// Distance + line-of-sight (skip for self-tying)
|
||||
boolean isSelfTying = player.equals(target);
|
||||
if (!isSelfTying) {
|
||||
if (player.distanceTo(target) > 4.0 || !player.hasLineOfSight(target)) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
}
|
||||
|
||||
// Create/continue tying task
|
||||
PlayerBindState playerState = PlayerBindState.getInstance(player);
|
||||
if (playerState == null) return InteractionResult.PASS;
|
||||
|
||||
int tyingSeconds = SettingsAccessor.getTyingPlayerTime(player.level().getGameRules());
|
||||
|
||||
V2TyingPlayerTask newTask = new V2TyingPlayerTask(
|
||||
stack.copy(),
|
||||
stack,
|
||||
targetState,
|
||||
target,
|
||||
tyingSeconds,
|
||||
player.level(),
|
||||
player
|
||||
);
|
||||
|
||||
TyingTask currentTask = playerState.getCurrentTyingTask();
|
||||
if (currentTask == null
|
||||
|| !(currentTask instanceof V2TyingPlayerTask)
|
||||
|| !currentTask.isSameTarget(target)
|
||||
|| currentTask.isOutdated()
|
||||
|| !ItemStack.matches(currentTask.getBind(), stack)) {
|
||||
// Start new task (also handles case where existing task is V1 TyingPlayerTask)
|
||||
playerState.setCurrentTyingTask(newTask);
|
||||
newTask.start();
|
||||
} else {
|
||||
newTask = (V2TyingPlayerTask) currentTask;
|
||||
}
|
||||
|
||||
newTask.update();
|
||||
|
||||
if (newTask.isStopped()) {
|
||||
// Item already consumed by V2TyingPlayerTask.onComplete() — don't shrink again
|
||||
playerState.setCurrentTyingTask(null);
|
||||
TiedUpMod.LOGGER.info("[TyingInteraction] {} tied {}", player.getName().getString(), target.getName().getString());
|
||||
}
|
||||
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.state.CollarRegistry;
|
||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Component: collar ownership behavior for data-driven items.
|
||||
*
|
||||
* <p>Marks an item as a collar with ownership capabilities.
|
||||
* Lifecycle hooks handle CollarRegistry registration/unregistration.</p>
|
||||
*
|
||||
* <p>JSON config: {@code "ownership": {}}</p>
|
||||
*/
|
||||
public class OwnershipComponent implements IItemComponent {
|
||||
|
||||
private OwnershipComponent() {}
|
||||
|
||||
public static IItemComponent fromJson(JsonObject config) {
|
||||
return new OwnershipComponent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEquipped(ItemStack stack, LivingEntity entity) {
|
||||
if (entity.level().isClientSide()) return;
|
||||
if (!(entity.level() instanceof ServerLevel serverLevel)) return;
|
||||
|
||||
List<UUID> owners = CollarHelper.getOwners(stack);
|
||||
if (owners.isEmpty()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[OwnershipComponent] Collar equipped on {} with no owners in NBT — skipping registry",
|
||||
entity.getName().getString()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
CollarRegistry registry = CollarRegistry.get(serverLevel);
|
||||
registry.registerCollar(entity.getUUID(), new HashSet<>(owners));
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[OwnershipComponent] Registered collar for {} with {} owner(s)",
|
||||
entity.getName().getString(), owners.size()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[OwnershipComponent] Failed to register collar for {}: {}",
|
||||
entity.getName().getString(), e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnequipped(ItemStack stack, LivingEntity entity) {
|
||||
if (entity.level().isClientSide()) return;
|
||||
if (!(entity.level() instanceof ServerLevel serverLevel)) return;
|
||||
|
||||
// Alert kidnappers + unregister from CollarRegistry
|
||||
// onCollarRemoved handles both the alert AND the unregister call internally,
|
||||
// so we do NOT call registry.unregisterWearer() separately to avoid double unregister.
|
||||
if (!CollarHelper.isRemovalAlertSuppressed()) {
|
||||
ItemCollar.onCollarRemoved(entity, true);
|
||||
} else {
|
||||
// Suppressed alert path: still need to unregister, just skip the alert
|
||||
try {
|
||||
CollarRegistry registry = CollarRegistry.get(serverLevel);
|
||||
registry.unregisterWearer(entity.getUUID());
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[OwnershipComponent] Unregistered collar for {} (alert suppressed)",
|
||||
entity.getName().getString()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[OwnershipComponent] Failed to unregister collar for {}: {}",
|
||||
entity.getName().getString(), e.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendTooltip(ItemStack stack, @Nullable Level level, List<Component> tooltip, TooltipFlag flag) {
|
||||
String nickname = CollarHelper.getNickname(stack);
|
||||
if (nickname != null && !nickname.isEmpty()) {
|
||||
tooltip.add(Component.translatable("item.tiedup.tooltip.nickname", nickname)
|
||||
.withStyle(ChatFormatting.LIGHT_PURPLE));
|
||||
}
|
||||
|
||||
List<UUID> owners = CollarHelper.getOwners(stack);
|
||||
if (!owners.isEmpty()) {
|
||||
tooltip.add(Component.translatable("item.tiedup.tooltip.owners", owners.size())
|
||||
.withStyle(ChatFormatting.GOLD));
|
||||
}
|
||||
|
||||
if (CollarHelper.canShock(stack)) {
|
||||
tooltip.add(Component.translatable("item.tiedup.tooltip.shock_capable")
|
||||
.withStyle(ChatFormatting.DARK_RED));
|
||||
}
|
||||
|
||||
if (CollarHelper.hasGPS(stack)) {
|
||||
tooltip.add(Component.translatable("item.tiedup.tooltip.gps_capable")
|
||||
.withStyle(ChatFormatting.AQUA));
|
||||
}
|
||||
|
||||
if (CollarHelper.isChokeCollar(stack)) {
|
||||
tooltip.add(Component.translatable("item.tiedup.tooltip.choke_capable")
|
||||
.withStyle(ChatFormatting.DARK_PURPLE));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,70 @@
|
||||
package com.tiedup.remake.v2.bondage.component;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -328,6 +331,7 @@ public final class DataDrivenItemParser {
|
||||
animationSource,
|
||||
occupiedRegions,
|
||||
blockedRegions,
|
||||
poseType,
|
||||
posePriority,
|
||||
escapeDifficulty,
|
||||
lockable,
|
||||
|
||||
Reference in New Issue
Block a user