Clean repo for open source release

Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,295 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.LeashProxyEntity;
import com.tiedup.remake.state.IPlayerLeashAccess;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import net.minecraft.world.entity.player.Player;
import org.jetbrains.annotations.Nullable;
/**
* Component responsible for captivity mechanics and leash proxy management.
* Phase 17: Advanced capture system (proxy-based leashing)
*
* Single Responsibility: Captivity lifecycle and transport management
* Complexity: VERY HIGH (mixin coupling, network sync, leash proxy coordination)
* Risk: VERY HIGH (critical path, mixin dependency, 4 network sync points)
*
* Mixin Dependency: IPlayerLeashAccess for leash proxy system
* Network Sync: 4 syncEnslavement() calls coordinated via host
*
* Captivity States:
* - Not Captive: No captor, no leash
* - Captive (by entity): Has captor, leashed to entity
* - Pole Binding: No captor, leashed to pole (LeashFenceKnotEntity)
*/
public class PlayerCaptivity {
private final IPlayerBindStateHost host;
public PlayerCaptivity(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Captivity Initiation ==========
/**
* Phase 17: Renamed from getEnslavedBy to getCapturedBy
* Initiates the capture process by a captor.
* Uses the proxy-based leash system (player is NOT mounted).
*
* Thread Safety: Synchronized to prevent race condition where two kidnappers
* could both pass the isCaptive() check and attempt to capture simultaneously.
*
* @param newCaptor The entity attempting to capture this player
* @return true if capture succeeded
*/
public synchronized boolean getCapturedBy(ICaptor newCaptor) {
Player player = host.getPlayer();
if (player == null || newCaptor == null) return false;
// Must be enslavable (tied up) OR captor can capture (includes collar owner exception)
if (
!isEnslavable() && !newCaptor.canCapture(host.getKidnapped())
) return false;
// Check if already captured (atomic check under synchronization)
if (isCaptive()) return false;
// Free all captives instead of transferring (until multi-captive is implemented)
if (host.getCaptorManager().hasCaptives()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] {} is being captured - freeing their {} captives",
player.getName().getString(),
host.getCaptorManager().getCaptiveCount()
);
host.getCaptorManager().freeAllCaptives(true);
}
// Use new proxy-based leash system
if (player instanceof IPlayerLeashAccess access) {
access.tiedup$attachLeash(newCaptor.getEntity());
newCaptor.addCaptive(host.getKidnapped());
host.setCaptor(newCaptor);
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] {} captured by {} (proxy leash)",
player.getName().getString(),
newCaptor.getEntity().getName().getString()
);
// Sync enslavement state to all clients
host.syncEnslavement();
return true;
}
TiedUpMod.LOGGER.error(
"[PlayerCaptivity] Player {} does not implement IPlayerLeashAccess!",
player.getName().getString()
);
return false;
}
// ========== Captivity Release ==========
/**
* Ends captivity with default behavior (drops leash item).
*/
public void free() {
free(true);
}
/**
* Phase 17: Ends captivity and detaches the leash proxy.
*
* @param dropLead Whether to drop the leash item
*/
public void free(boolean dropLead) {
Player player = host.getPlayer();
if (player == null) return;
if (!(player instanceof IPlayerLeashAccess access)) {
TiedUpMod.LOGGER.error(
"[PlayerCaptivity] Player {} does not implement IPlayerLeashAccess!",
player.getName().getString()
);
return;
}
ICaptor captor = host.getCaptor();
// Handle pole binding (no captor) - just detach leash
if (captor == null) {
if (access.tiedup$isLeashed()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Freeing {} from pole binding",
player.getName().getString()
);
if (dropLead) {
access.tiedup$dropLeash();
}
access.tiedup$detachLeash();
host.syncEnslavement();
}
return;
}
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Freeing {} from captivity",
player.getName().getString()
);
// 1. Remove from captor's tracking list
captor.removeCaptive(host.getKidnapped(), false);
// 2. Detach leash proxy
if (dropLead) {
access.tiedup$dropLeash();
}
access.tiedup$detachLeash();
// 3. Reset state
host.setCaptor(null);
// 4. Sync freed state to all clients
host.syncEnslavement();
}
// ========== Captivity Transfer ==========
/**
* Phase 17: Renamed from transferSlaveryTo to transferCaptivityTo
* Transfers captivity from current captor to a new captor.
*
* Thread Safety: Synchronized to prevent concurrent transfer attempts.
*
* @param newCaptor The new captor entity
*/
public synchronized void transferCaptivityTo(ICaptor newCaptor) {
Player player = host.getPlayer();
ICaptor currentCaptor = host.getCaptor();
if (
player == null ||
newCaptor == null ||
currentCaptor == null ||
!currentCaptor.allowCaptiveTransfer()
) return;
currentCaptor.removeCaptive(host.getKidnapped(), false);
// Re-attach leash to new captor
if (player instanceof IPlayerLeashAccess access) {
access.tiedup$detachLeash();
access.tiedup$attachLeash(newCaptor.getEntity());
}
newCaptor.addCaptive(host.getKidnapped());
host.setCaptor(newCaptor);
// Sync new captor to all clients
host.syncEnslavement();
}
// ========== State Queries ==========
/**
* Check if this player can be captured (leashed).
* Must be tied up to be leashed.
* Collar alone is NOT enough - collar owner exception is handled in canCapture().
*
* @return true if player is tied up and can be leashed
*/
public boolean isEnslavable() {
return host.isTiedUp();
}
/**
* Phase 17: Renamed from isSlave to isCaptive
* Check if player is currently captured by an entity.
*
* @return true if player has a captor and is leashed
*/
public boolean isCaptive() {
Player player = host.getPlayer();
if (host.getCaptor() == null) return false;
if (player instanceof IPlayerLeashAccess access) {
return access.tiedup$isLeashed();
}
return false;
}
/**
* Get the leash proxy entity (transport system).
* Phase 17: Proxy-based leashing (no mounting).
*
* @return The leash proxy, or null if not leashed
*/
@Nullable
public LeashProxyEntity getTransport() {
Player player = host.getPlayer();
if (player instanceof IPlayerLeashAccess access) {
return access.tiedup$getLeashProxy();
}
return null;
}
// ========== Captivity Monitoring ==========
/**
* Phase 17: Renamed from checkStillSlave to checkStillCaptive
* Periodically monitors captivity validity.
* Simplified: If any condition is invalid, free the captive immediately.
*
* Called from RestraintTaskTickHandler every player tick.
*/
public void checkStillCaptive() {
if (!isCaptive()) return;
Player player = host.getPlayer();
if (player == null) return;
// Check if no longer tied/collared
if (!host.isTiedUp() && !host.hasCollar()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Auto-freeing {} - no restraints",
player.getName().getString()
);
free();
return;
}
// Check leash proxy status
if (player instanceof IPlayerLeashAccess access) {
LeashProxyEntity proxy = access.tiedup$getLeashProxy();
if (proxy == null || proxy.proxyIsRemoved()) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Auto-freeing {} - proxy invalid",
player.getName().getString()
);
// Notify captor BEFORE freeing (triggers retrieval behavior)
ICaptor captor = host.getCaptor();
if (captor != null) {
captor.onCaptiveReleased(host.getKidnapped());
}
free();
return;
}
// Check if leash holder is still valid
if (proxy.getLeashHolder() == null) {
TiedUpMod.LOGGER.debug(
"[PlayerCaptivity] Auto-freeing {} - leash holder gone",
player.getName().getString()
);
// Notify captor BEFORE freeing (triggers retrieval behavior)
ICaptor captor = host.getCaptor();
if (captor != null) {
captor.onCaptiveReleased(host.getKidnapped());
}
free();
}
}
}
}

