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:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user