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:
386
src/main/java/com/tiedup/remake/state/CollarRegistry.java
Normal file
386
src/main/java/com/tiedup/remake/state/CollarRegistry.java
Normal file
@@ -0,0 +1,386 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Global registry for collar ownership relationships.
|
||||
*
|
||||
* This registry tracks which entities are wearing collars and who owns them.
|
||||
* It persists across server restarts and provides efficient lookups in both directions:
|
||||
* - Owner UUID → Set of collar-wearer UUIDs (slaves)
|
||||
* - Wearer UUID → Set of owner UUIDs (masters)
|
||||
*
|
||||
* Terminology:
|
||||
* - "Slave" = Entity wearing a collar owned by a player (passive ownership)
|
||||
* - "Captive" = Entity attached by leash (active physical control) - managed by PlayerCaptiveManager
|
||||
*
|
||||
* Phase 17: Terminology Refactoring
|
||||
*/
|
||||
public class CollarRegistry extends SavedData {
|
||||
|
||||
private static final String DATA_NAME = "tiedup_collar_registry";
|
||||
|
||||
// Owner UUID → Set of wearer UUIDs (a master can own multiple slaves)
|
||||
private final Map<UUID, Set<UUID>> ownerToWearers =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// Wearer UUID → Set of owner UUIDs (a collar can have multiple owners)
|
||||
private final Map<UUID, Set<UUID>> wearerToOwners =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// ==================== STATIC ACCESS ====================
|
||||
|
||||
/**
|
||||
* Get the CollarRegistry for a server level.
|
||||
* Creates a new registry if one doesn't exist.
|
||||
*/
|
||||
public static CollarRegistry get(ServerLevel level) {
|
||||
return level
|
||||
.getDataStorage()
|
||||
.computeIfAbsent(
|
||||
CollarRegistry::load,
|
||||
CollarRegistry::new,
|
||||
DATA_NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CollarRegistry from a MinecraftServer.
|
||||
* Uses the overworld as the storage dimension.
|
||||
*/
|
||||
public static CollarRegistry get(MinecraftServer server) {
|
||||
ServerLevel overworld = server.overworld();
|
||||
return get(overworld);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to get registry from a ServerPlayer.
|
||||
*/
|
||||
@Nullable
|
||||
public static CollarRegistry get(ServerPlayer player) {
|
||||
if (player.getServer() == null) return null;
|
||||
return get(player.getServer());
|
||||
}
|
||||
|
||||
// ==================== REGISTRATION ====================
|
||||
|
||||
/**
|
||||
* Register a collar relationship: owner now owns the collar on wearer.
|
||||
*
|
||||
* @param wearerUUID UUID of the entity wearing the collar
|
||||
* @param ownerUUID UUID of the collar's owner
|
||||
*/
|
||||
public void registerCollar(UUID wearerUUID, UUID ownerUUID) {
|
||||
// Add to owner → wearers map
|
||||
ownerToWearers
|
||||
.computeIfAbsent(ownerUUID, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(wearerUUID);
|
||||
|
||||
// Add to wearer → owners map
|
||||
wearerToOwners
|
||||
.computeIfAbsent(wearerUUID, k -> ConcurrentHashMap.newKeySet())
|
||||
.add(ownerUUID);
|
||||
|
||||
setDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a collar with multiple owners at once.
|
||||
*
|
||||
* @param wearerUUID UUID of the entity wearing the collar
|
||||
* @param ownerUUIDs Set of owner UUIDs
|
||||
*/
|
||||
public void registerCollar(UUID wearerUUID, Set<UUID> ownerUUIDs) {
|
||||
for (UUID ownerUUID : ownerUUIDs) {
|
||||
registerCollar(wearerUUID, ownerUUID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a specific owner from a collar wearer.
|
||||
*
|
||||
* @param wearerUUID UUID of the entity wearing the collar
|
||||
* @param ownerUUID UUID of the owner to remove
|
||||
*/
|
||||
private void unregisterOwner(UUID wearerUUID, UUID ownerUUID) {
|
||||
// Remove from owner → wearers map using atomic operation
|
||||
ownerToWearers.computeIfPresent(ownerUUID, (key, wearers) -> {
|
||||
wearers.remove(wearerUUID);
|
||||
return wearers.isEmpty() ? null : wearers;
|
||||
});
|
||||
|
||||
// Remove from wearer → owners map using atomic operation
|
||||
wearerToOwners.computeIfPresent(wearerUUID, (key, owners) -> {
|
||||
owners.remove(ownerUUID);
|
||||
return owners.isEmpty() ? null : owners;
|
||||
});
|
||||
|
||||
setDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely unregister a collar wearer (removes all owner relationships).
|
||||
* Called when a collar is removed from an entity.
|
||||
*
|
||||
* @param wearerUUID UUID of the entity whose collar was removed
|
||||
*/
|
||||
public void unregisterWearer(UUID wearerUUID) {
|
||||
Set<UUID> owners = wearerToOwners.remove(wearerUUID);
|
||||
if (owners != null) {
|
||||
for (UUID ownerUUID : owners) {
|
||||
// Use atomic operation for thread-safe removal
|
||||
ownerToWearers.computeIfPresent(ownerUUID, (key, wearers) -> {
|
||||
wearers.remove(wearerUUID);
|
||||
return wearers.isEmpty() ? null : wearers;
|
||||
});
|
||||
}
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a wearer's owners completely (replaces all existing owners).
|
||||
* Useful when collar NBT is the source of truth.
|
||||
*
|
||||
* @param wearerUUID UUID of the entity wearing the collar
|
||||
* @param newOwnerUUIDs New set of owner UUIDs
|
||||
*/
|
||||
public void updateWearerOwners(UUID wearerUUID, Set<UUID> newOwnerUUIDs) {
|
||||
// Get current owners
|
||||
Set<UUID> currentOwners = wearerToOwners.get(wearerUUID);
|
||||
if (currentOwners == null) {
|
||||
currentOwners = Collections.emptySet();
|
||||
}
|
||||
|
||||
// Find owners to remove
|
||||
Set<UUID> toRemove = new HashSet<>(currentOwners);
|
||||
toRemove.removeAll(newOwnerUUIDs);
|
||||
|
||||
// Find owners to add
|
||||
Set<UUID> toAdd = new HashSet<>(newOwnerUUIDs);
|
||||
toAdd.removeAll(currentOwners);
|
||||
|
||||
// Apply changes
|
||||
for (UUID ownerUUID : toRemove) {
|
||||
unregisterOwner(wearerUUID, ownerUUID);
|
||||
}
|
||||
for (UUID ownerUUID : toAdd) {
|
||||
registerCollar(wearerUUID, ownerUUID);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== QUERIES ====================
|
||||
|
||||
/**
|
||||
* Get all slaves (collar wearers) owned by a specific owner.
|
||||
*
|
||||
* @param ownerUUID UUID of the owner
|
||||
* @return Unmodifiable set of wearer UUIDs (never null)
|
||||
*/
|
||||
public Set<UUID> getSlaves(UUID ownerUUID) {
|
||||
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
|
||||
if (wearers == null) return Collections.emptySet();
|
||||
return Collections.unmodifiableSet(new HashSet<>(wearers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all owners of a specific collar wearer.
|
||||
*
|
||||
* @param wearerUUID UUID of the wearer
|
||||
* @return Unmodifiable set of owner UUIDs (never null)
|
||||
*/
|
||||
public Set<UUID> getOwners(UUID wearerUUID) {
|
||||
Set<UUID> owners = wearerToOwners.get(wearerUUID);
|
||||
if (owners == null) return Collections.emptySet();
|
||||
return Collections.unmodifiableSet(new HashSet<>(owners));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an owner has any slaves.
|
||||
*/
|
||||
public boolean hasSlaves(UUID ownerUUID) {
|
||||
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
|
||||
return wearers != null && !wearers.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a wearer has any owners.
|
||||
*/
|
||||
public boolean hasOwners(UUID wearerUUID) {
|
||||
Set<UUID> owners = wearerToOwners.get(wearerUUID);
|
||||
return owners != null && !owners.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific owner owns a specific wearer.
|
||||
*/
|
||||
public boolean isOwner(UUID ownerUUID, UUID wearerUUID) {
|
||||
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
|
||||
return wearers != null && wearers.contains(wearerUUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of slaves for an owner.
|
||||
*/
|
||||
public int getSlaveCount(UUID ownerUUID) {
|
||||
Set<UUID> wearers = ownerToWearers.get(ownerUUID);
|
||||
return wearers == null ? 0 : wearers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered wearers (for admin/debug purposes).
|
||||
*/
|
||||
public Set<UUID> getAllWearers() {
|
||||
return Collections.unmodifiableSet(
|
||||
new HashSet<>(wearerToOwners.keySet())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered owners (for admin/debug purposes).
|
||||
*/
|
||||
public Set<UUID> getAllOwners() {
|
||||
return Collections.unmodifiableSet(
|
||||
new HashSet<>(ownerToWearers.keySet())
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== ENTITY RESOLUTION ====================
|
||||
|
||||
/**
|
||||
* Find all slave entities for an owner that are currently loaded.
|
||||
* This is useful for GUI and proximity-based actions.
|
||||
*
|
||||
* @param owner The owner player
|
||||
* @return List of currently loaded slave entities
|
||||
*/
|
||||
public List<LivingEntity> findLoadedSlaves(ServerPlayer owner) {
|
||||
List<LivingEntity> loadedSlaves = new ArrayList<>();
|
||||
Set<UUID> slaveUUIDs = getSlaves(owner.getUUID());
|
||||
|
||||
MinecraftServer server = owner.getServer();
|
||||
if (server == null) return loadedSlaves;
|
||||
|
||||
for (UUID slaveUUID : slaveUUIDs) {
|
||||
Entity entity = findEntityByUUID(server, slaveUUID);
|
||||
if (entity instanceof LivingEntity living) {
|
||||
loadedSlaves.add(living);
|
||||
}
|
||||
}
|
||||
|
||||
return loadedSlaves;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an entity by UUID across all dimensions.
|
||||
* Optimized: checks player list first (O(1)) before searching dimensions.
|
||||
*/
|
||||
@Nullable
|
||||
private Entity findEntityByUUID(MinecraftServer server, UUID uuid) {
|
||||
// Check player first - O(1) lookup
|
||||
net.minecraft.server.level.ServerPlayer player = server
|
||||
.getPlayerList()
|
||||
.getPlayer(uuid);
|
||||
if (player != null) {
|
||||
return player;
|
||||
}
|
||||
|
||||
// Fallback: search dimensions for NPCs
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
Entity entity = level.getEntity(uuid);
|
||||
if (entity != null) {
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== PERSISTENCE ====================
|
||||
|
||||
@Override
|
||||
public @NotNull CompoundTag save(@NotNull CompoundTag tag) {
|
||||
ListTag registryList = new ListTag();
|
||||
|
||||
for (Map.Entry<UUID, Set<UUID>> entry : wearerToOwners.entrySet()) {
|
||||
CompoundTag wearerTag = new CompoundTag();
|
||||
wearerTag.putUUID("wearer", entry.getKey());
|
||||
|
||||
ListTag ownersTag = new ListTag();
|
||||
for (UUID ownerUUID : entry.getValue()) {
|
||||
CompoundTag ownerTag = new CompoundTag();
|
||||
ownerTag.putUUID("uuid", ownerUUID);
|
||||
ownersTag.add(ownerTag);
|
||||
}
|
||||
wearerTag.put("owners", ownersTag);
|
||||
registryList.add(wearerTag);
|
||||
}
|
||||
|
||||
tag.put("collar_registry", registryList);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public static CollarRegistry load(CompoundTag tag) {
|
||||
CollarRegistry registry = new CollarRegistry();
|
||||
|
||||
ListTag registryList = tag.getList("collar_registry", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < registryList.size(); i++) {
|
||||
CompoundTag wearerTag = registryList.getCompound(i);
|
||||
UUID wearerUUID = wearerTag.getUUID("wearer");
|
||||
|
||||
ListTag ownersTag = wearerTag.getList("owners", Tag.TAG_COMPOUND);
|
||||
for (int j = 0; j < ownersTag.size(); j++) {
|
||||
CompoundTag ownerTag = ownersTag.getCompound(j);
|
||||
UUID ownerUUID = ownerTag.getUUID("uuid");
|
||||
|
||||
// Register relationship (without marking dirty - we're loading)
|
||||
registry.ownerToWearers
|
||||
.computeIfAbsent(ownerUUID, k ->
|
||||
ConcurrentHashMap.newKeySet()
|
||||
)
|
||||
.add(wearerUUID);
|
||||
registry.wearerToOwners
|
||||
.computeIfAbsent(wearerUUID, k ->
|
||||
ConcurrentHashMap.newKeySet()
|
||||
)
|
||||
.add(ownerUUID);
|
||||
}
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
// ==================== DEBUG ====================
|
||||
|
||||
/**
|
||||
* Get a debug string representation of the registry.
|
||||
*/
|
||||
public String toDebugString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CollarRegistry:\n");
|
||||
sb.append(" Owners: ").append(ownerToWearers.size()).append("\n");
|
||||
sb.append(" Wearers: ").append(wearerToOwners.size()).append("\n");
|
||||
|
||||
for (Map.Entry<UUID, Set<UUID>> entry : ownerToWearers.entrySet()) {
|
||||
sb
|
||||
.append(" Owner ")
|
||||
.append(entry.getKey().toString().substring(0, 8))
|
||||
.append("... → ")
|
||||
.append(entry.getValue().size())
|
||||
.append(" slaves\n");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
61
src/main/java/com/tiedup/remake/state/HumanChairHelper.java
Normal file
61
src/main/java/com/tiedup/remake/state/HumanChairHelper.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import com.tiedup.remake.items.base.PoseType;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Centralizes human chair NBT tag keys and resolution logic.
|
||||
*
|
||||
* <p>The human chair mode is stored as NBT tags on the dog bind item.
|
||||
* This helper eliminates hardcoded string literals scattered across 7+ files.
|
||||
*/
|
||||
public final class HumanChairHelper {
|
||||
|
||||
/** NBT key indicating this bind is in human chair mode */
|
||||
public static final String NBT_KEY = "humanChairMode";
|
||||
|
||||
/** NBT key for the locked facing direction (degrees) */
|
||||
public static final String NBT_FACING_KEY = "humanChairFacing";
|
||||
|
||||
private HumanChairHelper() {}
|
||||
|
||||
/**
|
||||
* Check if a bind item is in human chair mode.
|
||||
*
|
||||
* @param bind The bind ItemStack to check
|
||||
* @return true if the bind has humanChairMode NBT set to true
|
||||
*/
|
||||
public static boolean isActive(ItemStack bind) {
|
||||
if (bind.isEmpty()) return false;
|
||||
CompoundTag tag = bind.getTag();
|
||||
return tag != null && tag.getBoolean(NBT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locked facing direction for human chair mode.
|
||||
*
|
||||
* @param bind The bind ItemStack
|
||||
* @return The facing angle in degrees, or 0 if not set
|
||||
*/
|
||||
public static float getFacing(ItemStack bind) {
|
||||
if (bind.isEmpty()) return 0f;
|
||||
CompoundTag tag = bind.getTag();
|
||||
return tag != null ? tag.getFloat(NBT_FACING_KEY) : 0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the effective pose type, overriding DOG to HUMAN_CHAIR
|
||||
* when the bind has humanChairMode enabled.
|
||||
*
|
||||
* @param base The base pose type from the bind item
|
||||
* @param bind The bind ItemStack (checked for humanChairMode NBT)
|
||||
* @return HUMAN_CHAIR if base is DOG and humanChairMode is active, otherwise base
|
||||
*/
|
||||
public static PoseType resolveEffectivePose(PoseType base, ItemStack bind) {
|
||||
if (base == PoseType.DOG && isActive(bind)) {
|
||||
return PoseType.HUMAN_CHAIR;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
553
src/main/java/com/tiedup/remake/state/IBondageState.java
Normal file
553
src/main/java/com/tiedup/remake/state/IBondageState.java
Normal file
@@ -0,0 +1,553 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import com.tiedup.remake.items.base.ILockable;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import java.util.function.Supplier;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Equipment CRUD, state checks, bulk operations, resistance, and lock safety
|
||||
* for bondage items on kidnapped entities.
|
||||
*
|
||||
* <p>This is the largest sub-interface, covering:</p>
|
||||
* <ul>
|
||||
* <li>State query methods (isTiedUp, isGagged, etc.)</li>
|
||||
* <li>Bind mode defaults (hasArmsBound, hasLegsBound, getBindModeId)</li>
|
||||
* <li>Deprecated per-slot put-on / take-off / get-current / replace methods</li>
|
||||
* <li>Bulk operations (applyBondage, untie, dropBondageItems)</li>
|
||||
* <li>Post-apply callbacks (checkXAfterApply defaults)</li>
|
||||
* <li>Lock safety helpers (ifUnlocked, isLocked, ifUnlockedReturn, takeBondageItemIfUnlocked)</li>
|
||||
* <li>Clothes permissions</li>
|
||||
* <li>Resistance system defaults</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see ICapturable
|
||||
* @see IRestrainableEntity
|
||||
* @see IRestrainable
|
||||
*/
|
||||
public interface IBondageState extends ICapturable {
|
||||
|
||||
// ========================================
|
||||
// V2 REGION-BASED EQUIPMENT ACCESS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the item equipped in a V2 body region.
|
||||
*
|
||||
* @param region The body region to query
|
||||
* @return The equipped ItemStack, or {@link ItemStack#EMPTY} if empty
|
||||
*/
|
||||
ItemStack getEquipment(BodyRegionV2 region);
|
||||
|
||||
/**
|
||||
* Equip an item in a V2 body region.
|
||||
* Dispatches to the appropriate slot-specific equip logic (sounds, hooks, sync).
|
||||
*
|
||||
* @param region The body region to equip into
|
||||
* @param stack The ItemStack to equip
|
||||
*/
|
||||
void equip(BodyRegionV2 region, ItemStack stack);
|
||||
|
||||
/**
|
||||
* Unequip the item from a V2 body region, respecting locks.
|
||||
*
|
||||
* @param region The body region to unequip from
|
||||
* @return The removed ItemStack, or {@link ItemStack#EMPTY} if locked/empty
|
||||
*/
|
||||
ItemStack unequip(BodyRegionV2 region);
|
||||
|
||||
/**
|
||||
* Force-unequip the item from a V2 body region, ignoring locks.
|
||||
* Used for admin commands, kidnapper theft, kill cleanup, etc.
|
||||
*
|
||||
* @param region The body region to force-unequip from
|
||||
* @return The removed ItemStack, or {@link ItemStack#EMPTY} if empty
|
||||
*/
|
||||
ItemStack forceUnequip(BodyRegionV2 region);
|
||||
|
||||
/**
|
||||
* Replace the item in a body region, returning the old item.
|
||||
*
|
||||
* @param region The body region
|
||||
* @param newStack The new ItemStack to equip
|
||||
* @param force If true, replace even if current item is locked
|
||||
* @return The old ItemStack, or empty if locked (and !force) or nothing equipped
|
||||
*/
|
||||
ItemStack replaceEquipment(BodyRegionV2 region, ItemStack newStack, boolean force);
|
||||
|
||||
// ========================================
|
||||
// STATE QUERIES - BONDAGE EQUIPMENT
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if entity has bind/ropes equipped.
|
||||
* @return true if BIND slot is not empty
|
||||
*/
|
||||
boolean isTiedUp();
|
||||
|
||||
/**
|
||||
* Check if entity's arms are bound.
|
||||
* True if tied with mode ARMS or FULL.
|
||||
* Leg Binding System.
|
||||
*
|
||||
* @return true if arms are bound
|
||||
*/
|
||||
default boolean hasArmsBound() {
|
||||
if (!isTiedUp()) return false;
|
||||
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
|
||||
if (bind.isEmpty()) return false;
|
||||
return ItemBind.hasArmsBound(bind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity's legs are bound.
|
||||
* True if tied with mode LEGS or FULL.
|
||||
* Leg Binding System.
|
||||
*
|
||||
* @return true if legs are bound
|
||||
*/
|
||||
default boolean hasLegsBound() {
|
||||
if (!isTiedUp()) return false;
|
||||
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
|
||||
if (bind.isEmpty()) return false;
|
||||
return ItemBind.hasLegsBound(bind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bind mode ID string.
|
||||
* Leg Binding System.
|
||||
*
|
||||
* @return "full", "arms", or "legs"
|
||||
*/
|
||||
default String getBindModeId() {
|
||||
if (!isTiedUp()) return ItemBind.BIND_MODE_FULL;
|
||||
ItemStack bind = getEquipment(BodyRegionV2.ARMS);
|
||||
if (bind.isEmpty()) return ItemBind.BIND_MODE_FULL;
|
||||
return ItemBind.getBindModeId(bind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity is gagged.
|
||||
* @return true if GAG slot is not empty
|
||||
*/
|
||||
boolean isGagged();
|
||||
|
||||
/**
|
||||
* Check if entity is blindfolded.
|
||||
* @return true if BLINDFOLD slot is not empty
|
||||
*/
|
||||
boolean isBlindfolded();
|
||||
|
||||
/**
|
||||
* Check if entity has earplugs.
|
||||
* @return true if EARPLUGS slot is not empty
|
||||
*/
|
||||
boolean hasEarplugs();
|
||||
|
||||
/**
|
||||
* Check if entity has collar.
|
||||
* @return true if COLLAR slot is not empty
|
||||
*/
|
||||
boolean hasCollar();
|
||||
|
||||
/**
|
||||
* Check if entity is wearing a locked collar.
|
||||
* @return true if has collar AND collar is locked
|
||||
*/
|
||||
boolean hasLockedCollar();
|
||||
|
||||
/**
|
||||
* Check if entity's collar has a custom name/nickname.
|
||||
* Used for collar ownership display.
|
||||
*
|
||||
* @return true if collar has NBT nickname
|
||||
*/
|
||||
boolean hasNamedCollar();
|
||||
|
||||
/**
|
||||
* Check if entity has clothes equipped.
|
||||
* @return true if CLOTHES slot is not empty
|
||||
*/
|
||||
boolean hasClothes();
|
||||
|
||||
/**
|
||||
* Check if entity has mittens equipped.
|
||||
* Phase 14.4: Mittens system
|
||||
* @return true if MITTENS slot is not empty
|
||||
*/
|
||||
boolean hasMittens();
|
||||
|
||||
/**
|
||||
* Check if clothes have "small arms" rendering flag.
|
||||
* Used by EntityDamsel for custom model rendering.
|
||||
*
|
||||
* @return true if clothes enable small arms mode
|
||||
*/
|
||||
boolean hasClothesWithSmallArms();
|
||||
|
||||
/**
|
||||
* Check if entity is both tied AND gagged.
|
||||
* Common check for "fully restrained" state.
|
||||
*
|
||||
* @return true if {@code isTiedUp() && isGagged()}
|
||||
*/
|
||||
boolean isBoundAndGagged();
|
||||
|
||||
/**
|
||||
* Check if gag item has gagging sound effect.
|
||||
* @return true if gag implements ItemGaggingEffect
|
||||
*/
|
||||
boolean hasGaggingEffect();
|
||||
|
||||
/**
|
||||
* Check if blindfold item has blinding visual effect.
|
||||
* @return true if blindfold implements IHasBlindingEffect
|
||||
*/
|
||||
boolean hasBlindingEffect();
|
||||
|
||||
/**
|
||||
* Check if entity has knives in inventory (for struggling).
|
||||
* @return true if inventory contains knife items
|
||||
*/
|
||||
boolean hasKnives();
|
||||
|
||||
// ========================================
|
||||
// BULK OPERATIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Apply a full set of bondage equipment at once.
|
||||
*
|
||||
* <p>Typically used when:</p>
|
||||
* <ul>
|
||||
* <li>EntityKidnapper captures a player</li>
|
||||
* <li>Trapped bed activates</li>
|
||||
* <li>Rope arrow hits target</li>
|
||||
* <li>Admin command applies full restraint</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param bind Bind ItemStack (or empty)
|
||||
* @param gag Gag ItemStack (or empty)
|
||||
* @param blindfold Blindfold ItemStack (or empty)
|
||||
* @param earplugs Earplugs ItemStack (or empty)
|
||||
* @param collar Collar ItemStack (or empty)
|
||||
* @param clothes Clothes ItemStack (or empty)
|
||||
*/
|
||||
void applyBondage(
|
||||
ItemStack bind,
|
||||
ItemStack gag,
|
||||
ItemStack blindfold,
|
||||
ItemStack earplugs,
|
||||
ItemStack collar,
|
||||
ItemStack clothes
|
||||
);
|
||||
|
||||
/**
|
||||
* Untie this entity, removing all bondage items.
|
||||
*
|
||||
* @param drop If true, drop items on ground. If false, items are deleted.
|
||||
*/
|
||||
void untie(boolean drop);
|
||||
|
||||
/**
|
||||
* Drop all equipped bondage items on the ground.
|
||||
* Equivalent to {@code dropBondageItems(true)}.
|
||||
*/
|
||||
void dropBondageItems(boolean drop);
|
||||
|
||||
/**
|
||||
* Drop specific bondage items with granular control.
|
||||
*
|
||||
* @param drop If false, skip all drops (no-op)
|
||||
* @param dropBind If true, drop bind item
|
||||
*/
|
||||
void dropBondageItems(boolean drop, boolean dropBind);
|
||||
|
||||
/**
|
||||
* Drop bondage items with full granular control.
|
||||
*
|
||||
* @param drop If false, skip all drops (no-op)
|
||||
* @param dropBind If true, drop bind
|
||||
* @param dropGag If true, drop gag
|
||||
* @param dropBlindfold If true, drop blindfold
|
||||
* @param dropEarplugs If true, drop earplugs
|
||||
* @param dropCollar If true, drop collar
|
||||
* @param dropClothes If true, drop clothes
|
||||
*/
|
||||
void dropBondageItems(
|
||||
boolean drop,
|
||||
boolean dropBind,
|
||||
boolean dropGag,
|
||||
boolean dropBlindfold,
|
||||
boolean dropEarplugs,
|
||||
boolean dropCollar,
|
||||
boolean dropClothes
|
||||
);
|
||||
|
||||
/**
|
||||
* Drop only clothes item.
|
||||
*/
|
||||
void dropClothes();
|
||||
|
||||
/**
|
||||
* Count how many bondage items can be removed (are not locked).
|
||||
*
|
||||
* @return Count of removable items (0-6)
|
||||
*/
|
||||
int getBondageItemsWhichCanBeRemovedCount();
|
||||
|
||||
// ========================================
|
||||
// CLOTHES PERMISSION SYSTEM
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if a player can remove clothes from this entity.
|
||||
* Used for permission checks in multiplayer.
|
||||
*
|
||||
* @param player The player attempting to remove clothes
|
||||
* @return true if allowed
|
||||
*/
|
||||
boolean canTakeOffClothes(Player player);
|
||||
|
||||
/**
|
||||
* Check if a player can change clothes on this entity.
|
||||
*
|
||||
* @param player The player attempting to change clothes
|
||||
* @return true if allowed
|
||||
*/
|
||||
boolean canChangeClothes(Player player);
|
||||
|
||||
/**
|
||||
* Check if clothes can be changed (global permission).
|
||||
*
|
||||
* @return true if clothes are changeable
|
||||
*/
|
||||
boolean canChangeClothes();
|
||||
|
||||
// ========================================
|
||||
// POST-APPLY CALLBACKS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Called after a bind is applied.
|
||||
* Implementations can trigger additional effects (sounds, particles, etc.).
|
||||
*
|
||||
* Phase 14.1.7: Added missing callback (was documented but not defined)
|
||||
*/
|
||||
default void checkBindAfterApply() {
|
||||
// Default: no-op
|
||||
// NPCs don't need post-apply logic by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a gag is applied.
|
||||
* Implementations can trigger additional effects (sounds, particles, etc.).
|
||||
*/
|
||||
default void checkGagAfterApply() {
|
||||
// Default: no-op
|
||||
// NPCs don't need post-apply logic by default
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a blindfold is applied.
|
||||
*/
|
||||
default void checkBlindfoldAfterApply() {
|
||||
// Default: no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after earplugs are applied.
|
||||
*/
|
||||
default void checkEarplugsAfterApply() {
|
||||
// Default: no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after a collar is applied.
|
||||
*/
|
||||
default void checkCollarAfterApply() {
|
||||
// Default: no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after mittens are applied.
|
||||
* Phase 14.4: Mittens system
|
||||
*/
|
||||
default void checkMittensAfterApply() {
|
||||
// Default: no-op
|
||||
// NPCs don't need post-apply logic by default
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// LOCK SAFETY HELPERS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Execute a runnable only if the item is unlocked.
|
||||
*
|
||||
* <p><b>Lock Check:</b> If {@code stack} is ILockable and locked, runnable is NOT executed.</p>
|
||||
*
|
||||
* <p><b>Usage Example:</b></p>
|
||||
* <pre>{@code
|
||||
* ifUnlocked(currentGag, false, () -> {
|
||||
* // Remove gag code here
|
||||
* });
|
||||
* }</pre>
|
||||
*
|
||||
* @param stack The item to check
|
||||
* @param force If true, ignore lock (NOT USED in default implementation - for consistency)
|
||||
* @param run The code to execute if unlocked
|
||||
*/
|
||||
default void ifUnlocked(ItemStack stack, boolean force, Runnable run) {
|
||||
if (!stack.isEmpty()) {
|
||||
if (
|
||||
stack.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(stack)
|
||||
) {
|
||||
return; // Locked - don't run
|
||||
}
|
||||
run.run(); // Unlocked or not lockable - safe to run
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is locked.
|
||||
*
|
||||
* @param stack The item to check
|
||||
* @param force If true, returns false (treat as unlocked)
|
||||
* @return true if item is locked AND force is false
|
||||
*/
|
||||
default boolean isLocked(ItemStack stack, boolean force) {
|
||||
return (
|
||||
!force &&
|
||||
stack.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(stack)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a value based on item lock state.
|
||||
*
|
||||
* <p><b>Usage Example:</b></p>
|
||||
* <pre>{@code
|
||||
* return ifUnlockedReturn(currentGag, false,
|
||||
* () -> removeAndReturnGag(), // If unlocked
|
||||
* () -> ItemStack.EMPTY // If locked
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* @param stack The item to check
|
||||
* @param force If true, always returns {@code run.get()}
|
||||
* @param run Supplier to call if unlocked
|
||||
* @param def Supplier to call if locked or stack is empty
|
||||
* @param <T> Return type
|
||||
* @return Result from run or def supplier
|
||||
*/
|
||||
default <T> T ifUnlockedReturn(
|
||||
ItemStack stack,
|
||||
boolean force,
|
||||
Supplier<T> run,
|
||||
Supplier<T> def
|
||||
) {
|
||||
if (stack.isEmpty()) {
|
||||
return def.get();
|
||||
}
|
||||
if (
|
||||
stack.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(stack) &&
|
||||
!force
|
||||
) {
|
||||
return def.get(); // Locked
|
||||
}
|
||||
return run.get(); // Unlocked
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a bondage item if it's unlocked, then drop it.
|
||||
*
|
||||
* <p>Helper method combining lock check + take + drop logic.</p>
|
||||
*
|
||||
* @param stack The current item in slot
|
||||
* @param takeOff Supplier that removes and returns the item
|
||||
*/
|
||||
default void takeBondageItemIfUnlocked(
|
||||
ItemStack stack,
|
||||
Supplier<ItemStack> takeOff
|
||||
) {
|
||||
if (stack.isEmpty()) return;
|
||||
// Locked ILockable items cannot be removed
|
||||
if (stack.getItem() instanceof ILockable lockable && lockable.isLocked(stack)) return;
|
||||
// Non-ILockable items or unlocked items: remove and drop
|
||||
ItemStack removed = takeOff.get();
|
||||
if (!removed.isEmpty()) {
|
||||
kidnappedDropItem(removed);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RESISTANCE SYSTEM (Phase 14.1.7)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get current bind resistance value.
|
||||
*
|
||||
* <p><b>Resistance System:</b></p>
|
||||
* Higher resistance = harder to struggle out of restraints.
|
||||
* Resistance decreases with each struggle attempt.
|
||||
* When resistance reaches 0, the entity escapes.
|
||||
*
|
||||
* <p><b>For Players:</b> Stored in NBT, persists across sessions
|
||||
* <p><b>For NPCs:</b> Default returns 0 (instant escape, can be overridden)
|
||||
*
|
||||
* Phase 14.1.7: Added to enable struggle system for NPCs
|
||||
*
|
||||
* @return Current resistance value (0 = can escape)
|
||||
*/
|
||||
default int getCurrentBindResistance() {
|
||||
// Default for NPCs: 0 resistance (they escape instantly)
|
||||
// PlayerBindState overrides this to return NBT-stored value
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current bind resistance.
|
||||
*
|
||||
* <p><b>For Players:</b> Updates NBT storage
|
||||
* <p><b>For NPCs:</b> Default no-op (no persistent resistance)
|
||||
*
|
||||
* Phase 14.1.7: Added to enable struggle system for NPCs
|
||||
*
|
||||
* @param resistance New resistance value
|
||||
*/
|
||||
default void setCurrentBindResistance(int resistance) {
|
||||
// Default for NPCs: no-op (they don't store resistance)
|
||||
// PlayerBindState overrides this to update NBT
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current collar resistance value.
|
||||
*
|
||||
* <p>Same as bind resistance but for collars.
|
||||
*
|
||||
* Phase 14.1.7: Added for collar struggle system
|
||||
*
|
||||
* @return Current collar resistance value
|
||||
*/
|
||||
default int getCurrentCollarResistance() {
|
||||
// Default for NPCs: 0 resistance
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current collar resistance.
|
||||
*
|
||||
* Phase 14.1.7: Added for collar struggle system
|
||||
*
|
||||
* @param resistance New resistance value
|
||||
*/
|
||||
default void setCurrentCollarResistance(int resistance) {
|
||||
// Default for NPCs: no-op
|
||||
}
|
||||
}
|
||||
171
src/main/java/com/tiedup/remake/state/ICaptor.java
Normal file
171
src/main/java/com/tiedup/remake/state/ICaptor.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
/**
|
||||
* Phase 8: Master-Captive Relationships
|
||||
* Phase 14.1.6: Refactored to use IRestrainable for NPC capture support
|
||||
* Phase 17: Terminology refactoring - slave → captive
|
||||
* C6-V2: Narrowed parameters from IRestrainable to IBondageState (minimum needed type)
|
||||
*
|
||||
* Interface for entities that can capture other entities (players or NPCs).
|
||||
*
|
||||
* Terminology (Phase 17):
|
||||
* - "Captive" = Entity attached by leash (active physical control)
|
||||
* - "Slave" = Entity wearing a collar owned by someone (passive ownership via CollarRegistry)
|
||||
*
|
||||
* Design Pattern:
|
||||
* - Interface-based design allows both players (PlayerCaptorManager)
|
||||
* and NPCs (EntityKidnapper - Phase 14.2+) to act as captors
|
||||
* - Separates concerns: ICaptor manages captives, IBondageState is managed
|
||||
*
|
||||
* Implementation:
|
||||
* - PlayerCaptorManager: For player captors (Phase 8)
|
||||
* - EntityKidnapper: For NPC captors (Phase 14.2+)
|
||||
*
|
||||
* @see IBondageState
|
||||
* @see PlayerCaptorManager
|
||||
*/
|
||||
public interface ICaptor {
|
||||
// ========================================
|
||||
// Captive Management
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Add a captive to this captor's captive list.
|
||||
* Called when capture succeeds.
|
||||
*
|
||||
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
|
||||
* Phase 17: Renamed from addSlave to addCaptive
|
||||
* C6-V2: Narrowed from IRestrainable to IBondageState
|
||||
*
|
||||
* @param captive The IBondageState entity to capture
|
||||
*/
|
||||
void addCaptive(IBondageState captive);
|
||||
|
||||
/**
|
||||
* Remove a captive from this captor's captive list.
|
||||
* Called when freeing a captive or when captive escapes.
|
||||
*
|
||||
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
|
||||
* Phase 17: Renamed from removeSlave to removeCaptive
|
||||
* C6-V2: Narrowed from IRestrainable to IBondageState
|
||||
*
|
||||
* @param captive The IBondageState captive to remove
|
||||
* @param transportState If true, also despawn the transport entity
|
||||
*/
|
||||
void removeCaptive(IBondageState captive, boolean transportState);
|
||||
|
||||
/**
|
||||
* Check if this captor can capture the given target.
|
||||
*
|
||||
* Conditions (from original):
|
||||
* - Target must be tied up OR have collar with this captor as owner
|
||||
* - Target must not already be captured
|
||||
*
|
||||
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
|
||||
* Phase 17: Renamed from canEnslave to canCapture
|
||||
* C6-V2: Narrowed from IRestrainable to IBondageState
|
||||
*
|
||||
* @param target The potential IBondageState captive
|
||||
* @return true if capture is allowed
|
||||
*/
|
||||
boolean canCapture(IBondageState target);
|
||||
|
||||
/**
|
||||
* Check if this captor can release the given captive.
|
||||
* Only the current captor can release their captive.
|
||||
*
|
||||
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
|
||||
* Phase 17: Renamed from canFree to canRelease
|
||||
* C6-V2: Narrowed from IRestrainable to IBondageState
|
||||
*
|
||||
* @param captive The IBondageState captive to check
|
||||
* @return true if this captor is the captive's captor
|
||||
*/
|
||||
boolean canRelease(IBondageState captive);
|
||||
|
||||
// ========================================
|
||||
// Configuration
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Whether this captor allows captives to be transferred to other captors.
|
||||
*
|
||||
* Phase 17: Renamed from allowSlaveTransfer to allowCaptiveTransfer
|
||||
*
|
||||
* @return true if captive transfer is allowed (default for players)
|
||||
*/
|
||||
boolean allowCaptiveTransfer();
|
||||
|
||||
/**
|
||||
* Whether this captor can have multiple captives simultaneously.
|
||||
*
|
||||
* Phase 17: Renamed from allowMultipleSlaves to allowMultipleCaptives
|
||||
*
|
||||
* @return true if multiple captives allowed (default for players)
|
||||
*/
|
||||
boolean allowMultipleCaptives();
|
||||
|
||||
// ========================================
|
||||
// Event Callbacks
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Called when a captive logs out while captured.
|
||||
* Allows the captor to handle cleanup or persistence.
|
||||
*
|
||||
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
|
||||
* Phase 17: Renamed from onSlaveLogout to onCaptiveLogout
|
||||
* C6-V2: Narrowed from IRestrainable to IBondageState
|
||||
* Note: For NPC captives, this may never be called (NPCs don't log out)
|
||||
*
|
||||
* @param captive The IBondageState captive that logged out
|
||||
*/
|
||||
void onCaptiveLogout(IBondageState captive);
|
||||
|
||||
/**
|
||||
* Called when a captive is released (freed).
|
||||
* Allows the captor to react to losing a captive.
|
||||
*
|
||||
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
|
||||
* Phase 17: Renamed from onSlaveReleased to onCaptiveReleased
|
||||
* C6-V2: Narrowed from IRestrainable to IBondageState
|
||||
*
|
||||
* @param captive The IBondageState captive that was released
|
||||
*/
|
||||
void onCaptiveReleased(IBondageState captive);
|
||||
|
||||
/**
|
||||
* Called when a captive attempts to struggle.
|
||||
* Allows the captor to react (e.g., shock collar activation).
|
||||
*
|
||||
* Phase 14.1.6: Changed from PlayerBindState to IRestrainable
|
||||
* Phase 17: Renamed from onSlaveStruggle to onCaptiveStruggle
|
||||
* C6-V2: Narrowed from IRestrainable to IBondageState
|
||||
*
|
||||
* @param captive The IBondageState captive that struggled
|
||||
*/
|
||||
void onCaptiveStruggle(IBondageState captive);
|
||||
|
||||
// ========================================
|
||||
// Queries
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if this captor currently has any captives.
|
||||
*
|
||||
* Phase 17: Renamed from hasSlaves to hasCaptives
|
||||
*
|
||||
* @return true if captive list is not empty
|
||||
*/
|
||||
boolean hasCaptives();
|
||||
|
||||
/**
|
||||
* Get the entity representing this captor.
|
||||
* Used for lead attachment and position queries.
|
||||
*
|
||||
* @return The entity (Player or custom entity)
|
||||
*/
|
||||
Entity getEntity();
|
||||
}
|
||||
166
src/main/java/com/tiedup/remake/state/ICapturable.java
Normal file
166
src/main/java/com/tiedup/remake/state/ICapturable.java
Normal file
@@ -0,0 +1,166 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
/**
|
||||
* Capture, leash, and transport interface for kidnapped entities.
|
||||
*
|
||||
* <p>Covers the capture lifecycle: being captured by a captor, being freed,
|
||||
* being transferred to another captor, and querying capture state.</p>
|
||||
*
|
||||
* @see IRestrainableEntity
|
||||
* @see IBondageState
|
||||
* @see IRestrainable
|
||||
*/
|
||||
public interface ICapturable extends IRestrainableEntity {
|
||||
|
||||
// ========================================
|
||||
// CAPTURE LIFECYCLE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Capture this entity by the given captor.
|
||||
*
|
||||
* <p><b>Prerequisites:</b></p>
|
||||
* <ul>
|
||||
* <li>Must be tied up OR have collar with captor as owner</li>
|
||||
* <li>Must not already be captured ({@link #isCaptive()} == false)</li>
|
||||
* <li>Captor must allow capture ({@link ICaptor#canCapture(IRestrainable)})</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Process for Players:</b></p>
|
||||
* <ol>
|
||||
* <li>Validate conditions (tied up or has owner's collar)</li>
|
||||
* <li>Transfer any existing captives to new captor (if allowed)</li>
|
||||
* <li>Create LeashProxyEntity that follows the player</li>
|
||||
* <li>Attach leash from proxy to captor entity</li>
|
||||
* <li>Add captive to captor's captive list</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p><b>Process for NPCs:</b></p>
|
||||
* <ol>
|
||||
* <li>Validate conditions</li>
|
||||
* <li>Use vanilla setLeashedTo() directly on the NPC</li>
|
||||
* </ol>
|
||||
*
|
||||
* Phase 17: Renamed from getEnslavedBy to getCapturedBy
|
||||
*
|
||||
* @param captor The captor attempting to capture
|
||||
* @return true if capture succeeded, false otherwise
|
||||
*/
|
||||
boolean getCapturedBy(ICaptor captor);
|
||||
|
||||
/**
|
||||
* Free this captive from capture.
|
||||
* Equivalent to {@code free(true)}.
|
||||
*
|
||||
* <p>This will:</p>
|
||||
* <ul>
|
||||
* <li>Despawn the transport entity</li>
|
||||
* <li>Drop the lead item</li>
|
||||
* <li>Remove from captor's captive list</li>
|
||||
* </ul>
|
||||
*/
|
||||
void free();
|
||||
|
||||
/**
|
||||
* Free this captive from capture with transport state option.
|
||||
*
|
||||
* @param transportState If true, despawn the transport entity. If false, keep it (for transfer).
|
||||
*/
|
||||
void free(boolean transportState);
|
||||
|
||||
/**
|
||||
* Transfer this captive to a new captor.
|
||||
* Current captor loses the captive, new captor gains it.
|
||||
*
|
||||
* <p>Only works if current captor allows captive transfer
|
||||
* ({@link ICaptor#allowCaptiveTransfer()} == true).</p>
|
||||
*
|
||||
* Phase 17: Renamed from transferSlaveryTo to transferCaptivityTo
|
||||
*
|
||||
* @param newCaptor The new captor to transfer to
|
||||
*/
|
||||
void transferCaptivityTo(ICaptor newCaptor);
|
||||
|
||||
// ========================================
|
||||
// STATE QUERIES - CAPTURE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if this entity can be captured.
|
||||
*
|
||||
* <p><b>From original code (PlayerBindState.java:195-225):</b></p>
|
||||
* <ul>
|
||||
* <li>If tied up: Always capturable</li>
|
||||
* <li>If NOT tied up: Only capturable if has collar AND collar has captor as owner</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return true if capturable
|
||||
*/
|
||||
boolean isEnslavable();
|
||||
|
||||
/**
|
||||
* Check if this entity is currently captured (attached by leash).
|
||||
*
|
||||
* <p><b>For Players:</b> Returns true when LeashProxyEntity is attached and leashed to captor</p>
|
||||
* <p><b>For NPCs:</b> Returns true when vanilla leash is attached</p>
|
||||
*
|
||||
* Phase 17: Renamed from isSlave to isCaptive
|
||||
*
|
||||
* @return true if captured (has leash holder)
|
||||
*/
|
||||
boolean isCaptive();
|
||||
|
||||
/**
|
||||
* Check if this entity can be tied up (not already restrained).
|
||||
*
|
||||
* @return true if entity can accept bind items
|
||||
*/
|
||||
boolean canBeTiedUp();
|
||||
|
||||
/**
|
||||
* Check if this entity is tied to a pole (immobilized).
|
||||
*
|
||||
* @return true if tied to a static pole entity
|
||||
*/
|
||||
boolean isTiedToPole();
|
||||
|
||||
/**
|
||||
* Tie this entity to the closest fence/pole within range.
|
||||
* Searches for fence blocks near the entity's current position.
|
||||
*
|
||||
* @param searchRadius The radius in blocks to search for fences
|
||||
* @return true if successfully tied to a pole
|
||||
*/
|
||||
boolean tieToClosestPole(int searchRadius);
|
||||
|
||||
/**
|
||||
* Check if this entity can be auto-kidnapped by events.
|
||||
* Used by EntityKidnapper AI to determine valid targets.
|
||||
*
|
||||
* @return true if can be targeted by kidnapping events
|
||||
*/
|
||||
boolean canBeKidnappedByEvents();
|
||||
|
||||
/**
|
||||
* Get the current captor (the entity holding the leash).
|
||||
*
|
||||
* Phase 17: Renamed from getMaster to getCaptor
|
||||
*
|
||||
* @return The captor, or null if not captured
|
||||
*/
|
||||
ICaptor getCaptor();
|
||||
|
||||
/**
|
||||
* Get the leash proxy or transport entity for this captive.
|
||||
*
|
||||
* <p><b>For Players:</b> Returns the LeashProxyEntity following the player</p>
|
||||
* <p><b>For NPCs:</b> Returns null (NPCs use vanilla leash directly)</p>
|
||||
*
|
||||
* @return The proxy/transport entity, or null if not applicable
|
||||
*/
|
||||
@Nullable
|
||||
Entity getTransport();
|
||||
}
|
||||
102
src/main/java/com/tiedup/remake/state/ICoercible.java
Normal file
102
src/main/java/com/tiedup/remake/state/ICoercible.java
Normal file
@@ -0,0 +1,102 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/**
|
||||
* Active coercion interface for kidnapped entities.
|
||||
*
|
||||
* <p>Covers tightening binds, chloroform, electric shocks,
|
||||
* and forceful item removal by another entity.</p>
|
||||
*
|
||||
* @see IRestrainableEntity
|
||||
* @see IRestrainable
|
||||
*/
|
||||
// C6-V2: takeBondageItemBy narrowed from IRestrainable to IRestrainableEntity
|
||||
public interface ICoercible extends IRestrainableEntity {
|
||||
|
||||
// ========================================
|
||||
// SPECIAL INTERACTIONS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Tighten binds on this entity (increase resistance).
|
||||
* Called when master uses paddle/whip on slave.
|
||||
*
|
||||
* <p><b>Effects:</b></p>
|
||||
* <ul>
|
||||
* <li>Resets bind resistance to maximum</li>
|
||||
* <li>Plays slap/whip sound</li>
|
||||
* <li>Shows message to entity</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param tightener The player tightening the binds
|
||||
*/
|
||||
void tighten(Player tightener);
|
||||
|
||||
/**
|
||||
* Apply chloroform effect to this entity.
|
||||
*
|
||||
* <p><b>Effects (from original):</b></p>
|
||||
* <ul>
|
||||
* <li>Slowness effect (duration from parameter)</li>
|
||||
* <li>Weakness effect</li>
|
||||
* <li>Prevents movement/interaction</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param duration Effect duration in ticks
|
||||
*/
|
||||
void applyChloroform(int duration);
|
||||
|
||||
/**
|
||||
* Shock this kidnapped entity.
|
||||
* Uses default damage and no message.
|
||||
*
|
||||
* <p><b>Effects:</b></p>
|
||||
* <ul>
|
||||
* <li>Plays electric shock sound</li>
|
||||
* <li>Applies damage (default: 1.0F)</li>
|
||||
* <li>Shows shock particles (client-side)</li>
|
||||
* </ul>
|
||||
*/
|
||||
void shockKidnapped();
|
||||
|
||||
/**
|
||||
* Shock this kidnapped entity with custom message and damage.
|
||||
*
|
||||
* @param messageAddon Additional message to send to the entity
|
||||
* @param damage Damage amount to apply
|
||||
*/
|
||||
void shockKidnapped(String messageAddon, float damage);
|
||||
|
||||
/**
|
||||
* Another entity takes a bondage item from this entity.
|
||||
* Used when master removes items from slave.
|
||||
*
|
||||
* Phase 14.1.7: Changed from PlayerBindState to IRestrainable for polymorphism
|
||||
* C6-V2: Narrowed from IRestrainable to IRestrainableEntity (only uses identity methods)
|
||||
* This allows NPCs to take items from Players or other NPCs
|
||||
*
|
||||
* @param taker The IRestrainableEntity taking the item
|
||||
* @param slotIndex The slot index (0-5: bind, gag, blindfold, earplugs, collar, clothes)
|
||||
*/
|
||||
void takeBondageItemBy(IRestrainableEntity taker, int slotIndex);
|
||||
|
||||
// ========================================
|
||||
// COLLAR TIMERS (Phase 14.1.4)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Force-stops and clears any active auto-shock collar timer.
|
||||
*
|
||||
* <p>Called when:</p>
|
||||
* <ul>
|
||||
* <li>GPS collar is removed</li>
|
||||
* <li>Auto-shock collar is removed</li>
|
||||
* <li>Entity is freed from slavery</li>
|
||||
* </ul>
|
||||
*/
|
||||
default void resetAutoShockTimer() {
|
||||
// Default no-op (NPCs don't have timers by default)
|
||||
// PlayerBindState overrides this to clear timer
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import com.tiedup.remake.entities.LeashProxyEntity;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
/**
|
||||
* Interface for accessing leash-related fields injected into ServerPlayer via mixin.
|
||||
* Cast any ServerPlayer to this interface to access leash functionality.
|
||||
*
|
||||
* NOTE: This interface is in a separate package from mixins because mixin packages
|
||||
* cannot be referenced directly from regular code.
|
||||
*
|
||||
* Usage:
|
||||
* <pre>
|
||||
* if (player instanceof IPlayerLeashAccess access) {
|
||||
* access.tiedup$attachLeash(masterEntity);
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public interface IPlayerLeashAccess {
|
||||
/**
|
||||
* Attach a leash from this player to a holder entity.
|
||||
* Creates a LeashProxyEntity if not already present.
|
||||
*
|
||||
* @param holder The entity holding the leash (master player, NPC, or fence knot)
|
||||
*/
|
||||
void tiedup$attachLeash(Entity holder);
|
||||
|
||||
/**
|
||||
* Detach the leash from this player.
|
||||
* Discards the LeashProxyEntity.
|
||||
*/
|
||||
void tiedup$detachLeash();
|
||||
|
||||
/**
|
||||
* Drop a lead item when detaching.
|
||||
*/
|
||||
void tiedup$dropLeash();
|
||||
|
||||
/**
|
||||
* Check if this player is currently leashed.
|
||||
*
|
||||
* @return true if leashed to an entity
|
||||
*/
|
||||
boolean tiedup$isLeashed();
|
||||
|
||||
/**
|
||||
* Get the entity holding this player's leash.
|
||||
*
|
||||
* @return The leash holder, or null if not leashed
|
||||
*/
|
||||
Entity tiedup$getLeashHolder();
|
||||
|
||||
/**
|
||||
* Get the proxy entity used for leash rendering.
|
||||
*
|
||||
* @return The LeashProxyEntity, or null if not leashed
|
||||
*/
|
||||
LeashProxyEntity tiedup$getLeashProxy();
|
||||
|
||||
/**
|
||||
* Tick the leash system - check validity and apply traction.
|
||||
* Called from Forge TickEvent.PlayerTickEvent.
|
||||
*/
|
||||
void tiedup$tickLeash();
|
||||
|
||||
/**
|
||||
* Set extra slack on the leash (increases pull start and max distances).
|
||||
* Used during "pet leads" dogwalk so the player can walk ahead without being yanked back.
|
||||
*
|
||||
* @param slack Extra distance in blocks (0.0 = no slack)
|
||||
*/
|
||||
void tiedup$setLeashSlack(double slack);
|
||||
|
||||
/**
|
||||
* Get the current leash slack value.
|
||||
*
|
||||
* @return Extra slack distance in blocks
|
||||
*/
|
||||
double tiedup$getLeashSlack();
|
||||
}
|
||||
40
src/main/java/com/tiedup/remake/state/IRestrainable.java
Normal file
40
src/main/java/com/tiedup/remake/state/IRestrainable.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
/**
|
||||
* Union interface for entities that participate in the full restraint system.
|
||||
*
|
||||
* <p>Prefer using the narrowest sub-interface for your needs:</p>
|
||||
* <ul>
|
||||
* <li>{@link IBondageState} — equipment, state checks, capture, identity (most common)</li>
|
||||
* <li>{@link ICoercible} — shock, chloroform, tighten</li>
|
||||
* <li>{@link ISaleable} — sale system</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Use {@code IRestrainable} only when you genuinely need methods from multiple
|
||||
* branches (e.g., both bondage state AND coercion, or both bondage AND sale).</p>
|
||||
*
|
||||
* <p><b>Inheritance hierarchy:</b></p>
|
||||
* <pre>
|
||||
* IRestrainableEntity (base)
|
||||
* +-- ICapturable extends IRestrainableEntity
|
||||
* | +-- IBondageState extends ICapturable
|
||||
* +-- ICoercible extends IRestrainableEntity
|
||||
*
|
||||
* ISaleable (standalone)
|
||||
*
|
||||
* IRestrainable extends IBondageState, ICoercible, ISaleable
|
||||
* </pre>
|
||||
*
|
||||
* <p><b>Implementors:</b> PlayerBindState, DamselBondageManager,
|
||||
* MCAKidnappedAdapter</p>
|
||||
*
|
||||
* @see IRestrainableEntity
|
||||
* @see ICapturable
|
||||
* @see IBondageState
|
||||
* @see ICoercible
|
||||
* @see ISaleable
|
||||
* @see PlayerBindState
|
||||
*/
|
||||
public interface IRestrainable extends IBondageState, ICoercible, ISaleable {
|
||||
// Empty union body — all methods inherited from sub-interfaces.
|
||||
}
|
||||
207
src/main/java/com/tiedup/remake/state/IRestrainableEntity.java
Normal file
207
src/main/java/com/tiedup/remake/state/IRestrainableEntity.java
Normal file
@@ -0,0 +1,207 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import java.util.UUID;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Base identity, lifecycle, and utility interface for kidnapped entities.
|
||||
*
|
||||
* <p>Provides the fundamental identity methods ({@link #asLivingEntity()},
|
||||
* {@link #getKidnappedUniqueId()}, etc.), death handling, and default
|
||||
* helper methods for communication and movement queries.</p>
|
||||
*
|
||||
* <p>Also hosts the V2 region-based equipment API defaults, which delegate
|
||||
* to {@code V2EquipmentHelper} and work automatically for Players.</p>
|
||||
*
|
||||
* @see ICapturable
|
||||
* @see IBondageState
|
||||
* @see ICoercible
|
||||
* @see IRestrainable
|
||||
*/
|
||||
public interface IRestrainableEntity {
|
||||
|
||||
// ========================================
|
||||
// ENTITY IDENTITY
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the underlying LivingEntity.
|
||||
*
|
||||
* <p>For Players: return the player
|
||||
* <p>For NPCs: return this (cast to LivingEntity)
|
||||
*
|
||||
* @return The LivingEntity backing this IRestrainable instance
|
||||
*/
|
||||
LivingEntity asLivingEntity();
|
||||
|
||||
/**
|
||||
* Get the unique ID of this kidnapped entity.
|
||||
*
|
||||
* @return The entity's UUID
|
||||
*/
|
||||
UUID getKidnappedUniqueId();
|
||||
|
||||
/**
|
||||
* Get the display name of this kidnapped entity.
|
||||
* For players: player name. For NPCs: entity custom name or default name.
|
||||
*
|
||||
* @return The entity's name as string
|
||||
*/
|
||||
String getKidnappedName();
|
||||
|
||||
/**
|
||||
* Get the name from the collar's NBT tag (nickname).
|
||||
* If collar has no nickname, returns empty string or entity name.
|
||||
*
|
||||
* @return The collar nickname, or fallback name
|
||||
*/
|
||||
String getNameFromCollar();
|
||||
|
||||
// ========================================
|
||||
// LIFECYCLE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Called when this entity dies while kidnapped.
|
||||
*
|
||||
* <p><b>From original (PlayerBindState.java:1395-1461):</b></p>
|
||||
* <ul>
|
||||
* <li>Unlock all locked items</li>
|
||||
* <li>Drop bondage items (if configured)</li>
|
||||
* <li>Free from slavery</li>
|
||||
* <li>Reset state</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param world The world the entity died in
|
||||
* @return true if death was handled as kidnapped entity
|
||||
*/
|
||||
boolean onDeathKidnapped(Level world);
|
||||
|
||||
// ========================================
|
||||
// UTILITY
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Drop an item at this entity's position.
|
||||
* Helper method for dropping bondage items.
|
||||
*
|
||||
* @param stack The ItemStack to drop
|
||||
*/
|
||||
void kidnappedDropItem(ItemStack stack);
|
||||
|
||||
/**
|
||||
* Teleport this entity to a specific position.
|
||||
* Used by collar teleport commands and warp points.
|
||||
*
|
||||
* @param position The target position (x, y, z, dimension)
|
||||
*/
|
||||
void teleportToPosition(com.tiedup.remake.util.teleport.Position position);
|
||||
|
||||
// ========================================
|
||||
// DEFAULT HELPER METHODS - COMMUNICATION & STATE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Send a message to this entity (if it can receive messages).
|
||||
*
|
||||
* <p>For Players: Uses displayClientMessage()
|
||||
* <p>For NPCs: No-op (NPCs don't need visual feedback)
|
||||
*
|
||||
* @param message The message component to send
|
||||
* @param actionBar If true, shows message in action bar; if false, shows in chat
|
||||
*/
|
||||
default void sendMessage(Component message, boolean actionBar) {
|
||||
LivingEntity entity = asLivingEntity();
|
||||
if (entity instanceof Player player) {
|
||||
player.displayClientMessage(message, actionBar);
|
||||
}
|
||||
// NPCs: silent no-op (acceptable degradation)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity is currently sprinting.
|
||||
*
|
||||
* <p>For Players: player.isSprinting()
|
||||
* <p>For NPCs: Checks movement speed
|
||||
*
|
||||
* @return true if entity is sprinting
|
||||
*/
|
||||
default boolean isSprinting() {
|
||||
LivingEntity entity = asLivingEntity();
|
||||
if (entity instanceof Player player) {
|
||||
return player.isSprinting();
|
||||
}
|
||||
// For NPCs: check movement speed
|
||||
return entity.getDeltaMovement().horizontalDistanceSqr() > 0.1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if entity is currently swimming.
|
||||
*
|
||||
* @return true if entity is swimming
|
||||
*/
|
||||
default boolean isSwimming() {
|
||||
return asLivingEntity().isSwimming();
|
||||
}
|
||||
|
||||
// ==================== V2 REGION-BASED API ====================
|
||||
// These default methods delegate to V2EquipmentHelper.
|
||||
// They work automatically for Players (V2 capability attached since Epic 0).
|
||||
// For Damsels, requires IV2EquipmentHolder implementation (Epic 4B).
|
||||
|
||||
/**
|
||||
* Get all V2 equipped items (de-duplicated).
|
||||
* @return Unmodifiable map of region to ItemStack, or empty map if no V2 support.
|
||||
*/
|
||||
default java.util.Map<com.tiedup.remake.v2.BodyRegionV2, ItemStack> getAllEquippedV2() {
|
||||
com.tiedup.remake.v2.bondage.IV2BondageEquipment equip =
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.getEquipment(asLivingEntity());
|
||||
return equip != null ? equip.getAllEquipped() : java.util.Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get V2 item in a specific body region.
|
||||
*/
|
||||
default ItemStack getItemInRegion(com.tiedup.remake.v2.BodyRegionV2 region) {
|
||||
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.getInRegion(asLivingEntity(), region);
|
||||
}
|
||||
|
||||
/**
|
||||
* Equip a V2 item to its declared regions. Server-only.
|
||||
* @return The equip result.
|
||||
*/
|
||||
default com.tiedup.remake.v2.bondage.V2EquipResult equipToRegion(ItemStack stack) {
|
||||
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.equipItem(asLivingEntity(), stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unequip V2 item from a region. Server-only.
|
||||
*/
|
||||
default ItemStack unequipFromRegion(com.tiedup.remake.v2.BodyRegionV2 region) {
|
||||
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.unequipFromRegion(asLivingEntity(), region);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unequip V2 item from a region with force option. Server-only.
|
||||
*/
|
||||
default ItemStack unequipFromRegion(com.tiedup.remake.v2.BodyRegionV2 region, boolean force) {
|
||||
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.unequipFromRegion(asLivingEntity(), region, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this entity has V2 equipment support.
|
||||
*/
|
||||
default boolean hasV2Support() {
|
||||
return com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper
|
||||
.getEquipment(asLivingEntity()) != null;
|
||||
}
|
||||
}
|
||||
39
src/main/java/com/tiedup/remake/state/ISaleable.java
Normal file
39
src/main/java/com/tiedup/remake/state/ISaleable.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import com.tiedup.remake.util.tasks.ItemTask;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Standalone sale state interface for entities that can be put up for sale.
|
||||
*
|
||||
* @see IRestrainable
|
||||
*/
|
||||
public interface ISaleable {
|
||||
|
||||
/**
|
||||
* Check if this entity is marked for sale by a captor.
|
||||
*
|
||||
* @return true if captive is being sold
|
||||
*/
|
||||
boolean isForSell();
|
||||
|
||||
/**
|
||||
* Get the sale price for this captive.
|
||||
*
|
||||
* @return The sale price ItemTask, or null if not for sale
|
||||
*/
|
||||
@Nullable
|
||||
ItemTask getSalePrice();
|
||||
|
||||
/**
|
||||
* Mark this captive as for sale with the given price.
|
||||
*
|
||||
* @param price The sale price
|
||||
*/
|
||||
void putForSale(ItemTask price);
|
||||
|
||||
/**
|
||||
* Cancel the sale and reset sale state.
|
||||
*/
|
||||
void cancelSale();
|
||||
}
|
||||
1283
src/main/java/com/tiedup/remake/state/PlayerBindState.java
Normal file
1283
src/main/java/com/tiedup/remake/state/PlayerBindState.java
Normal file
File diff suppressed because it is too large
Load Diff
427
src/main/java/com/tiedup/remake/state/PlayerCaptorManager.java
Normal file
427
src/main/java/com/tiedup/remake/state/PlayerCaptorManager.java
Normal file
@@ -0,0 +1,427 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
// C6-V2: IRestrainable → IBondageState (narrowed API)
|
||||
|
||||
/**
|
||||
* Phase 8: Master-Captive Relationships
|
||||
* Phase 17: Renamed from PlayerKidnapperManager, terminology slave → captive
|
||||
*
|
||||
* Manages capture relationships for player captors.
|
||||
*
|
||||
* Terminology (Phase 17):
|
||||
* - "Captive" = Entity attached by leash (active physical control)
|
||||
* - "Slave" = Entity wearing a collar owned by someone (passive ownership via CollarRegistry)
|
||||
*
|
||||
* Features:
|
||||
* - Supports multiple captives simultaneously
|
||||
* - Allows captive transfer between captors
|
||||
* - Tracks captive list persistently
|
||||
* - Handles cleanup on captive logout/escape
|
||||
*
|
||||
* Thread Safety:
|
||||
* - Uses CopyOnWriteArrayList to avoid ConcurrentModificationException
|
||||
* - Safe for iteration during modification
|
||||
*
|
||||
* Design:
|
||||
* - Each PlayerBindState has one PlayerCaptorManager
|
||||
* - Manager tracks all captives owned by that player
|
||||
* - Implements ICaptor interface for polymorphic usage
|
||||
*
|
||||
* @see ICaptor
|
||||
* @see PlayerBindState
|
||||
*/
|
||||
public class PlayerCaptorManager implements ICaptor {
|
||||
|
||||
/**
|
||||
* The player who owns this manager (the captor).
|
||||
*/
|
||||
private final Player captor;
|
||||
|
||||
/**
|
||||
* List of all captives currently owned by this captor.
|
||||
* Thread-safe to avoid concurrent modification during iteration.
|
||||
*
|
||||
* Phase 14.1.6: Changed from List<PlayerBindState> to List<IBondageState>
|
||||
* Phase 17: Renamed from slaves to captives
|
||||
*/
|
||||
private final List<IBondageState> captives;
|
||||
|
||||
/**
|
||||
* Create a new captor manager for the given player.
|
||||
*
|
||||
* @param captor The player who will be the captor
|
||||
*/
|
||||
public PlayerCaptorManager(Player captor) {
|
||||
this.captor = captor;
|
||||
this.captives = new CopyOnWriteArrayList<>();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ICaptor Implementation
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
|
||||
* Phase 17: Renamed from addSlave to addCaptive
|
||||
*/
|
||||
@Override
|
||||
public synchronized void addCaptive(IBondageState captive) {
|
||||
if (captive == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[PlayerCaptorManager] Attempted to add null captive"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!captives.contains(captive)) {
|
||||
captives.add(captive);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PlayerCaptorManager] {} captured {} (total captives: {})",
|
||||
captor.getName().getString(),
|
||||
captive.getKidnappedName(),
|
||||
captives.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
|
||||
* Phase 17: Renamed from removeSlave to removeCaptive
|
||||
*
|
||||
* Thread Safety: Synchronized on 'this' to match addCaptive and freeAllCaptives.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void removeCaptive(
|
||||
IBondageState captive,
|
||||
boolean transportState
|
||||
) {
|
||||
if (captive == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[PlayerCaptorManager] Attempted to remove null captive"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (captives.remove(captive)) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PlayerCaptorManager] {} freed {} (remaining captives: {})",
|
||||
captor.getName().getString(),
|
||||
captive.getKidnappedName(),
|
||||
captives.size()
|
||||
);
|
||||
|
||||
// If requested, also despawn the transport entity
|
||||
if (transportState && captive.getTransport() != null) {
|
||||
captive.getTransport().discard();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
|
||||
* Phase 17: Renamed from canEnslave to canCapture
|
||||
*/
|
||||
@Override
|
||||
public boolean canCapture(IBondageState target) {
|
||||
if (target == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// From original code (PlayerBindState.java:195-225):
|
||||
// Can capture if:
|
||||
// - Target is tied up, OR
|
||||
// - Target has collar AND collar has this captor as owner
|
||||
|
||||
// Phase 14.1.6: Use asLivingEntity() instead of getPlayer()
|
||||
net.minecraft.world.entity.LivingEntity targetEntity =
|
||||
target.asLivingEntity();
|
||||
if (targetEntity == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if target is tied up
|
||||
if (target.isTiedUp()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if target has collar with this captor as owner
|
||||
if (target.hasCollar()) {
|
||||
ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemCollar collarItem) {
|
||||
if (
|
||||
collarItem.getOwners(collar).contains(this.captor.getUUID())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
|
||||
* Phase 17: Renamed from canFree to canRelease
|
||||
*/
|
||||
@Override
|
||||
public boolean canRelease(IBondageState captive) {
|
||||
if (captive == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can only release if this manager is the captive's captor
|
||||
return captive.getCaptor() == this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 17: Renamed from allowSlaveTransfer to allowCaptiveTransfer
|
||||
*/
|
||||
@Override
|
||||
public boolean allowCaptiveTransfer() {
|
||||
// Players always allow captive transfer
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 17: Renamed from allowMultipleSlaves to allowMultipleCaptives
|
||||
*/
|
||||
@Override
|
||||
public boolean allowMultipleCaptives() {
|
||||
// Players can have multiple captives
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
|
||||
* Phase 17: Renamed from onSlaveLogout to onCaptiveLogout
|
||||
* Note: For NPC captives, this is never called (NPCs don't log out)
|
||||
*/
|
||||
@Override
|
||||
public void onCaptiveLogout(IBondageState captive) {
|
||||
if (captive == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PlayerCaptorManager] Captive {} logged out while captured by {}",
|
||||
captive.getKidnappedName(),
|
||||
captor.getName().getString()
|
||||
);
|
||||
|
||||
// Keep captive in list - they might reconnect
|
||||
// Transport entity will despawn after timeout
|
||||
// On reconnect, checkStillCaptive() will clean up if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
|
||||
* Phase 17: Renamed from onSlaveReleased to onCaptiveReleased
|
||||
*/
|
||||
@Override
|
||||
public void onCaptiveReleased(IBondageState captive) {
|
||||
if (captive == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PlayerCaptorManager] Captive {} was released from {}",
|
||||
captive.getKidnappedName(),
|
||||
captor.getName().getString()
|
||||
);
|
||||
|
||||
// No special action needed - already removed from list by removeCaptive()
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 14.1.6: Changed parameter from PlayerBindState to IBondageState
|
||||
* Phase 17: Renamed from onSlaveStruggle to onCaptiveStruggle
|
||||
*/
|
||||
@Override
|
||||
public void onCaptiveStruggle(IBondageState captive) {
|
||||
if (captive == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerCaptorManager] Captive {} struggled (captor: {})",
|
||||
captive.getKidnappedName(),
|
||||
captor.getName().getString()
|
||||
);
|
||||
|
||||
// Phase 8: No action for basic struggle
|
||||
// Phase 14: Shock collar would activate here
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 17: Renamed from hasSlaves to hasCaptives
|
||||
*/
|
||||
@Override
|
||||
public boolean hasCaptives() {
|
||||
return !captives.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getEntity() {
|
||||
return captor;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Additional Methods
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Frees all captives currently owned by this captor.
|
||||
*
|
||||
* Phase 17: Renamed from freeAllSlaves to freeAllCaptives
|
||||
*
|
||||
* Thread Safety: Synchronized on 'this' to match addCaptive and removeCaptive.
|
||||
*
|
||||
* @param transportState If true, destroy the transporter entities
|
||||
*/
|
||||
public synchronized void freeAllCaptives(boolean transportState) {
|
||||
// Use a copy to avoid concurrent modification while iterating
|
||||
List<IBondageState> copy = new ArrayList<>(captives);
|
||||
for (IBondageState captive : copy) {
|
||||
captive.free(transportState);
|
||||
}
|
||||
captives.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees all captives with default behavior (destroy transport entities).
|
||||
*/
|
||||
public void freeAllCaptives() {
|
||||
freeAllCaptives(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of the captive list.
|
||||
* Safe for iteration without concurrent modification issues.
|
||||
*
|
||||
* Phase 17: Renamed from getSlaves to getCaptives
|
||||
*
|
||||
* @return Copy of the current captive list
|
||||
*/
|
||||
public List<IBondageState> getCaptives() {
|
||||
return new ArrayList<>(captives);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of captives currently owned.
|
||||
*
|
||||
* Phase 17: Renamed from getSlaveCount to getCaptiveCount
|
||||
*
|
||||
* @return Captive count
|
||||
*/
|
||||
public int getCaptiveCount() {
|
||||
return captives.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer all captives from this captor to a new captor.
|
||||
* Used when this player gets captured themselves.
|
||||
*
|
||||
* From original code:
|
||||
* - When a player gets captured, their captives transfer to new captor
|
||||
* - Prevents circular capture issues
|
||||
*
|
||||
* Phase 17: Renamed from transferAllSlavesTo to transferAllCaptivesTo
|
||||
*
|
||||
* @param newCaptor The new captor to transfer captives to
|
||||
*/
|
||||
public void transferAllCaptivesTo(ICaptor newCaptor) {
|
||||
if (newCaptor == null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[PlayerCaptorManager] Attempted to transfer captives to null captor"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (captives.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[PlayerCaptorManager] Transferring {} captives from {} to {}",
|
||||
captives.size(),
|
||||
captor.getName().getString(),
|
||||
newCaptor.getEntity().getName().getString()
|
||||
);
|
||||
|
||||
// Create copy to avoid concurrent modification
|
||||
List<IBondageState> captivesToTransfer = new ArrayList<>(captives);
|
||||
|
||||
for (IBondageState captive : captivesToTransfer) {
|
||||
if (captive != null) {
|
||||
captive.transferCaptivityTo(newCaptor);
|
||||
}
|
||||
}
|
||||
|
||||
// All captives should now be removed from this manager's list
|
||||
if (!captives.isEmpty()) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[PlayerCaptorManager] {} captives remain after transfer - cleaning up",
|
||||
captives.size()
|
||||
);
|
||||
captives.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the captor player.
|
||||
*
|
||||
* @return The player who owns this manager
|
||||
*/
|
||||
public Player getCaptor() {
|
||||
return captor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up invalid captives from the list.
|
||||
* Removes captives that are no longer valid (offline, transport gone, etc.).
|
||||
*
|
||||
* Phase 17: Renamed from cleanupInvalidSlaves to cleanupInvalidCaptives
|
||||
*
|
||||
* Should be called periodically (e.g., on tick).
|
||||
*/
|
||||
public void cleanupInvalidCaptives() {
|
||||
captives.removeIf(captive -> {
|
||||
if (captive == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove if not actually captured anymore
|
||||
if (!captive.isCaptive()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerCaptorManager] Removing invalid captive {}",
|
||||
captive.getKidnappedName()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove if captured by different captor
|
||||
if (captive.getCaptor() != this) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[PlayerCaptorManager] Removing captive {} (belongs to different captor)",
|
||||
captive.getKidnappedName()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Backward Compatibility (Phase 17)
|
||||
// ========================================
|
||||
}
|
||||
187
src/main/java/com/tiedup/remake/state/SocialData.java
Normal file
187
src/main/java/com/tiedup/remake/state/SocialData.java
Normal file
@@ -0,0 +1,187 @@
|
||||
package com.tiedup.remake.state;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import java.util.*;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
/**
|
||||
* Persistent storage for social command data.
|
||||
*
|
||||
* Stores:
|
||||
* - Block lists (which players have blocked which other players)
|
||||
* - Talk area settings (local chat distance per player)
|
||||
*
|
||||
* Phase 18: Social Commands persistence.
|
||||
*/
|
||||
public class SocialData extends SavedData {
|
||||
|
||||
private static final String DATA_NAME = TiedUpMod.MOD_ID + "_social";
|
||||
|
||||
// Block lists per player UUID
|
||||
private final Map<UUID, Set<UUID>> blockedPlayers = new HashMap<>();
|
||||
|
||||
// Talk area settings per player UUID
|
||||
private final Map<UUID, Integer> talkAreas = new HashMap<>();
|
||||
|
||||
public SocialData() {}
|
||||
|
||||
/**
|
||||
* Load from NBT.
|
||||
*/
|
||||
public static SocialData load(CompoundTag tag) {
|
||||
SocialData data = new SocialData();
|
||||
|
||||
// Load blocked players
|
||||
if (tag.contains("blocked")) {
|
||||
ListTag blockedList = tag.getList("blocked", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < blockedList.size(); i++) {
|
||||
CompoundTag entry = blockedList.getCompound(i);
|
||||
UUID blocker = entry.getUUID("blocker");
|
||||
Set<UUID> blocked = new HashSet<>();
|
||||
|
||||
ListTag blockedUUIDs = entry.getList(
|
||||
"blocked_uuids",
|
||||
Tag.TAG_COMPOUND
|
||||
);
|
||||
for (int j = 0; j < blockedUUIDs.size(); j++) {
|
||||
CompoundTag uuidTag = blockedUUIDs.getCompound(j);
|
||||
blocked.add(uuidTag.getUUID("uuid"));
|
||||
}
|
||||
|
||||
if (!blocked.isEmpty()) {
|
||||
data.blockedPlayers.put(blocker, blocked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load talk areas
|
||||
if (tag.contains("talk_areas")) {
|
||||
ListTag talkList = tag.getList("talk_areas", Tag.TAG_COMPOUND);
|
||||
for (int i = 0; i < talkList.size(); i++) {
|
||||
CompoundTag entry = talkList.getCompound(i);
|
||||
UUID player = entry.getUUID("player");
|
||||
int distance = entry.getInt("distance");
|
||||
data.talkAreas.put(player, distance);
|
||||
}
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[SocialData] Loaded {} block lists, {} talk areas",
|
||||
data.blockedPlayers.size(),
|
||||
data.talkAreas.size()
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag save(CompoundTag tag) {
|
||||
// Save blocked players
|
||||
ListTag blockedList = new ListTag();
|
||||
for (Map.Entry<UUID, Set<UUID>> entry : blockedPlayers.entrySet()) {
|
||||
if (entry.getValue().isEmpty()) continue;
|
||||
|
||||
CompoundTag blockerTag = new CompoundTag();
|
||||
blockerTag.putUUID("blocker", entry.getKey());
|
||||
|
||||
ListTag blockedUUIDs = new ListTag();
|
||||
for (UUID blocked : entry.getValue()) {
|
||||
CompoundTag uuidTag = new CompoundTag();
|
||||
uuidTag.putUUID("uuid", blocked);
|
||||
blockedUUIDs.add(uuidTag);
|
||||
}
|
||||
blockerTag.put("blocked_uuids", blockedUUIDs);
|
||||
blockedList.add(blockerTag);
|
||||
}
|
||||
tag.put("blocked", blockedList);
|
||||
|
||||
// Save talk areas
|
||||
ListTag talkList = new ListTag();
|
||||
for (Map.Entry<UUID, Integer> entry : talkAreas.entrySet()) {
|
||||
CompoundTag talkTag = new CompoundTag();
|
||||
talkTag.putUUID("player", entry.getKey());
|
||||
talkTag.putInt("distance", entry.getValue());
|
||||
talkList.add(talkTag);
|
||||
}
|
||||
tag.put("talk_areas", talkList);
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
// ==================== Block List Methods ====================
|
||||
|
||||
/**
|
||||
* Add a player to another player's block list.
|
||||
*/
|
||||
public void addBlock(UUID blocker, UUID blocked) {
|
||||
blockedPlayers
|
||||
.computeIfAbsent(blocker, k -> new HashSet<>())
|
||||
.add(blocked);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a player from another player's block list.
|
||||
*/
|
||||
public void removeBlock(UUID blocker, UUID blocked) {
|
||||
Set<UUID> set = blockedPlayers.get(blocker);
|
||||
if (set != null) {
|
||||
set.remove(blocked);
|
||||
if (set.isEmpty()) {
|
||||
blockedPlayers.remove(blocker);
|
||||
}
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player has blocked another.
|
||||
*/
|
||||
public boolean isBlocked(UUID blocker, UUID blocked) {
|
||||
Set<UUID> set = blockedPlayers.get(blocker);
|
||||
return set != null && set.contains(blocked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of players blocked by a player.
|
||||
*/
|
||||
public Set<UUID> getBlockedPlayers(UUID blocker) {
|
||||
return blockedPlayers.getOrDefault(blocker, Collections.emptySet());
|
||||
}
|
||||
|
||||
// ==================== Talk Area Methods ====================
|
||||
|
||||
/**
|
||||
* Set a player's talk area distance.
|
||||
*/
|
||||
public void setTalkArea(UUID player, int distance) {
|
||||
if (distance <= 0) {
|
||||
talkAreas.remove(player);
|
||||
} else {
|
||||
talkAreas.put(player, distance);
|
||||
}
|
||||
setDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player's talk area distance (0 = global chat).
|
||||
*/
|
||||
public int getTalkArea(UUID player) {
|
||||
return talkAreas.getOrDefault(player, 0);
|
||||
}
|
||||
|
||||
// ==================== Static Access ====================
|
||||
|
||||
/**
|
||||
* Get the SocialData for a server level.
|
||||
*/
|
||||
public static SocialData get(ServerLevel level) {
|
||||
return level
|
||||
.getDataStorage()
|
||||
.computeIfAbsent(SocialData::load, SocialData::new, DATA_NAME);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.tiedup.remake.state.hosts;
|
||||
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
import com.tiedup.remake.state.PlayerCaptorManager;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Core host interface for PlayerBindState components.
|
||||
* Provides access to player data and coordination methods.
|
||||
*
|
||||
* Thread Safety: Methods accessing volatile fields are thread-safe.
|
||||
*/
|
||||
public interface IPlayerBindStateHost {
|
||||
// ========== Entity Access ==========
|
||||
|
||||
/**
|
||||
* Get the player entity associated with this state.
|
||||
* @return The player entity
|
||||
*/
|
||||
Player getPlayer();
|
||||
|
||||
/**
|
||||
* Get the player's UUID.
|
||||
* @return The player's unique identifier
|
||||
*/
|
||||
UUID getPlayerUUID();
|
||||
|
||||
/**
|
||||
* Get the current level/world the player is in.
|
||||
* @return The player's current level
|
||||
*/
|
||||
Level getLevel();
|
||||
|
||||
/**
|
||||
* Check if this instance is on the client side.
|
||||
* @return true if client-side, false if server-side
|
||||
*/
|
||||
boolean isClientSide();
|
||||
|
||||
/**
|
||||
* Get the IBondageState interface for this player state.
|
||||
* Used when components need to pass the player state to other systems.
|
||||
* @return This instance cast to IBondageState
|
||||
*/
|
||||
IBondageState getKidnapped();
|
||||
|
||||
// ========== Lifecycle ==========
|
||||
|
||||
/**
|
||||
* Check if the player is currently online.
|
||||
* @return true if player is online
|
||||
*/
|
||||
boolean isOnline();
|
||||
|
||||
/**
|
||||
* Set the player's online status.
|
||||
* @param online true if player is online
|
||||
*/
|
||||
void setOnline(boolean online);
|
||||
|
||||
// ========== Network Sync (Centralized) ==========
|
||||
|
||||
/**
|
||||
* Sync clothes configuration to all tracking clients.
|
||||
* Used when clothes are equipped/changed.
|
||||
*/
|
||||
void syncClothesConfig();
|
||||
|
||||
/**
|
||||
* Sync enslavement/captivity state to all clients.
|
||||
* Used when capture/free state changes.
|
||||
*/
|
||||
void syncEnslavement();
|
||||
|
||||
// ========== System Access ==========
|
||||
|
||||
/**
|
||||
* Get the current captor (master) of this player.
|
||||
* @return The captor, or null if not captive
|
||||
*/
|
||||
ICaptor getCaptor();
|
||||
|
||||
/**
|
||||
* Set the captor for this player.
|
||||
* @param captor The new captor, or null to clear
|
||||
*/
|
||||
void setCaptor(ICaptor captor);
|
||||
|
||||
/**
|
||||
* Get the captor manager (for when player acts as captor).
|
||||
* @return The captor manager
|
||||
*/
|
||||
PlayerCaptorManager getCaptorManager();
|
||||
|
||||
// ========== State Queries ==========
|
||||
|
||||
/**
|
||||
* Check if player is tied up (has bind equipment).
|
||||
* @return true if player has bind
|
||||
*/
|
||||
boolean isTiedUp();
|
||||
|
||||
/**
|
||||
* Check if player has a collar equipped.
|
||||
* @return true if player has collar
|
||||
*/
|
||||
boolean hasCollar();
|
||||
|
||||
// ========== Struggle State (Volatile Wrapper) ==========
|
||||
|
||||
/**
|
||||
* Set struggle animation state (server-side).
|
||||
* Thread-safe (volatile field).
|
||||
* @param struggling True to start animation, false to stop
|
||||
* @param tick Current game time tick
|
||||
*/
|
||||
void setStruggling(boolean struggling, long tick);
|
||||
|
||||
/**
|
||||
* Set struggle animation flag (client-side only).
|
||||
* Thread-safe (volatile field).
|
||||
* Does NOT update timer - server manages timer.
|
||||
* @param struggling True if struggling
|
||||
*/
|
||||
void setStrugglingClient(boolean struggling);
|
||||
|
||||
/**
|
||||
* Check if player is currently playing struggle animation.
|
||||
* Thread-safe (volatile field).
|
||||
* @return true if struggling
|
||||
*/
|
||||
boolean isStruggling();
|
||||
|
||||
/**
|
||||
* Get the tick when struggle animation started.
|
||||
* Thread-safe (volatile field).
|
||||
* @return Start tick
|
||||
*/
|
||||
long getStruggleStartTick();
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package com.tiedup.remake.state.struggle;
|
||||
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.base.ILockable;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Phase 21: Struggle implementation for accessory items (gag, blindfold, earplugs, collar).
|
||||
*
|
||||
* Accessories have NO base resistance - only locked items can be struggled.
|
||||
* Lock adds 250 resistance. Struggle success destroys the padlock (lockable=false).
|
||||
*
|
||||
* Unlike binds, accessories are NOT automatically removed on struggle success.
|
||||
* The item remains equipped but unlocked, allowing manual removal.
|
||||
*/
|
||||
public class StruggleAccessory extends StruggleState {
|
||||
|
||||
/** The V2 body region of the accessory being struggled against. */
|
||||
private final BodyRegionV2 accessoryRegion;
|
||||
|
||||
public StruggleAccessory(BodyRegionV2 accessoryRegion) {
|
||||
if (accessoryRegion == BodyRegionV2.ARMS) {
|
||||
throw new IllegalArgumentException(
|
||||
"Use StruggleBinds for ARMS region, not StruggleAccessory"
|
||||
);
|
||||
}
|
||||
this.accessoryRegion = accessoryRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current resistance for this accessory.
|
||||
* Accessories have 0 base resistance - only lock resistance exists.
|
||||
* Resistance is stored in the item's NBT to persist between attempts.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return Current resistance value (0 if not locked, 250 if locked and not yet struggled)
|
||||
*/
|
||||
@Override
|
||||
protected int getResistanceState(PlayerBindState state) {
|
||||
ItemStack stack = getAccessoryStack(state);
|
||||
if (stack.isEmpty()) return 0;
|
||||
|
||||
if (stack.getItem() instanceof ILockable lockable) {
|
||||
return lockable.getCurrentLockResistance(stack);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current resistance for this accessory struggle.
|
||||
* Stored in the item's NBT to persist between attempts.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @param resistance The new resistance value
|
||||
*/
|
||||
@Override
|
||||
protected void setResistanceState(PlayerBindState state, int resistance) {
|
||||
ItemStack stack = getAccessoryStack(state);
|
||||
if (stack.isEmpty()) return;
|
||||
|
||||
if (stack.getItem() instanceof ILockable lockable) {
|
||||
lockable.setCurrentLockResistance(stack, resistance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the player can struggle against this accessory.
|
||||
* Only locked accessories can be struggled against.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return True if the accessory is equipped and locked
|
||||
*/
|
||||
@Override
|
||||
protected boolean canStruggle(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ItemStack stack = getAccessoryStack(state);
|
||||
if (stack.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only locked accessories can be struggled against
|
||||
if (!(stack.getItem() instanceof ILockable lockable)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lockable.isLocked(stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the accessory is locked.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return true if the accessory is locked
|
||||
*/
|
||||
@Override
|
||||
protected boolean isItemLocked(PlayerBindState state) {
|
||||
ItemStack stack = getAccessoryStack(state);
|
||||
if (
|
||||
stack.isEmpty() || !(stack.getItem() instanceof ILockable lockable)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return lockable.isLocked(stack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the player successfully struggles against the accessory.
|
||||
* Unlocks the item and DESTROYS the padlock (lockable=false).
|
||||
* The item remains equipped but can now be removed manually.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
*/
|
||||
@Override
|
||||
protected void successAction(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null) return;
|
||||
|
||||
ItemStack stack = getAccessoryStack(state);
|
||||
if (stack.isEmpty()) return;
|
||||
|
||||
// Destroy the padlock (unlock + lockable=false)
|
||||
if (stack.getItem() instanceof ILockable lockable) {
|
||||
lockable.breakLock(stack);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[STRUGGLE] {} destroyed padlock on {} (region: {})",
|
||||
player.getName().getString(),
|
||||
stack.getDisplayName().getString(),
|
||||
accessoryRegion
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageCategory getSuccessCategory() {
|
||||
return MessageCategory.STRUGGLE_SUCCESS; // Used for color (green)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageCategory getFailCategory() {
|
||||
return MessageCategory.STRUGGLE_FAIL; // Used for color (gray)
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to send custom success message per accessory type.
|
||||
*/
|
||||
@Override
|
||||
protected void notifySuccess(Player player, int newResistance) {
|
||||
String message = switch (accessoryRegion) {
|
||||
case MOUTH -> "You manage to loosen the gag's lock...";
|
||||
case EYES -> "The blindfold's lock gives way...";
|
||||
case EARS -> "You feel the earplug lock breaking...";
|
||||
case NECK -> "The collar's lock snaps open...";
|
||||
case TORSO -> "The clothing lock breaks...";
|
||||
case HANDS -> "The mitten lock comes undone...";
|
||||
default -> "The lock breaks!";
|
||||
};
|
||||
SystemMessageManager.sendToPlayer(
|
||||
player,
|
||||
getSuccessCategory(),
|
||||
message + " (Resistance: " + newResistance + ")"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to send custom failure message per accessory type.
|
||||
*/
|
||||
@Override
|
||||
protected void notifyFailure(Player player) {
|
||||
String message = switch (accessoryRegion) {
|
||||
case MOUTH -> "You struggle against the gag, but the lock holds.";
|
||||
case EYES -> "The blindfold's lock refuses to budge.";
|
||||
case EARS -> "The earplug lock stays firmly shut.";
|
||||
case NECK -> "The collar's lock resists your efforts.";
|
||||
case TORSO -> "The clothing lock holds tight.";
|
||||
case HANDS -> "The mitten lock won't give in.";
|
||||
default -> "The lock holds firm.";
|
||||
};
|
||||
SystemMessageManager.sendToPlayer(player, getFailCategory(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 13: Trigger shock collar check when struggling against accessories.
|
||||
*/
|
||||
@Override
|
||||
protected boolean onAttempt(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
!collar.isEmpty() &&
|
||||
collar.getItem() instanceof
|
||||
com.tiedup.remake.items.ItemShockCollar shockCollar
|
||||
) {
|
||||
return shockCollar.notifyStruggle(player, collar);
|
||||
}
|
||||
return true; // No collar, proceed normally
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the accessory ItemStack from the player.
|
||||
*/
|
||||
private ItemStack getAccessoryStack(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null) return ItemStack.EMPTY;
|
||||
return V2EquipmentHelper.getInRegion(player, accessoryRegion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the V2 body region of the accessory this struggle targets.
|
||||
*/
|
||||
public BodyRegionV2 getAccessoryRegion() {
|
||||
return accessoryRegion;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package com.tiedup.remake.state.struggle;
|
||||
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.base.ILockable;
|
||||
import com.tiedup.remake.items.base.ItemBind;
|
||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Phase 7: Struggle implementation for bind restraints (ropes).
|
||||
*
|
||||
* Handles struggling against binds:
|
||||
* - Gets/sets resistance from equipped bind item
|
||||
* - Drops bondage items when escaping
|
||||
* - Unties the player when resistance reaches 0
|
||||
*
|
||||
* Based on original StruggleBinds from 1.12.2 (lines 17-117)
|
||||
*/
|
||||
public class StruggleBinds extends StruggleState {
|
||||
|
||||
/**
|
||||
* Get the current bind resistance from the equipped bind item.
|
||||
*
|
||||
* Based on original StruggleBinds.getResistanceState() (lines 23-25)
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return Current resistance value
|
||||
*/
|
||||
@Override
|
||||
protected int getResistanceState(PlayerBindState state) {
|
||||
return state.getCurrentBindResistance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current bind resistance on the equipped bind item.
|
||||
*
|
||||
* Based on original StruggleBinds.setResistanceState() (lines 28-30)
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @param resistance The new resistance value
|
||||
*/
|
||||
@Override
|
||||
protected void setResistanceState(PlayerBindState state, int resistance) {
|
||||
state.setCurrentBindResistance(resistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the player can struggle against their binds.
|
||||
* Returns false if:
|
||||
* - No bind item equipped
|
||||
* - Bind item has struggle disabled
|
||||
*
|
||||
* Phase 20: Locked items can now be struggled, but with x10 resistance.
|
||||
* The resistance penalty is applied in StruggleState.struggle().
|
||||
*
|
||||
* Based on original StruggleBinds.canStruggle() (lines 33-41)
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return True if struggling is allowed
|
||||
*/
|
||||
@Override
|
||||
protected boolean canStruggle(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ItemStack bindStack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
|
||||
if (
|
||||
bindStack.isEmpty() ||
|
||||
!(bindStack.getItem() instanceof ItemBind bind)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 20: Locked items can now be struggled (with x10 resistance)
|
||||
// The locked check has been moved to struggle() where decrease is reduced
|
||||
|
||||
return bind.canBeStruggledOut(bindStack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the bind item is locked.
|
||||
* Used by StruggleState to apply x10 resistance penalty.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return true if the bind is locked
|
||||
*/
|
||||
@Override
|
||||
protected boolean isItemLocked(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null) return false;
|
||||
|
||||
ItemStack bindStack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
|
||||
if (
|
||||
bindStack.isEmpty() ||
|
||||
!(bindStack.getItem() instanceof ItemBind bind)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return bind.isLocked(bindStack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the player successfully escapes from their binds.
|
||||
* Drops all bondage items and completely unties the player.
|
||||
*
|
||||
* Based on original StruggleBinds.successAction() (lines 44-47)
|
||||
*
|
||||
* @param state The player's bind state
|
||||
*/
|
||||
@Override
|
||||
protected void successAction(PlayerBindState state) {
|
||||
dropBondageItems(state);
|
||||
untieTarget(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageCategory getSuccessCategory() {
|
||||
return MessageCategory.STRUGGLE_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageCategory getFailCategory() {
|
||||
return MessageCategory.STRUGGLE_FAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 13: Trigger shock collar check even when struggling against binds.
|
||||
* If shocked, the attempt is missed.
|
||||
*/
|
||||
@Override
|
||||
protected boolean onAttempt(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
!collar.isEmpty() &&
|
||||
collar.getItem() instanceof
|
||||
com.tiedup.remake.items.ItemShockCollar shockCollar
|
||||
) {
|
||||
return shockCollar.notifyStruggle(player, collar);
|
||||
}
|
||||
return true; // No collar, proceed normally
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the bind items to the ground.
|
||||
* Accessories are NOT dropped during bind struggle.
|
||||
*
|
||||
* Phase 21: Struggle success destroys the padlock (lockable=false).
|
||||
*
|
||||
* Based on original StruggleBinds.dropBondageItems() (lines 50-69)
|
||||
*
|
||||
* @param state The player's bind state
|
||||
*/
|
||||
private void dropBondageItems(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null || player.level() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ONLY drop the BIND slot
|
||||
ItemStack stack = V2EquipmentHelper.getInRegion(player, BodyRegionV2.ARMS);
|
||||
if (!stack.isEmpty()) {
|
||||
// Phase 21: Struggle success DESTROYS the padlock
|
||||
// The bind drops without its padlock (lockable=false)
|
||||
ItemStack toDrop = stack.copy();
|
||||
if (
|
||||
toDrop.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLockable(toDrop)
|
||||
) {
|
||||
lockable.breakLock(toDrop);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[STRUGGLE] Padlock destroyed on {} during struggle",
|
||||
toDrop.getDisplayName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
ItemEntity itemEntity = new ItemEntity(
|
||||
player.level(),
|
||||
player.getX(),
|
||||
player.getY(),
|
||||
player.getZ(),
|
||||
toDrop
|
||||
);
|
||||
player.level().addFreshEntity(itemEntity);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[STRUGGLE] Dropped {} from {}",
|
||||
toDrop.getDisplayName().getString(),
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove only the bind items from the player (untie).
|
||||
* Accessories like gags, collars, and blindfolds remain equipped.
|
||||
*
|
||||
* Based on original StruggleBinds.untieTarget() (lines 72-78)
|
||||
*
|
||||
* @param state The player's bind state
|
||||
*/
|
||||
private void untieTarget(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 17: 1. Remove from captivity/leash
|
||||
if (state.isCaptive()) {
|
||||
state.free();
|
||||
}
|
||||
// Also detach leash if tied to pole (no captor but still leashed)
|
||||
else if (
|
||||
player instanceof IPlayerLeashAccess access &&
|
||||
access.tiedup$isLeashed()
|
||||
) {
|
||||
access.tiedup$detachLeash();
|
||||
access.tiedup$dropLeash();
|
||||
}
|
||||
|
||||
// 2. ONLY unequip the BIND slot (ropes, etc.)
|
||||
V2EquipmentHelper.unequipFromRegion(player, BodyRegionV2.ARMS);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[STRUGGLE] {} has struggled out of their binds and leash",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: External success action for mini-game system.
|
||||
* Called when player completes struggle mini-game successfully.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
*/
|
||||
public void successActionExternal(PlayerBindState state) {
|
||||
successAction(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2: Set external cooldown timer (e.g., from mini-game exhaustion).
|
||||
*
|
||||
* @param seconds Cooldown duration in seconds
|
||||
* @param level The level for timer
|
||||
*/
|
||||
public void setExternalCooldown(
|
||||
int seconds,
|
||||
net.minecraft.world.level.Level level
|
||||
) {
|
||||
this.struggleCooldownTimer = new com.tiedup.remake.util.time.Timer(
|
||||
seconds,
|
||||
level
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.5: Check if struggle cooldown is active.
|
||||
* Used by QTE mini-game to prevent starting a new session during cooldown.
|
||||
*
|
||||
* @return true if cooldown is active (cannot struggle yet)
|
||||
*/
|
||||
public boolean isCooldownActive() {
|
||||
return (
|
||||
struggleCooldownTimer != null && !struggleCooldownTimer.isExpired()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v2.5: Get remaining cooldown time in seconds.
|
||||
*
|
||||
* @return Remaining seconds, or 0 if no cooldown
|
||||
*/
|
||||
public int getRemainingCooldownSeconds() {
|
||||
if (
|
||||
struggleCooldownTimer == null || struggleCooldownTimer.isExpired()
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
return struggleCooldownTimer.getSecondsRemaining();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tighten the player's binds (restore resistance to base value).
|
||||
* Called when another player right-clicks with a paddle/whip.
|
||||
*
|
||||
* Based on original StruggleBinds.tighten() (lines 81-101)
|
||||
*
|
||||
* @param tightener The player performing the tightening
|
||||
* @param state The target player's bind state
|
||||
*/
|
||||
public void tighten(Player tightener, PlayerBindState state) {
|
||||
if (state == null || !state.isTiedUp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player target = state.getPlayer();
|
||||
if (target == null || target.level() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack bindStack = V2EquipmentHelper.getInRegion(target, BodyRegionV2.ARMS);
|
||||
if (
|
||||
bindStack.isEmpty() ||
|
||||
!(bindStack.getItem() instanceof ItemBind bind)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get base resistance from config (BUG-003 fix: was using ModGameRules which
|
||||
// only knew 4 types and returned hardcoded 100 for the other 10)
|
||||
int baseResistance = SettingsAccessor.getBindResistance(
|
||||
bind.getItemName()
|
||||
);
|
||||
|
||||
// Set current resistance to base (full restore)
|
||||
setResistanceState(state, baseResistance);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[STRUGGLE] {} tightened {}'s binds (resistance restored to {})",
|
||||
tightener.getName().getString(),
|
||||
target.getName().getString(),
|
||||
baseResistance
|
||||
);
|
||||
|
||||
// Send message to target: "X tightened your binds!"
|
||||
SystemMessageManager.sendBindsTightened(tightener, target);
|
||||
|
||||
// Send confirmation to tightener
|
||||
SystemMessageManager.sendToPlayer(
|
||||
tightener,
|
||||
"You tightened " + target.getName().getString() + "'s binds!",
|
||||
net.minecraft.ChatFormatting.YELLOW
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package com.tiedup.remake.state.struggle;
|
||||
|
||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Phase 8: Master-Slave Relationships
|
||||
*
|
||||
* Struggle mechanics specifically for locked collars.
|
||||
*
|
||||
* How it Works:
|
||||
* - Only locked collars can be struggled against
|
||||
* - Success: Collar becomes UNLOCKED (not removed)
|
||||
* - No items are dropped (unlike bind struggle)
|
||||
* - Uses collar-specific GameRules for probability and resistance
|
||||
*
|
||||
* Differences from StruggleBinds:
|
||||
* - Condition: hasLockedCollar() instead of isTiedUp()
|
||||
* - Success: unlock collar instead of untie + drop items
|
||||
* - Resistance source: collar NBT instead of bind NBT
|
||||
* - GameRules: STRUGGLE_COLLAR, PROBABILITY_STRUGGLE_COLLAR, etc.
|
||||
*
|
||||
* Usage:
|
||||
* - Slave wears locked collar
|
||||
* - Presses R (struggle key)
|
||||
* - Eventually unlocks collar (can then remove it)
|
||||
*
|
||||
* @see StruggleBinds
|
||||
* @see StruggleState
|
||||
*/
|
||||
public class StruggleCollar extends StruggleState {
|
||||
|
||||
/**
|
||||
* Get the current resistance from the equipped collar.
|
||||
*
|
||||
* @param state The PlayerBindState
|
||||
* @return Current collar resistance, or 0 if no collar
|
||||
*/
|
||||
@Override
|
||||
protected int getResistanceState(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
collar.isEmpty() ||
|
||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return collarItem.getCurrentResistance(collar, player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current resistance on the equipped collar.
|
||||
*
|
||||
* @param state The PlayerBindState
|
||||
* @param resistance The new resistance value
|
||||
*/
|
||||
@Override
|
||||
protected void setResistanceState(PlayerBindState state, int resistance) {
|
||||
Player player = state.getPlayer();
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
collar.isEmpty() ||
|
||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
collarItem.setCurrentResistance(collar, resistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the collar can be struggled against.
|
||||
*
|
||||
* Conditions:
|
||||
* - Must have collar equipped
|
||||
* - Collar must be LOCKED
|
||||
* - Collar must allow struggle (canBeStruggledOut)
|
||||
*
|
||||
* Note: Can struggle collar even if NOT tied up.
|
||||
*
|
||||
* @param state The PlayerBindState
|
||||
* @return true if can struggle collar
|
||||
*/
|
||||
@Override
|
||||
protected boolean canStruggle(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
|
||||
// Phase 13 logic: Can only struggle against collar if NOT tied up
|
||||
if (state.isTiedUp()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
collar.isEmpty() ||
|
||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
||||
) {
|
||||
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar equipped");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if locked
|
||||
if (!collarItem.isLocked(collar)) {
|
||||
TiedUpMod.LOGGER.debug("[StruggleCollar] Collar is not locked");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if struggle is enabled
|
||||
if (!collarItem.canBeStruggledOut(collar)) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleCollar] Collar struggle is disabled"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to perform when struggle is attempted.
|
||||
* Used for random shock chance on shock collars.
|
||||
*/
|
||||
@Override
|
||||
protected boolean onAttempt(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
!collar.isEmpty() &&
|
||||
collar.getItem() instanceof
|
||||
com.tiedup.remake.items.ItemShockCollar shockCollar
|
||||
) {
|
||||
return shockCollar.notifyStruggle(player, collar);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to perform when struggle succeeds (resistance = 0).
|
||||
*
|
||||
* For collars: Unlock the collar.
|
||||
* The player can then manually remove it.
|
||||
*
|
||||
* @param state The PlayerBindState
|
||||
*/
|
||||
@Override
|
||||
protected void successAction(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(player, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
collar.isEmpty() ||
|
||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
||||
) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[StruggleCollar] successAction called but no collar equipped"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock the collar
|
||||
collarItem.setLocked(collar, false);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleCollar] {} unlocked their collar!",
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
// Note: Collar is NOT removed, just unlocked
|
||||
// Player can now manually remove it
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageCategory getSuccessCategory() {
|
||||
return MessageCategory.STRUGGLE_COLLAR_SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MessageCategory getFailCategory() {
|
||||
return MessageCategory.STRUGGLE_COLLAR_FAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tighten the collar (restore resistance to base value).
|
||||
* Called when a master tightens the slave's collar.
|
||||
*
|
||||
* Conditions:
|
||||
* - Collar must be locked
|
||||
* - Tightener must be different from wearer
|
||||
*
|
||||
* @param tightener The player tightening the collar
|
||||
* @param state The PlayerBindState of the collar wearer
|
||||
*/
|
||||
public void tighten(Player tightener, PlayerBindState state) {
|
||||
Player target = state.getPlayer();
|
||||
|
||||
if (tightener == null || target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't tighten your own collar
|
||||
if (tightener.getUUID().equals(target.getUUID())) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleCollar] Cannot tighten own collar"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ItemStack collar = V2EquipmentHelper.getInRegion(target, BodyRegionV2.NECK);
|
||||
|
||||
if (
|
||||
collar.isEmpty() ||
|
||||
!(collar.getItem() instanceof ItemCollar collarItem)
|
||||
) {
|
||||
TiedUpMod.LOGGER.debug("[StruggleCollar] No collar to tighten");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if collar is locked
|
||||
if (!collarItem.isLocked(collar)) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleCollar] Collar must be locked to tighten"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get base resistance from GameRules
|
||||
int baseResistance = collarItem.getBaseResistance(target);
|
||||
int currentResistance = collarItem.getCurrentResistance(collar, target);
|
||||
|
||||
// Only tighten if current resistance is lower than base
|
||||
if (currentResistance >= baseResistance) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[StruggleCollar] Collar already at max resistance"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore to base resistance
|
||||
collarItem.setCurrentResistance(collar, baseResistance);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[StruggleCollar] {} tightened {}'s collar (resistance {} -> {})",
|
||||
tightener.getName().getString(),
|
||||
target.getName().getString(),
|
||||
currentResistance,
|
||||
baseResistance
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.tiedup.remake.state.struggle;
|
||||
|
||||
import com.tiedup.remake.core.SystemMessageManager;
|
||||
import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.util.time.Timer;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.GameRules;
|
||||
|
||||
/**
|
||||
* Phase 7: Base class for struggle mechanics.
|
||||
*
|
||||
* Handles the logic for players/NPCs struggling against restraints:
|
||||
* - Cooldown timer between attempts
|
||||
* - Probability-based success (dice roll)
|
||||
* - Resistance decrease on success
|
||||
* - Automatic removal when resistance reaches 0
|
||||
*
|
||||
* v2.5: This is now used for NPC struggle only.
|
||||
* Players use the continuous struggle mini-game (ContinuousStruggleMiniGameState).
|
||||
* Knife bonuses have been removed - knives now work by active cutting.
|
||||
*
|
||||
* Architecture:
|
||||
* - StruggleState (abstract base)
|
||||
* ├─ StruggleBinds (struggle against bind restraints)
|
||||
* └─ StruggleCollar (struggle against collar/ownership)
|
||||
*/
|
||||
public abstract class StruggleState {
|
||||
|
||||
/**
|
||||
* Cooldown timer for struggle attempts.
|
||||
* Reset after each struggle attempt.
|
||||
* Volatile for thread safety across network/tick threads.
|
||||
*/
|
||||
protected volatile Timer struggleCooldownTimer;
|
||||
|
||||
/**
|
||||
* Main struggle logic (dice roll based).
|
||||
* Used for NPC struggle - players use QTE mini-game instead.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Check if struggle is enabled (GameRule)
|
||||
* 2. Check if item can be struggled out of
|
||||
* 3. Check cooldown timer
|
||||
* 4. Roll dice for success (probability %)
|
||||
* 5. If success: decrease resistance by random amount
|
||||
* 6. If resistance <= 0: call successAction()
|
||||
* 7. Set cooldown for next attempt
|
||||
*
|
||||
* @param state The player's bind state
|
||||
*/
|
||||
public synchronized void struggle(PlayerBindState state) {
|
||||
Player player = state.getPlayer();
|
||||
if (player == null || player.level() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameRules gameRules = player.level().getGameRules();
|
||||
|
||||
// 1. Check if struggle system is enabled
|
||||
if (!SettingsAccessor.isStruggleEnabled(gameRules)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Check if item can be struggled out of (not locked or forced)
|
||||
if (!canStruggle(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Check cooldown timer (prevent spamming)
|
||||
if (
|
||||
struggleCooldownTimer != null && !struggleCooldownTimer.isExpired()
|
||||
) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[STRUGGLE] Cooldown not expired yet - ignoring struggle attempt"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start struggle animation (after cooldown check passes)
|
||||
if (!state.isStruggling()) {
|
||||
state.setStruggling(true, player.level().getGameTime());
|
||||
com.tiedup.remake.network.sync.SyncManager.syncStruggleState(
|
||||
player
|
||||
);
|
||||
}
|
||||
|
||||
// Phase 13: Trigger attempt effects (shock collar check)
|
||||
if (!onAttempt(state)) {
|
||||
return; // Interrupted by pain
|
||||
}
|
||||
|
||||
// 4. Roll for success
|
||||
int probability = SettingsAccessor.getProbabilityStruggle(gameRules);
|
||||
int roll = player.getRandom().nextInt(100) + 1;
|
||||
boolean success = roll <= probability;
|
||||
|
||||
if (success) {
|
||||
// Calculate resistance decrease
|
||||
int currentResistance = getResistanceState(state);
|
||||
int minDecrease = SettingsAccessor.getStruggleMinDecrease(gameRules);
|
||||
int maxDecrease = SettingsAccessor.getStruggleMaxDecrease(gameRules);
|
||||
|
||||
int decrease =
|
||||
minDecrease +
|
||||
player.getRandom().nextInt(maxDecrease - minDecrease + 1);
|
||||
|
||||
int newResistance = Math.max(0, currentResistance - decrease);
|
||||
setResistanceState(state, newResistance);
|
||||
|
||||
// Feedback
|
||||
notifySuccess(player, newResistance);
|
||||
|
||||
// Check for escape
|
||||
if (newResistance <= 0) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[STRUGGLE] {} broke free! (resistance reached 0)",
|
||||
player.getName().getString()
|
||||
);
|
||||
successAction(state);
|
||||
}
|
||||
} else {
|
||||
notifyFailure(player);
|
||||
}
|
||||
|
||||
// 5. Set cooldown
|
||||
int cooldownTicks = SettingsAccessor.getStruggleTimer(gameRules);
|
||||
struggleCooldownTimer = new Timer(cooldownTicks / 20, player.level());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message category for successful struggle attempts.
|
||||
*/
|
||||
protected abstract MessageCategory getSuccessCategory();
|
||||
|
||||
/**
|
||||
* Get the message category for failed struggle attempts.
|
||||
*/
|
||||
protected abstract MessageCategory getFailCategory();
|
||||
|
||||
/**
|
||||
* Send success notification to the player.
|
||||
*
|
||||
* @param player The player to notify
|
||||
* @param newResistance The new resistance value after decrease
|
||||
*/
|
||||
protected void notifySuccess(Player player, int newResistance) {
|
||||
SystemMessageManager.sendWithResistance(
|
||||
player,
|
||||
getSuccessCategory(),
|
||||
newResistance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send failure notification to the player.
|
||||
*
|
||||
* @param player The player to notify
|
||||
*/
|
||||
protected void notifyFailure(Player player) {
|
||||
SystemMessageManager.sendToPlayer(player, getFailCategory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the player successfully struggles free (resistance reaches 0).
|
||||
* Subclasses implement this to handle the escape action.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
*/
|
||||
protected abstract void successAction(PlayerBindState state);
|
||||
|
||||
/**
|
||||
* Called when a struggle attempt is performed (regardless of success/fail).
|
||||
* Can be used for side effects like random shocks.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return true to proceed with struggle, false to interrupt
|
||||
*/
|
||||
protected boolean onAttempt(PlayerBindState state) {
|
||||
return true; // Default: proceed
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current resistance value from the player's state.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return Current resistance value
|
||||
*/
|
||||
protected abstract int getResistanceState(PlayerBindState state);
|
||||
|
||||
/**
|
||||
* Set the current resistance value in the player's state.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @param resistance The new resistance value
|
||||
*/
|
||||
protected abstract void setResistanceState(
|
||||
PlayerBindState state,
|
||||
int resistance
|
||||
);
|
||||
|
||||
/**
|
||||
* Check if the player can struggle.
|
||||
* Returns false if the item cannot be struggled out of.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return True if struggling is allowed
|
||||
*/
|
||||
protected abstract boolean canStruggle(PlayerBindState state);
|
||||
|
||||
/**
|
||||
* Check if the item being struggled against is locked.
|
||||
*
|
||||
* @param state The player's bind state
|
||||
* @return true if the item is locked
|
||||
*/
|
||||
protected boolean isItemLocked(PlayerBindState state) {
|
||||
return false; // Default: not locked, subclasses override
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if debug logging is enabled.
|
||||
*
|
||||
* @return True if debug logging should be printed
|
||||
*/
|
||||
protected boolean isDebugEnabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user