View File

@@ -0,0 +1,149 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for clothes permission and management.
* Handles who can remove/change clothes and manages clothes replacement with network sync.
*
* Single Responsibility: Clothes permissions and synchronization
* Complexity: MEDIUM (network sync coordination)
* Risk: MEDIUM (must coordinate SyncManager calls)
*
* Epic 5F: Uses V2EquipmentHelper/BodyRegionV2.
*/
public class PlayerClothesPermission {
private final IPlayerBindStateHost host;
public PlayerClothesPermission(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Permission Checks ==========
/**
* Check if a specific player can take off this player's clothes.
* Currently permissive - anyone can take off clothes.
* Future: Could check if 'player' is owner/master when tied up.
*
* @param player The player attempting to remove clothes
* @return true if allowed
*/
public boolean canTakeOffClothes(Player player) {
// Currently permissive - anyone can take off clothes
// Future: Could check if 'player' is owner/master when tied up
return true;
}
/**
* Check if a specific player can change this player's clothes.
* Currently permissive - anyone can change clothes.
* Future: Could check if 'player' is owner/master when tied up.
*
* @param player The player attempting to change clothes
* @return true if allowed
*/
public boolean canChangeClothes(Player player) {
// Currently permissive - anyone can change clothes
// Future: Could check if 'player' is owner/master when tied up
return true;
}
/**
* Check if clothes can be changed (no specific player context).
* Checks if no clothes are equipped, or if clothes are not locked.
*
* @return true if clothes can be changed
*/
public boolean canChangeClothes() {
ItemStack clothes = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.TORSO);
if (clothes.isEmpty()) return true;
// Check if clothes are locked
return !isLocked(clothes, false);
}
// ========== Clothes Management ==========
/**
* Replace current clothes with new clothes.
* Removes old clothes and equips new ones.
* Syncs clothes config to all clients.
*
* @param newClothes The new clothes to equip
* @return The old clothes, or empty if none
*/
public ItemStack replaceClothes(ItemStack newClothes) {
return replaceClothes(newClothes, false);
}
/**
* Replace current clothes with new clothes, with optional force.
* If force is true, bypasses lock checks.
* Syncs clothes config to all clients.
*
* @param newClothes The new clothes to equip
* @param force true to bypass lock checks
* @return The old clothes, or empty if none
*/
public ItemStack replaceClothes(ItemStack newClothes, boolean force) {
Player player = host.getPlayer();
if (player == null || player.level().isClientSide) return ItemStack.EMPTY;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return ItemStack.EMPTY;
// Take off old clothes
ItemStack old = V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.TORSO);
// Equip new clothes if we successfully removed old ones
if (!old.isEmpty()) {
equip.setInRegion(BodyRegionV2.TORSO, newClothes.copy());
// Fire lifecycle hook for new item
if (!newClothes.isEmpty() && newClothes.getItem() instanceof IV2BondageItem newItem) {
newItem.onEquipped(newClothes, player);
}
V2EquipmentHelper.sync(player);
// CRITICAL: Sync clothes config to all tracking clients
// This ensures dynamic textures and other clothes properties are synced
host.syncClothesConfig();
}
return old;
}
// ========== Helper Methods ==========
/**
* Check if an item is locked.
* Helper method for lock checking.
*
* @param stack The item to check
* @param force If true, returns false (bypasses lock)
* @return true if locked and not forced
*/
private boolean isLocked(ItemStack stack, boolean force) {
if (force) return false;
if (stack.isEmpty()) return false;
// Check if item has locked property
if (
stack.getItem() instanceof
com.tiedup.remake.items.base.ILockable lockable
) {
return lockable.isLocked(stack);
}
return false;
}
}

View File

@@ -0,0 +1,113 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for retrieving player data and equipment.
* Provides access to current bondage items and player information.
*
* Single Responsibility: Data retrieval
* Complexity: LOW (simple getters)
* Risk: LOW (read-only access)
*
* Epic 5F: Uses V2EquipmentHelper/BodyRegionV2.
*/
public class PlayerDataRetrieval {
private final IPlayerBindStateHost host;
public PlayerDataRetrieval(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Equipment Getters ==========
public ItemStack getCurrentBind() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.ARMS);
}
public ItemStack getCurrentGag() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
}
public ItemStack getCurrentBlindfold() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
}
public ItemStack getCurrentEarplugs() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EARS);
}
public ItemStack getCurrentClothes() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.TORSO);
}
public ItemStack getCurrentMittens() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.HANDS);
}
public ItemStack getCurrentCollar() {
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
}
// ========== Player Information ==========
/**
* Get the player's display name, checking collar for nickname first.
* If collar has a nickname, returns that; otherwise returns player name.
*/
public String getNameFromCollar() {
Player player = host.getPlayer();
ItemStack collar = getCurrentCollar();
if (
!collar.isEmpty() &&
collar.getItem() instanceof ItemCollar collarItem
) {
// Try to get nickname from collar NBT
String nickname = collarItem.getNickname(collar);
if (nickname != null && !nickname.isEmpty()) {
return nickname;
}
}
// Fallback to player name
return player.getName().getString();
}
/**
* Check if player has a named collar (collar with nickname).
*/
public boolean hasNamedCollar() {
ItemStack collar = getCurrentCollar();
if (collar.isEmpty()) return false;
if (collar.getItem() instanceof ItemCollar collarItem) {
String nickname = collarItem.getNickname(collar);
return nickname != null && !nickname.isEmpty();
}
return false;
}
/**
* Check if player has clothes with small arms flag.
* TODO Phase 14+: Check clothes NBT for small arms flag
*/
public boolean hasClothesWithSmallArms() {
return false;
}
/**
* Get the player as a LivingEntity.
* Used for generic entity operations.
*/
public LivingEntity asLivingEntity() {
return host.getPlayer();
}
}

View File

@@ -0,0 +1,429 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.v2.bondage.IV2BondageItem;
import com.tiedup.remake.items.base.ILockable;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import java.util.function.Supplier;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for bondage equipment management.
* Handles putting on, taking off, and replacing all bondage items.
*
* Single Responsibility: Equipment lifecycle management
* Complexity: MEDIUM (V2 equipment coupling)
* Risk: MEDIUM (core equipment system)
*
* Epic 5F: Uses V2EquipmentHelper.
* Uses low-level setInRegion for equips because V1 items (IBondageItem) do not
* implement IV2BondageItem, so V2EquipmentHelper.equipItem() would reject them.
*/
public class PlayerEquipment {
private final IPlayerBindStateHost host;
public PlayerEquipment(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Put On Methods ==========
/** Equips a bind item and applies speed reduction. */
public void putBindOn(ItemStack bind) {
equipInRegion(BodyRegionV2.ARMS, bind);
}
/** Equips a gag (enables gag talk if implemented). Issue #14 fix: now calls onEquipped. */
public void putGagOn(ItemStack gag) {
equipInRegion(BodyRegionV2.MOUTH, gag);
}
/** Equips a blindfold (restricts vision). Issue #14 fix: now calls onEquipped. */
public void putBlindfoldOn(ItemStack blindfold) {
equipInRegion(BodyRegionV2.EYES, blindfold);
}
/** Equips a collar (starts GPS/Shock monitoring). Issue #14 fix: now calls onEquipped. */
public void putCollarOn(ItemStack collar) {
equipInRegion(BodyRegionV2.NECK, collar);
}
/** Issue #14 fix: now calls onEquipped. Syncs clothes config to all clients. */
public void putClothesOn(ItemStack clothes) {
equipInRegion(BodyRegionV2.TORSO, clothes);
// Sync clothes config (dynamic textures, etc.) to all tracking clients
host.syncClothesConfig();
}
/** Equips mittens (blocks hand interactions). Phase 14.4: Mittens system. Issue #14 fix: now calls onEquipped. */
public void putMittensOn(ItemStack mittens) {
equipInRegion(BodyRegionV2.HANDS, mittens);
checkMittensAfterApply();
}
/** Equips earplugs (muffles sounds). Issue #14 fix: now calls onEquipped. */
public void putEarplugsOn(ItemStack earplugs) {
equipInRegion(BodyRegionV2.EARS, earplugs);
checkEarplugsAfterApply();
}
// ========== Take Off Methods ==========
/** Removes binds and restores speed. */
public ItemStack takeBindOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.ARMS);
}
public ItemStack takeGagOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.MOUTH);
}
public ItemStack takeBlindfoldOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.EYES);
}
public ItemStack takeCollarOff() {
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.NECK);
}
/** Removes mittens. Phase 14.4: Mittens system */
public ItemStack takeMittensOff() {
ItemStack mittens = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.HANDS);
if (isLocked(mittens, false)) {
return ItemStack.EMPTY;
}
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.HANDS);
}
public ItemStack takeEarplugsOff() {
ItemStack earplugs = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EARS);
if (isLocked(earplugs, false)) {
return ItemStack.EMPTY;
}
return V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.EARS);
}
public ItemStack takeClothesOff() {
ItemStack clothes = V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.TORSO);
// Sync clothes removal to all tracking clients
host.syncClothesConfig();
return clothes;
}
/**
* Tries to remove the collar. Fails if locked unless forced.
*/
public ItemStack takeCollarOff(boolean force) {
Player player = host.getPlayer();
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (collar.isEmpty()) return ItemStack.EMPTY;
if (collar.getItem() instanceof ItemCollar collarItem) {
if (!force && collarItem.isLocked(collar)) return ItemStack.EMPTY;
collarItem.setLocked(collar, false);
}
return V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.NECK);
}
// ========== Replacement Methods ==========
/** Replaces the blindfold and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceBlindfold(ItemStack newBlindfold) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.EYES, newBlindfold);
}
/** Replaces the gag and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceGag(ItemStack newGag) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.MOUTH, newGag);
}
/** Replaces the collar and returns the old one. Issue #14 fix: now calls lifecycle hooks. */
public ItemStack replaceCollar(ItemStack newCollar) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
if (current.isEmpty()) return ItemStack.EMPTY;
return replaceInRegion(BodyRegionV2.NECK, newCollar);
}
/**
* Thread Safety: Synchronized to prevent inventory tearing (gap between remove and add).
*/
public synchronized ItemStack replaceBind(ItemStack newBind) {
ItemStack old = takeBindOff();
if (!old.isEmpty()) {
putBindOn(newBind);
}
return old;
}
/**
* Thread Safety: Synchronized to prevent inventory tearing (gap between remove and add).
*/
public synchronized ItemStack replaceBind(
ItemStack newBind,
boolean force
) {
// Safety: Don't remove current bind if newBind is empty
if (newBind.isEmpty()) {
return ItemStack.EMPTY;
}
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.ARMS);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeBindOff();
if (!old.isEmpty()) {
putBindOn(newBind);
}
return old;
}
public ItemStack replaceGag(ItemStack newGag, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.MOUTH);
putGagOn(newGag);
return old;
}
public ItemStack replaceBlindfold(ItemStack newBlindfold, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = V2EquipmentHelper.unequipFromRegion(host.getPlayer(), BodyRegionV2.EYES);
putBlindfoldOn(newBlindfold);
return old;
}
public ItemStack replaceCollar(ItemStack newCollar, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeCollarOff(force);
putCollarOn(newCollar);
return old;
}
public ItemStack replaceEarplugs(ItemStack newEarplugs) {
return replaceEarplugs(newEarplugs, false);
}
public ItemStack replaceEarplugs(ItemStack newEarplugs, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EARS);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeEarplugsOff();
putEarplugsOn(newEarplugs);
return old;
}
public ItemStack replaceMittens(ItemStack newMittens) {
return replaceMittens(newMittens, false);
}
public ItemStack replaceMittens(ItemStack newMittens, boolean force) {
ItemStack current = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.HANDS);
if (isLocked(current, force)) {
return ItemStack.EMPTY;
}
ItemStack old = takeMittensOff();
if (!old.isEmpty() || !hasMittens()) {
putMittensOn(newMittens);
}
return old;
}
// ========== Resistance Methods ==========
/**
* Phase 14.1.7: Now part of IRestrainable interface
*/
public synchronized int getCurrentBindResistance() {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
) return 0;
return bind.getCurrentResistance(stack, player);
}
/**
* Phase 14.1.7: Now part of IRestrainable interface
*/
public synchronized void setCurrentBindResistance(int resistance) {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemBind bind)
) return;
bind.setCurrentResistance(stack, resistance);
}
/**
* Phase 14.1.7: Added for IRestrainable interface
*/
public synchronized int getCurrentCollarResistance() {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
) return 0;
return collar.getCurrentResistance(stack, player);
}
/**
* Phase 14.1.7: Added for IRestrainable interface
*/
public synchronized void setCurrentCollarResistance(int resistance) {
Player player = host.getPlayer();
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
if (
stack.isEmpty() || !(stack.getItem() instanceof ItemCollar collar)
) return;
collar.setCurrentResistance(stack, resistance);
}
// ========== Helper Methods ==========
/** Helper to drop an item at the kidnapped player's feet. */
public void kidnappedDropItem(ItemStack stack) {
if (stack.isEmpty() || host.getPlayer() == null) return;
host.getPlayer().drop(stack, false);
}
/**
* Helper to take a bondage item if it's unlocked, and drop it.
*/
public void takeBondageItemIfUnlocked(
ItemStack item,
Supplier<ItemStack> takeOffMethod
) {
if (isLocked(item, false)) {
return; // Item is locked, cannot remove
}
ItemStack removed = takeOffMethod.get();
if (!removed.isEmpty()) {
kidnappedDropItem(removed);
}
}
/**
* Check if an item is locked.
* @param stack The item to check
* @param force If true, returns false (bypasses lock)
* @return true if locked and not forced
*/
public boolean isLocked(ItemStack stack, boolean force) {
if (force) return false;
if (stack.isEmpty()) return false;
if (stack.getItem() instanceof ILockable lockable) {
return lockable.isLocked(stack);
}
return false;
}
private boolean hasMittens() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.HANDS);
}
// ========== Low-level V2 equipment operations ==========
/**
* Low-level equip: writes to V2 capability, calls IBondageItem lifecycle hooks, syncs.
* Uses setInRegion directly because V1 items do not implement IV2BondageItem.
*/
private void equipInRegion(BodyRegionV2 region, ItemStack stack) {
Player player = host.getPlayer();
if (player == null || player.level().isClientSide) return;
if (stack.isEmpty()) return;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return;
// Check if already occupied
if (equip.isRegionOccupied(region)) return;
// Check canEquip via V1 IBondageItem interface
if (stack.getItem() instanceof IV2BondageItem bondageItem) {
if (!bondageItem.canEquip(stack, player)) return;
}
equip.setInRegion(region, stack.copy());
// Fire lifecycle hook
if (stack.getItem() instanceof IV2BondageItem bondageItem) {
bondageItem.onEquipped(stack, player);
}
V2EquipmentHelper.sync(player);
}
/**
* Low-level replace: unequips old item with lifecycle hooks, equips new item.
* Unequips old item (with onUnequipped hook), sets new item, fires onEquipped.
*/
private ItemStack replaceInRegion(BodyRegionV2 region, ItemStack newStack) {
Player player = host.getPlayer();
if (player == null || player.level().isClientSide) return ItemStack.EMPTY;
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(player);
if (equip == null) return ItemStack.EMPTY;
ItemStack oldStack = equip.getInRegion(region);
// Call onUnequipped for the old item
if (!oldStack.isEmpty() && oldStack.getItem() instanceof IV2BondageItem oldItem) {
oldItem.onUnequipped(oldStack, player);
}
// Set the new item
equip.setInRegion(region, newStack.copy());
// Call onEquipped for the new item
if (!newStack.isEmpty() && newStack.getItem() instanceof IV2BondageItem newItem) {
newItem.onEquipped(newStack, player);
}
V2EquipmentHelper.sync(player);
return oldStack;
}
// ========== Callbacks ==========
public void checkGagAfterApply() {
// Gag talk handled via ChatEventHandler + GagTalkManager (event-based)
// No initialization needed here - effects apply automatically on chat
}
public void checkBlindfoldAfterApply() {
// Effects applied client-side via rendering
}
public void checkEarplugsAfterApply() {
// Effects applied client-side via sound system
}
public void checkCollarAfterApply() {
// Collar timers/GPS handled elsewhere
}
public void checkMittensAfterApply() {
// Mittens effects handled elsewhere
}
}

View File

@@ -0,0 +1,155 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.base.ItemBind;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import java.util.UUID;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Component responsible for player lifecycle management.
* Handles connection, death, respawn scenarios.
*
* Single Responsibility: Lifecycle events and registry coordination
* Complexity: HIGH (coordinates 4+ registries on death)
* Risk: HIGH (critical path, must maintain registry cleanup order)
*
* Registry Cleanup Order (on death):
* 1. CellRegistryV2 - remove from cells
* 2. PrisonerManager - release prisoner (via PrisonerService)
*/
public class PlayerLifecycle {
private final IPlayerBindStateHost host;
public PlayerLifecycle(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Lifecycle Methods ==========
/**
* Resets the player instance upon reconnection or respawn.
*
* IMPORTANT: Handle transport restoration for pole binding.
* With proxy-based leash system, leashes are not persisted through disconnection.
* The leash proxy is ephemeral and will be recreated if needed.
*
* Leg Binding: Speed reduction based on hasLegsBound(), not isTiedUp().
*/
public void resetNewConnection(Player player) {
// Update player reference
host.setOnline(true);
// Phase 17: Clear captor reference (enslavement ends on disconnect)
host.setCaptor(null);
// Reset struggle animation state (prevent stuck animations)
host.setStrugglingClient(false);
// Leash proxy doesn't persist through disconnection
// If player was leashed, they are now freed
// H6 fix: V1 speed reduction re-application is no longer needed for players.
// MovementStyleManager (V2 tick-based system) re-resolves the active movement
// style on the first tick after login (clearMovementState() resets activeMovementStyle
// to null, triggering a fresh activation). The V1 RestraintEffectUtils call here would
// cause double stacking with the V2 MULTIPLY_BASE modifier.
}
/**
* Called when the kidnapped player dies.
* Comprehensive cleanup: unlock items, drop items, free captivity, cleanup registries.
*
* @param world The world/level where death occurred
* @return true if death was handled
*/
public boolean onDeathKidnapped(Level world) {
Player player = host.getPlayer();
// Mark player as offline
host.setOnline(false);
// Clean up all registries on death (server-side only)
if (world instanceof ServerLevel serverLevel) {
UUID playerId = player.getUUID();
cleanupRegistries(serverLevel, playerId);
}
TiedUpMod.LOGGER.debug(
"[PlayerLifecycle] {} died while kidnapped",
player.getName().getString()
);
return true;
}
/**
* Coordinates cleanup of all registries when player dies.
* Order matters: Cell → Camp → Ransom → Prisoner
*
* @param serverLevel The server level
* @param playerId The player's UUID
*/
private void cleanupRegistries(ServerLevel serverLevel, UUID playerId) {
String playerName = host.getPlayer().getName().getString();
// 1. Clean up CellRegistryV2 - remove from any cells
CellRegistryV2 cellRegistry = CellRegistryV2.get(serverLevel);
int cellsRemoved = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsRemoved > 0) {
TiedUpMod.LOGGER.debug(
"[PlayerLifecycle] Removed {} from {} cells on death",
playerName,
cellsRemoved
);
}
// 2. Clean up prisoner state - release from imprisonment
com.tiedup.remake.prison.PrisonerManager manager =
com.tiedup.remake.prison.PrisonerManager.get(serverLevel);
com.tiedup.remake.prison.PrisonerState state = manager.getState(
playerId
);
// Release if imprisoned or working (player died)
if (
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
state == com.tiedup.remake.prison.PrisonerState.WORKING
) {
// Use centralized escape service for complete cleanup
com.tiedup.remake.prison.service.PrisonerService.get().escape(
serverLevel,
playerId,
"player_death"
);
}
}
// ========== Helper Methods ==========
/**
* Get the current bind ItemStack.
* Epic 5F: Migrated to V2EquipmentHelper.
*/
private ItemStack getCurrentBind() {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
host.getPlayer(),
com.tiedup.remake.v2.BodyRegionV2.ARMS
);
}
/**
* Check if player has legs bound.
*/
private boolean hasLegsBound(ItemStack bind) {
if (bind.isEmpty()) return false;
if (!(bind.getItem() instanceof ItemBind)) return false;
return ItemBind.hasLegsBound(bind);
}
}

View File

@@ -0,0 +1,54 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.util.tasks.ItemTask;
/**
* Component responsible for player sale system.
* Phase 14.3.5: Sale system fields
*
* Single Responsibility: Sale state management
* Complexity: LOW (simple state tracking)
* Risk: LOW (isolated system)
*/
public class PlayerSale {
// ========== Sale Fields ==========
private boolean forSale = false;
private ItemTask salePrice = null;
// ========== Sale Methods ==========
/**
* Check if player is currently for sale.
*/
public boolean isForSell() {
return this.forSale && this.salePrice != null;
}
/**
* Get the sale price.
* @return The price, or null if not for sale
*/
public ItemTask getSalePrice() {
return this.salePrice;
}
/**
* Put player up for sale with the given price.
* @param price The sale price task
*/
public void putForSale(ItemTask price) {
if (price == null) return;
this.forSale = true;
this.salePrice = price;
}
/**
* Cancel the sale and clear price.
*/
public void cancelSale() {
this.forSale = false;
this.salePrice = null;
}
}

View File

@@ -0,0 +1,260 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.core.ModSounds;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemGpsCollar;
import com.tiedup.remake.items.ItemShockCollarAuto;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.util.GameConstants;
import com.tiedup.remake.util.time.Timer;
import java.util.List;
import java.util.UUID;
import net.minecraft.ChatFormatting;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Component responsible for shock collar mechanics and GPS tracking.
* Phase 13: Advanced collar features (shocks, GPS tracking)
*
* Single Responsibility: Collar automation and GPS monitoring
* Complexity: HIGH (synchronized timer, GPS zone checks, network messages)
* Risk: HIGH (timer thread management)
*
* Threading: Uses synchronized blocks for timer access (volatile Timer field)
*/
public class PlayerShockCollar {
private final IPlayerBindStateHost host;
// Phase 13: Collar automation fields
// volatile: accessed from synchronized blocks across multiple threads
private volatile Timer timerAutoShockCollar;
private final Object lockTimerAutoShock = new Object();
public PlayerShockCollar(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Shock Functionality ==========
/**
* Triggers a shock with default damage.
*/
public void shockKidnapped() {
this.shockKidnapped(null, GameConstants.DEFAULT_SHOCK_DAMAGE);
}
/**
* Triggers a visual and auditory shock effect.
* Damage is applied (shock can kill).
*
* @param messageAddon Optional message addon for HUD (e.g., GPS violation)
* @param damage Shock damage amount
*/
public void shockKidnapped(@Nullable String messageAddon, float damage) {
Player player = host.getPlayer();
if (player == null || !player.isAlive()) return;
// Sound effects
player
.level()
.playSound(
null,
player.blockPosition(),
ModSounds.ELECTRIC_SHOCK.get(),
net.minecraft.sounds.SoundSource.PLAYERS,
GameConstants.SHOCK_SOUND_VOLUME,
GameConstants.SHOCK_SOUND_PITCH
);
// Particle effects
if (
player.level() instanceof
net.minecraft.server.level.ServerLevel serverLevel
) {
serverLevel.sendParticles(
net.minecraft.core.particles.ParticleTypes.ELECTRIC_SPARK,
player.getX(),
player.getY() + GameConstants.SHOCK_PARTICLE_Y_OFFSET,
player.getZ(),
GameConstants.SHOCK_PARTICLE_COUNT,
GameConstants.SHOCK_PARTICLE_X_SPREAD,
GameConstants.SHOCK_PARTICLE_Y_SPREAD,
GameConstants.SHOCK_PARTICLE_Z_SPREAD,
GameConstants.SHOCK_PARTICLE_SPEED
);
}
// Damage logic - shock can kill
if (damage > 0) {
player.hurt(player.damageSources().magic(), damage);
}
// HUD Message via SystemMessageManager
if (messageAddon != null) {
// Custom message with addon (e.g., GPS violation)
SystemMessageManager.sendToPlayer(
player,
MessageCategory.SLAVE_SHOCK,
SystemMessageManager.getTemplate(MessageCategory.SLAVE_SHOCK) +
messageAddon
);
} else {
SystemMessageManager.sendToPlayer(
player,
MessageCategory.SLAVE_SHOCK
);
}
}
// ========== Auto-Shock & GPS Monitoring ==========
/**
* Periodic check for Auto-Shock intervals and GPS Safe Zones.
* Called from RestraintTaskTickHandler every player tick.
*
* Thread Safety: Refactored to avoid alien method calls under lock.
* We compute shock decisions inside synchronized block, then execute
* outside the lock to prevent deadlock risk.
*/
public void checkAutoShockCollar() {
Player player = host.getPlayer();
if (player == null || !player.isAlive()) return;
// Flags set inside lock, actions performed outside
boolean shouldShockAuto = false;
boolean shouldShockGPS = false;
ItemGpsCollar gpsCollar = null;
ItemStack gpsStack = null;
synchronized (lockTimerAutoShock) {
ItemStack collarStack = getCurrentCollar();
if (collarStack.isEmpty()) return;
// Auto-Shock Collar handling
if (
collarStack.getItem() instanceof ItemShockCollarAuto collarShock
) {
if (
timerAutoShockCollar != null &&
timerAutoShockCollar.isExpired()
) {
shouldShockAuto = true;
}
if (
timerAutoShockCollar == null ||
timerAutoShockCollar.isExpired()
) {
timerAutoShockCollar = new Timer(
collarShock.getInterval() /
GameConstants.TICKS_PER_SECOND,
player.level()
);
}
}
// GPS Collar handling
else if (collarStack.getItem() instanceof ItemGpsCollar gps) {
if (
gps.isActive(collarStack) &&
(timerAutoShockCollar == null ||
timerAutoShockCollar.isExpired())
) {
List<ItemGpsCollar.SafeSpot> safeSpots = gps.getSafeSpots(
collarStack
);
if (safeSpots != null && !safeSpots.isEmpty()) {
boolean isSafe = false;
for (ItemGpsCollar.SafeSpot spot : safeSpots) {
if (spot.isInside(player)) {
isSafe = true;
break;
}
}
if (!isSafe) {
timerAutoShockCollar = new Timer(
gps.getShockInterval(collarStack) /
GameConstants.TICKS_PER_SECOND,
player.level()
);
shouldShockGPS = true;
gpsCollar = gps;
gpsStack = collarStack.copy();
}
}
}
}
}
// Execute shock actions OUTSIDE the lock (avoid alien method call under lock)
if (shouldShockAuto) {
this.shockKidnapped();
}
if (shouldShockGPS && gpsCollar != null) {
this.shockKidnapped(
" Return back to your allowed area!",
GameConstants.DEFAULT_SHOCK_DAMAGE
);
warnOwnersGPSViolation(gpsCollar, gpsStack);
}
}
/**
* Sends a global alert to masters when a slave violates their GPS zone.
* Private helper method.
*/
private void warnOwnersGPSViolation(ItemGpsCollar gps, ItemStack stack) {
Player player = host.getPlayer();
if (player.getServer() == null) return;
// Format: "ALERT: <player name> is outside the safe zone!"
String alertMessage = String.format(
SystemMessageManager.getTemplate(MessageCategory.GPS_OWNER_ALERT),
player.getName().getString()
);
for (UUID ownerId : gps.getOwners(stack)) {
ServerPlayer owner = player
.getServer()
.getPlayerList()
.getPlayer(ownerId);
if (owner != null) {
SystemMessageManager.sendChatToPlayer(
owner,
alertMessage,
ChatFormatting.RED
);
}
}
}
/**
* Force-stops and clears any active shock timers.
* Threading: Synchronized block protects timer access
*/
public void resetAutoShockTimer() {
synchronized (lockTimerAutoShock) {
this.timerAutoShockCollar = null;
}
}
// ========== Helper Methods ==========
/**
* Get the current collar ItemStack.
* Epic 5F: Migrated to V2EquipmentHelper.
*/
private ItemStack getCurrentCollar() {
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getInRegion(
host.getPlayer(),
com.tiedup.remake.v2.BodyRegionV2.NECK
);
}
}

View File

@@ -0,0 +1,85 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IRestrainableEntity;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.util.RestraintEffectUtils;
import com.tiedup.remake.v2.BodyRegionV2;
/**
* Component responsible for special player interactions.
* v2.5: Knife cut target for accessory cutting
* Phase 14.1.7: Item transfers between players
*
* Single Responsibility: Special action management
* Complexity: MEDIUM (external dependencies)
* Risk: LOW (well-defined interactions)
*/
public class PlayerSpecialActions {
private final IPlayerBindStateHost host;
// v2.5: Knife cut target region for accessory cutting (migrated to V2)
private BodyRegionV2 knifeCutRegion = null;
public PlayerSpecialActions(IPlayerBindStateHost host) {
this.host = host;
}
// ========== Knife Cut Target ==========
/**
* Set the body region target for knife cutting.
* Used when player selects "Cut" from StruggleChoiceScreen.
*
* @param region The body region to cut (NECK, MOUTH, etc.)
*/
public void setKnifeCutTarget(BodyRegionV2 region) {
this.knifeCutRegion = region;
}
/**
* Get the current knife cut target region.
*
* @return The target region, or null if none
*/
public BodyRegionV2 getKnifeCutTarget() {
return knifeCutRegion;
}
/**
* Clear the knife cut target.
*/
public void clearKnifeCutTarget() {
this.knifeCutRegion = null;
}
// ========== Special Interactions ==========
/**
* Apply chloroform effects to the player.
*
* @param duration Duration in seconds
*/
public void applyChloroform(int duration) {
if (host.getPlayer() == null) return;
RestraintEffectUtils.applyChloroformEffects(host.getPlayer(), duration);
}
/**
* Phase 14.1.7: Transfer bondage item from this player to another.
* Updated to use IRestrainable parameter (was PlayerBindState)
*
* @param taker The entity taking the item
* @param slotIndex The slot index to take from
*/
public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) {
// TODO Phase 14+: Transfer item from this player to taker
TiedUpMod.LOGGER.debug(
"[PlayerSpecialActions] {} taking bondage item from {} (slot {})",
taker.getKidnappedName(),
host.getPlayer().getName().getString(),
slotIndex
);
}
}

View File

@@ -0,0 +1,148 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
/**
* Component responsible for querying player restraint state.
* Provides read-only checks for equipment status.
*
* Single Responsibility: State queries
* Complexity: LOW (read-only delegation)
* Risk: LOW (no state modification)
*
* Epic 5F: Uses V2EquipmentHelper/BodyRegionV2.
*/
public class PlayerStateQuery {
private final IPlayerBindStateHost host;
public PlayerStateQuery(IPlayerBindStateHost host) {
this.host = host;
}
// ========== State Query Methods ==========
/** Check if player has ropes/ties equipped. */
public boolean isTiedUp() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.ARMS);
}
/** Check if player is currently gagged. */
public boolean isGagged() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.MOUTH);
}
/** Check if player is blindfolded. */
public boolean isBlindfolded() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.EYES);
}
/** Check if player has earplugs. */
public boolean hasEarplugs() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.EARS);
}
public boolean isEarplugged() {
return hasEarplugs();
}
/** Check if player is wearing a collar. */
public boolean hasCollar() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.NECK);
}
/** Returns the current collar ItemStack, or empty if none. */
public ItemStack getCurrentCollar() {
if (!hasCollar()) return ItemStack.EMPTY;
return V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.NECK);
}
public boolean hasClothes() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.TORSO);
}
/** Check if player has mittens equipped. Phase 14.4: Mittens system */
public boolean hasMittens() {
return V2EquipmentHelper.isRegionOccupied(host.getPlayer(), BodyRegionV2.HANDS);
}
/** Check if player can be tied up (not already tied). */
public boolean canBeTiedUp() {
return !isTiedUp();
}
/** Check if player is both tied and gagged. */
public boolean isBoundAndGagged() {
return isTiedUp() && isGagged();
}
/** Check if player has knives in inventory. */
public boolean hasKnives() {
Player player = host.getPlayer();
if (player == null) return false;
// Check main inventory for knife items
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
ItemStack stack = player.getInventory().getItem(i);
if (
!stack.isEmpty() &&
stack.getItem() instanceof com.tiedup.remake.items.base.IKnife
) {
return true;
}
}
return false;
}
/**
* Check if player has a gagging effect enabled.
* Checks both if gagged AND if the gag item implements the effect.
*/
public boolean hasGaggingEffect() {
if (!isGagged()) return false;
ItemStack gag = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.MOUTH);
if (gag.isEmpty()) return false;
return (
gag.getItem() instanceof
com.tiedup.remake.items.base.IHasGaggingEffect
);
}
/**
* Check if player has a blinding effect enabled.
* Checks both if blindfolded AND if the blindfold item implements the effect.
*/
public boolean hasBlindingEffect() {
if (!isBlindfolded()) return false;
ItemStack blindfold = V2EquipmentHelper.getInRegion(host.getPlayer(), BodyRegionV2.EYES);
if (blindfold.isEmpty()) return false;
return (
blindfold.getItem() instanceof
com.tiedup.remake.items.base.IHasBlindingEffect
);
}
/**
* Check if player can be kidnapped by random events.
* Checks game rules and current state.
*/
public boolean canBeKidnappedByEvents() {
Player player = host.getPlayer();
// Check if kidnapper spawning is enabled
if (player != null && player.level() != null) {
if (
!com.tiedup.remake.core.SettingsAccessor.doKidnappersSpawn(
player.level().getGameRules()
)
) {
return false;
}
}
// Can't be kidnapped if already tied up or captive (grace period protection)
return !isTiedUp() && !host.getKidnapped().isCaptive();
}
}

View File

@@ -0,0 +1,128 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.state.hosts.IPlayerBindStateHost;
import com.tiedup.remake.state.struggle.StruggleBinds;
import com.tiedup.remake.state.struggle.StruggleCollar;
import net.minecraft.world.entity.player.Player;
/**
* Component responsible for struggle mechanics and resistance management.
* Phase 7: Struggle & Resistance Methods
*
* Single Responsibility: Struggle state and resistance tracking
* Complexity: MEDIUM (volatile fields, animation coordination)
* Risk: MEDIUM (thread-safety requirements)
*
* Threading: Uses host's volatile fields for animation state (isStruggling, struggleStartTick)
*
* Note: StruggleBinds and StruggleCollar require PlayerBindState parameter.
* Since PlayerBindState implements IPlayerBindStateHost, we can safely cast.
*/
public class PlayerStruggle {
private final IPlayerBindStateHost host;
private final PlayerBindState state; // Cast reference for struggle system
// Phase 7 & 8: Struggle state tracking
private final StruggleBinds struggleBindState;
private final StruggleCollar struggleCollarState;
public PlayerStruggle(IPlayerBindStateHost host) {
this.host = host;
// Safe cast: PlayerBindState implements IPlayerBindStateHost
this.state = (PlayerBindState) host;
// Initialize sub-states for struggling
this.struggleBindState = new StruggleBinds();
this.struggleCollarState = new StruggleCollar();
}
// ========== Struggle Methods ==========
/**
* Entry point for the Struggle logic (Key R).
* Distributes effort between Binds and Collar.
*
* Thread Safety: Synchronized to prevent lost updates when multiple struggle
* packets arrive simultaneously (e.g., from macro/rapid keypresses).
*/
public synchronized void struggle() {
if (struggleBindState != null) struggleBindState.struggle(state);
if (struggleCollarState != null) struggleCollarState.struggle(state);
}
/**
* Restores resistance to base values when a master tightens the ties.
*
* Thread Safety: Synchronized to prevent race with struggle operations.
*/
public synchronized void tighten(Player tightener) {
if (struggleBindState != null) struggleBindState.tighten(
tightener,
state
);
if (struggleCollarState != null) struggleCollarState.tighten(
tightener,
state
);
}
/**
* Get the StruggleBinds instance for external access (mini-game system).
*/
public StruggleBinds getStruggleBinds() {
return struggleBindState;
}
/**
* Set a cooldown on struggle attempts (used after mini-game exhaustion).
* @param seconds Cooldown duration in seconds
*/
public void setStruggleCooldown(int seconds) {
if (
struggleBindState != null &&
host.getPlayer() != null &&
host.getLevel() != null
) {
struggleBindState.setExternalCooldown(seconds, host.getLevel());
}
}
/**
* v2.5: Check if struggle cooldown is active.
* @return true if cooldown is active (cannot struggle yet)
*/
public boolean isStruggleCooldownActive() {
return (
struggleBindState != null && struggleBindState.isCooldownActive()
);
}
/**
* v2.5: Get remaining struggle cooldown in seconds.
* @return Remaining seconds, or 0 if no cooldown
*/
public int getStruggleCooldownRemaining() {
return struggleBindState != null
? struggleBindState.getRemainingCooldownSeconds()
: 0;
}
// ========== Animation Control ==========
// Note: Animation state (isStruggling, struggleStartTick) is managed by host
// via IPlayerBindStateHost interface (volatile fields for thread-safety)
/**
* Check if struggle animation should stop (duration expired).
* @param currentTick Current game time tick
* @return True if animation has been playing for >= 80 ticks
*/
public boolean shouldStopStruggling(long currentTick) {
if (!host.isStruggling()) return false;
return (
(currentTick - host.getStruggleStartTick()) >=
com.tiedup.remake.util.GameConstants.STRUGGLE_ANIMATION_DURATION_TICKS
);
}
}

View File

@@ -0,0 +1,85 @@
package com.tiedup.remake.state.components;
import com.tiedup.remake.tasks.PlayerStateTask;
import com.tiedup.remake.tasks.TimedInteractTask;
import com.tiedup.remake.tasks.TyingTask;
import com.tiedup.remake.tasks.UntyingTask;
/**
* Component responsible for tracking tying/untying tasks.
* Phase 6: Tying/Untying task tracking (Phase 14.2.6: unified for Players + NPCs)
*
* Single Responsibility: Task state management
* Complexity: LOW (simple getters/setters)
* Risk: LOW (isolated, no external dependencies)
*/
public class PlayerTaskManagement {
// ========== Task Fields ==========
private TyingTask currentTyingTask;
private UntyingTask currentUntyingTask;
private PlayerStateTask clientTyingTask;
private PlayerStateTask clientUntyingTask;
private TimedInteractTask currentFeedingTask;
private PlayerStateTask clientFeedingTask;
private PlayerStateTask restrainedState;
// ========== Task Getters/Setters ==========
public TyingTask getCurrentTyingTask() {
return currentTyingTask;
}
public void setCurrentTyingTask(TyingTask task) {
this.currentTyingTask = task;
}
public UntyingTask getCurrentUntyingTask() {
return currentUntyingTask;
}
public void setCurrentUntyingTask(UntyingTask task) {
this.currentUntyingTask = task;
}
public PlayerStateTask getClientTyingTask() {
return clientTyingTask;
}
public void setClientTyingTask(PlayerStateTask task) {
this.clientTyingTask = task;
}
public PlayerStateTask getClientUntyingTask() {
return clientUntyingTask;
}
public void setClientUntyingTask(PlayerStateTask task) {
this.clientUntyingTask = task;
}
public TimedInteractTask getCurrentFeedingTask() {
return currentFeedingTask;
}
public void setCurrentFeedingTask(TimedInteractTask task) {
this.currentFeedingTask = task;
}
public PlayerStateTask getClientFeedingTask() {
return clientFeedingTask;
}
public void setClientFeedingTask(PlayerStateTask task) {
this.clientFeedingTask = task;
}
public PlayerStateTask getRestrainedState() {
return restrainedState;
}
public void setRestrainedState(PlayerStateTask state) {
this.restrainedState = state;
}
}