refactor/god-class-decomposition #17

Merged
NotEvil merged 2 commits from refactor/god-class-decomposition into develop 2026-04-16 12:38:56 +00:00
26 changed files with 1430 additions and 968 deletions

View File

@@ -94,7 +94,7 @@ public final class CampLifecycleManager {
); );
} else { } else {
// Offline: full escape via PrisonerService (no grace period needed) // Offline: full escape via PrisonerService (no grace period needed)
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
level, level,
prisonerId, prisonerId,
"camp death" "camp death"

View File

@@ -640,7 +640,7 @@ public class CellRegistryV2 extends SavedData {
currentState == currentState ==
com.tiedup.remake.prison.PrisonerState.IMPRISONED com.tiedup.remake.prison.PrisonerState.IMPRISONED
) { ) {
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
level, level,
id, id,
"offline_cleanup" "offline_cleanup"

View File

@@ -772,7 +772,7 @@ public class EntityDamsel
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED || state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
state == com.tiedup.remake.prison.PrisonerState.WORKING state == com.tiedup.remake.prison.PrisonerState.WORKING
) { ) {
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
serverLevel, serverLevel,
uuid, uuid,
"player_death" "player_death"

View File

@@ -1,7 +1,6 @@
package com.tiedup.remake.entities; package com.tiedup.remake.entities;
import com.tiedup.remake.cells.CellDataV2; import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.IDialogueSpeaker; import com.tiedup.remake.dialogue.IDialogueSpeaker;
@@ -11,31 +10,23 @@ import com.tiedup.remake.entities.ai.kidnapper.*;
import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem; import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem;
import com.tiedup.remake.entities.skins.Gender; import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.entities.skins.KidnapperSkinManager; import com.tiedup.remake.entities.skins.KidnapperSkinManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.personality.PersonalityType; import com.tiedup.remake.personality.PersonalityType;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor; import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.tasks.ItemTask; import com.tiedup.remake.util.tasks.ItemTask;
import com.tiedup.remake.util.tasks.SaleLoader;
import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.*; import net.minecraft.world.entity.*;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
@@ -44,7 +35,6 @@ import net.minecraft.world.entity.ai.goal.*;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
/** /**
* EntityKidnapper - Aggressive NPC that captures and enslaves players. * EntityKidnapper - Aggressive NPC that captures and enslaves players.
@@ -87,48 +77,6 @@ public class EntityKidnapper
implements ICaptor, IDialogueSpeaker implements ICaptor, IDialogueSpeaker
{ {
// CAPTIVE PRIORITY (for prisoner replacement)
/**
* Priority levels for captives when replacing prisoners in cells.
* Higher priority captives will cause lower priority prisoners to be released.
*/
public enum CaptivePriority {
DAMSEL(1),
DAMSEL_SHINY(2),
PLAYER(3);
private final int priority;
CaptivePriority(int priority) {
this.priority = priority;
}
public int getPriority() {
return priority;
}
/**
* Get the priority for an entity.
*
* @param entity The entity to check
* @return The captive priority
*/
public static CaptivePriority fromEntity(LivingEntity entity) {
if (entity instanceof Player) return PLAYER;
if (entity instanceof EntityDamselShiny) return DAMSEL_SHINY;
if (entity instanceof EntityDamsel) return DAMSEL;
return DAMSEL; // Default for unknown entities
}
/**
* Check if this priority is higher than another.
*/
public boolean isHigherThan(CaptivePriority other) {
return this.priority > other.priority;
}
}
// DATA SYNC (Client-Server) // DATA SYNC (Client-Server)
/** /**
@@ -194,11 +142,11 @@ public class EntityKidnapper
/** Whether this kidnapper is currently dogwalking a prisoner. */ /** Whether this kidnapper is currently dogwalking a prisoner. */
private boolean dogwalking = false; private boolean dogwalking = false;
/** Items stolen from players via KidnapperThiefGoal. Dropped at 100% on death. */ /** Loot manager for stolen items, collar keys, and death drops. */
private final List<ItemStack> stolenItems = new ArrayList<>(); private final com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager lootManager;
/** Collar keys generated when collaring captives. Dropped at 20% on death. */ /** Dialogue speaker implementation for IDialogueSpeaker delegation. */
private final List<ItemStack> collarKeys = new ArrayList<>(); private final com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue dialogue;
/** Job manager handles job assignment and tracking. */ /** Job manager handles job assignment and tracking. */
private final KidnapperJobManager jobManager = new KidnapperJobManager( private final KidnapperJobManager jobManager = new KidnapperJobManager(
@@ -275,6 +223,18 @@ public class EntityKidnapper
DATA_THEME_COLOR DATA_THEME_COLOR
); );
// Initialize loot manager
this.lootManager =
new com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager(
new com.tiedup.remake.entities.kidnapper.hosts.LootHost(this)
);
// Initialize dialogue component
this.dialogue =
new com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue(
new com.tiedup.remake.entities.kidnapper.hosts.DialogueHost(this)
);
// Initialize state manager // Initialize state manager
this.stateManager = this.stateManager =
new com.tiedup.remake.entities.kidnapper.components.KidnapperStateManager( new com.tiedup.remake.entities.kidnapper.components.KidnapperStateManager(
@@ -585,40 +545,35 @@ public class EntityKidnapper
// All real IBondageState instances are IRestrainable, so the cast is safe. // All real IBondageState instances are IRestrainable, so the cast is safe.
// Log a warning if the invariant is ever broken (future-proofing). // Log a warning if the invariant is ever broken (future-proofing).
private void withRestrainable(String method, IBondageState captive, java.util.function.Consumer<IRestrainable> action) {
if (captive instanceof IRestrainable r) action.accept(r);
else logBridgeWarning(method, captive);
}
private boolean testRestrainable(String method, IBondageState captive, java.util.function.Predicate<IRestrainable> test) {
if (captive instanceof IRestrainable r) return test.test(r);
logBridgeWarning(method, captive);
return false;
}
@Override @Override
public void addCaptive(IBondageState captive) { public void addCaptive(IBondageState captive) {
if (captive instanceof IRestrainable r) { withRestrainable("addCaptive", captive, captiveManager::addCaptive);
captiveManager.addCaptive(r);
} else {
logBridgeWarning("addCaptive", captive);
}
} }
@Override @Override
public void removeCaptive(IBondageState captive, boolean transportState) { public void removeCaptive(IBondageState captive, boolean transportState) {
if (captive instanceof IRestrainable r) { withRestrainable("removeCaptive", captive, r -> captiveManager.removeCaptive(r, transportState));
captiveManager.removeCaptive(r, transportState);
} else {
logBridgeWarning("removeCaptive", captive);
}
} }
@Override @Override
public boolean canCapture(IBondageState captive) { public boolean canCapture(IBondageState captive) {
if ( return testRestrainable("canCapture", captive, captiveManager::canCapture);
captive instanceof IRestrainable r
) return captiveManager.canCapture(r);
logBridgeWarning("canCapture", captive);
return false;
} }
@Override @Override
public boolean canRelease(IBondageState captive) { public boolean canRelease(IBondageState captive) {
if ( return testRestrainable("canRelease", captive, captiveManager::canRelease);
captive instanceof IRestrainable r
) return captiveManager.canRelease(r);
logBridgeWarning("canRelease", captive);
return false;
} }
@Override @Override
@@ -633,29 +588,17 @@ public class EntityKidnapper
@Override @Override
public void onCaptiveLogout(IBondageState captive) { public void onCaptiveLogout(IBondageState captive) {
if (captive instanceof IRestrainable r) { withRestrainable("onCaptiveLogout", captive, captiveManager::onCaptiveLogout);
captiveManager.onCaptiveLogout(r);
} else {
logBridgeWarning("onCaptiveLogout", captive);
}
} }
@Override @Override
public void onCaptiveReleased(IBondageState captive) { public void onCaptiveReleased(IBondageState captive) {
if (captive instanceof IRestrainable r) { withRestrainable("onCaptiveReleased", captive, captiveManager::onCaptiveReleased);
captiveManager.onCaptiveReleased(r);
} else {
logBridgeWarning("onCaptiveReleased", captive);
}
} }
@Override @Override
public void onCaptiveStruggle(IBondageState captive) { public void onCaptiveStruggle(IBondageState captive) {
if (captive instanceof IRestrainable r) { withRestrainable("onCaptiveStruggle", captive, captiveManager::onCaptiveStruggle);
captiveManager.onCaptiveStruggle(r);
} else {
logBridgeWarning("onCaptiveStruggle", captive);
}
} }
private void logBridgeWarning(String method, IBondageState captive) { private void logBridgeWarning(String method, IBondageState captive) {
@@ -936,130 +879,15 @@ public class EntityKidnapper
super.die(damageSource); super.die(damageSource);
} }
/** Token drop chance (5%) */
private static final float TOKEN_DROP_CHANCE = 0.05f;
/** /**
* Prevent taser from dropping when kidnapper dies. * Prevent taser from dropping when kidnapper dies.
* Taser is unique to kidnappers and should not be obtainable by players. * Delegates loot logic to KidnapperLootManager.
* Also handles token drop (5% chance).
*/ */
@Override @Override
protected void dropEquipment() { protected void dropEquipment() {
// Check main hand for taser - don't drop it lootManager.dropEquipment();
ItemStack mainHand = this.getItemBySlot(
net.minecraft.world.entity.EquipmentSlot.MAINHAND
);
if (
!mainHand.isEmpty() &&
mainHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
) {
this.setItemSlot(
net.minecraft.world.entity.EquipmentSlot.MAINHAND,
ItemStack.EMPTY
);
}
// Check off hand too
ItemStack offHand = this.getItemBySlot(
net.minecraft.world.entity.EquipmentSlot.OFFHAND
);
if (
!offHand.isEmpty() &&
offHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
) {
this.setItemSlot(
net.minecraft.world.entity.EquipmentSlot.OFFHAND,
ItemStack.EMPTY
);
}
super.dropEquipment(); super.dropEquipment();
lootManager.dropPostEquipment();
// Token drop: 5% chance when killed
if (
!this.level().isClientSide &&
this.getRandom().nextFloat() < TOKEN_DROP_CHANCE
) {
ItemStack token = new ItemStack(
com.tiedup.remake.items.ModItems.TOKEN.get()
);
this.spawnAtLocation(token);
TiedUpMod.LOGGER.info(
"[EntityKidnapper] {} dropped a token on death!",
this.getNpcName()
);
}
// Bind item drops from kidnapper inventory
if (!this.level().isClientSide) {
KidnapperItemSelector.SelectionResult selection =
getItemSelection();
if (selection != null) {
float dropChance = 0.15f;
if (
!selection.bind.isEmpty() &&
this.getRandom().nextFloat() < dropChance
) {
this.spawnAtLocation(selection.bind.copy());
}
if (
selection.hasGag() &&
this.getRandom().nextFloat() < dropChance
) {
this.spawnAtLocation(selection.gag.copy());
}
if (
selection.hasMittens() &&
this.getRandom().nextFloat() < dropChance
) {
this.spawnAtLocation(selection.mittens.copy());
}
if (
selection.hasEarplugs() &&
this.getRandom().nextFloat() < dropChance
) {
this.spawnAtLocation(selection.earplugs.copy());
}
if (
selection.hasBlindfold() &&
this.getRandom().nextFloat() < dropChance
) {
this.spawnAtLocation(selection.blindfold.copy());
}
}
}
// Drop stolen items at 100% rate (player's property)
if (!this.level().isClientSide) {
for (ItemStack stolen : this.stolenItems) {
if (!stolen.isEmpty()) {
this.spawnAtLocation(stolen);
}
}
if (!this.stolenItems.isEmpty()) {
TiedUpMod.LOGGER.info(
"[EntityKidnapper] {} dropped {} stolen item(s) on death",
this.getNpcName(),
this.stolenItems.size()
);
}
this.stolenItems.clear();
}
// Drop collar keys at 20% rate
if (!this.level().isClientSide) {
for (ItemStack key : this.collarKeys) {
if (!key.isEmpty() && this.getRandom().nextFloat() < 0.20f) {
this.spawnAtLocation(key);
TiedUpMod.LOGGER.info(
"[EntityKidnapper] {} dropped a collar key on death",
this.getNpcName()
);
}
}
this.collarKeys.clear();
}
} }
/** /**
@@ -1255,27 +1083,8 @@ public class EntityKidnapper
// Delegate to data serializer // Delegate to data serializer
dataSerializer.saveToNBT(tag); dataSerializer.saveToNBT(tag);
// Save stolen items // Delegate stolen items and collar keys to loot manager
if (!this.stolenItems.isEmpty()) { lootManager.saveToNBT(tag);
ListTag stolenTag = new ListTag();
for (ItemStack stack : this.stolenItems) {
if (!stack.isEmpty()) {
stolenTag.add(stack.save(new CompoundTag()));
}
}
tag.put("StolenItems", stolenTag);
}
// Save collar keys
if (!this.collarKeys.isEmpty()) {
ListTag keysTag = new ListTag();
for (ItemStack key : this.collarKeys) {
if (!key.isEmpty()) {
keysTag.add(key.save(new CompoundTag()));
}
}
tag.put("CollarKeys", keysTag);
}
} }
@Override @Override
@@ -1285,29 +1094,8 @@ public class EntityKidnapper
// Delegate to data serializer // Delegate to data serializer
dataSerializer.loadFromNBT(tag); dataSerializer.loadFromNBT(tag);
// Load stolen items // Delegate stolen items and collar keys to loot manager
this.stolenItems.clear(); lootManager.loadFromNBT(tag);
if (tag.contains("StolenItems", Tag.TAG_LIST)) {
ListTag stolenTag = tag.getList("StolenItems", Tag.TAG_COMPOUND);
for (int i = 0; i < stolenTag.size(); i++) {
ItemStack stack = ItemStack.of(stolenTag.getCompound(i));
if (!stack.isEmpty()) {
this.stolenItems.add(stack);
}
}
}
// Load collar keys
this.collarKeys.clear();
if (tag.contains("CollarKeys", Tag.TAG_LIST)) {
ListTag keysTag = tag.getList("CollarKeys", Tag.TAG_COMPOUND);
for (int i = 0; i < keysTag.size(); i++) {
ItemStack key = ItemStack.of(keysTag.getCompound(i));
if (!key.isEmpty()) {
this.collarKeys.add(key);
}
}
}
} }
// ESCAPE TRACKING METHODS // ESCAPE TRACKING METHODS
@@ -1370,59 +1158,14 @@ public class EntityKidnapper
return CollarHelper.isOwner(collar, player); return CollarHelper.isOwner(collar, player);
} }
/** Damage reduction multiplier against monsters (50% damage taken) */
private static final float MONSTER_DAMAGE_REDUCTION = 0.5f;
@Override @Override
public boolean hurt( public boolean hurt(
net.minecraft.world.damagesource.DamageSource source, net.minecraft.world.damagesource.DamageSource source,
float amount float amount
) { ) {
float finalAmount = amount; float modified = aggressionSystem.processIncomingDamage(source, amount);
if (modified <= 0) return false;
// Track the attacker for fight back system return super.hurt(source, modified);
if (source.getEntity() instanceof LivingEntity attacker) {
aggressionSystem.setLastAttacker(attacker);
// Punish prisoners who dare to attack us
if (
!this.level().isClientSide &&
attacker instanceof ServerPlayer player
) {
punishAttackingPrisoner(player);
// Expire PROTECTED status if player attacks a kidnapper
// Attacking a kidnapper voids your safe-exit window
if (this.level() instanceof ServerLevel serverLevel) {
com.tiedup.remake.prison.PrisonerManager pm =
com.tiedup.remake.prison.PrisonerManager.get(
serverLevel
);
if (
pm.getState(player.getUUID()) ==
com.tiedup.remake.prison.PrisonerState.PROTECTED
) {
pm.expireProtection(
player.getUUID(),
serverLevel.getGameTime()
);
TiedUpMod.LOGGER.debug(
"[EntityKidnapper] Expired PROTECTED status for {} (attacked kidnapper)",
player.getName().getString()
);
}
}
}
// Camp kidnappers take reduced damage from monsters (trained fighters)
if (
this.getAssociatedStructure() != null &&
attacker instanceof net.minecraft.world.entity.monster.Monster
) {
finalAmount = amount * MONSTER_DAMAGE_REDUCTION;
}
}
return super.hurt(source, finalAmount);
} }
/** /**
@@ -1432,7 +1175,7 @@ public class EntityKidnapper
* @param player The player who attacked * @param player The player who attacked
* @return true if punishment was applied * @return true if punishment was applied
*/ */
protected boolean punishAttackingPrisoner(ServerPlayer player) { public boolean punishAttackingPrisoner(ServerPlayer player) {
return captiveManager.punishAttackingPrisoner(player); return captiveManager.punishAttackingPrisoner(player);
} }
@@ -1912,76 +1655,31 @@ public class EntityKidnapper
com.tiedup.remake.util.MessageDispatcher.actionTo(this, player, action); com.tiedup.remake.util.MessageDispatcher.actionTo(this, player, action);
} }
/** Dialogue cooldown timer (ticks remaining before next dialogue) */
private int dialogueCooldown = 0;
@Override @Override
public String getDialogueName() { public String getDialogueName() {
return this.getNpcName(); return dialogue.getDialogueName();
} }
@Override @Override
public SpeakerType getSpeakerType() { public SpeakerType getSpeakerType() {
return SpeakerType.KIDNAPPER; return dialogue.getSpeakerType();
} }
@Override @Override
@Nullable @Nullable
public PersonalityType getSpeakerPersonality() { public PersonalityType getSpeakerPersonality() {
// Map kidnapper theme to a personality-like behavior return dialogue.getSpeakerPersonality();
KidnapperTheme theme = this.getTheme();
if (theme == null) {
return PersonalityType.CALM;
}
return switch (theme) {
case ROPE, SHIBARI -> PersonalityType.CALM; // Traditional, methodical
case TAPE, LEATHER, CHAIN -> PersonalityType.FIERCE; // Rough, aggressive
case MEDICAL, ASYLUM -> PersonalityType.PROUD; // Clinical, professional
case LATEX, RIBBON -> PersonalityType.PLAYFUL; // Playful, teasing
case BEAM, WRAP -> PersonalityType.CURIOUS; // Experimental, modern
};
} }
@Override @Override
public int getSpeakerMood() { public int getSpeakerMood() {
// Kidnappers mood is based on: return dialogue.getSpeakerMood();
// - Having a captive (+20)
// - Current state (varies)
int mood = 50;
if (this.hasCaptives()) {
mood += 20;
}
// State-based adjustment
KidnapperState state = this.getCurrentState();
if (state != null) {
mood += switch (state) {
case SELLING -> 10; // Excited about sale
case JOB_WATCH -> 5;
case GUARD -> 0;
case CAPTURE -> 15; // Hunting excitement
case PUNISH -> -10; // Stern
case PATROL, IDLE, HUNT -> 0; // Neutral for patrolling/hunting
case ALERT -> -5; // Concerned
case TRANSPORT -> 5;
};
}
return Math.max(0, Math.min(100, mood));
} }
@Override @Override
@Nullable @Nullable
public String getTargetRelation(Player player) { public String getTargetRelation(Player player) {
// Check if this kidnapper is holding the player captive return dialogue.getTargetRelation(player);
IRestrainable captive = this.getCaptive();
if (captive != null && captive.asLivingEntity() == player) {
return "captor";
}
return null;
} }
@Override @Override
@@ -1991,12 +1689,12 @@ public class EntityKidnapper
@Override @Override
public int getDialogueCooldown() { public int getDialogueCooldown() {
return this.dialogueCooldown; return dialogue.getDialogueCooldown();
} }
@Override @Override
public void setDialogueCooldown(int ticks) { public void setDialogueCooldown(int ticks) {
this.dialogueCooldown = ticks; dialogue.setDialogueCooldown(ticks);
} }
/** /**
@@ -2004,9 +1702,7 @@ public class EntityKidnapper
* Called from the main tick method. * Called from the main tick method.
*/ */
protected void tickDialogueCooldown() { protected void tickDialogueCooldown() {
if (this.dialogueCooldown > 0) { dialogue.tickDialogueCooldown();
this.dialogueCooldown--;
}
} }
// STOLEN ITEMS (Thief Goal) // STOLEN ITEMS (Thief Goal)
@@ -2016,9 +1712,7 @@ public class EntityKidnapper
* Called by KidnapperThiefGoal when stealing from a player. * Called by KidnapperThiefGoal when stealing from a player.
*/ */
public void addStolenItem(ItemStack stack) { public void addStolenItem(ItemStack stack) {
if (!stack.isEmpty()) { lootManager.addStolenItem(stack);
this.stolenItems.add(stack.copy());
}
} }
// COLLAR KEYS (Capture Goal) // COLLAR KEYS (Capture Goal)
@@ -2028,8 +1722,6 @@ public class EntityKidnapper
* Called by KidnapperCaptureGoal when collaring a captive. * Called by KidnapperCaptureGoal when collaring a captive.
*/ */
public void addCollarKey(ItemStack keyStack) { public void addCollarKey(ItemStack keyStack) {
if (!keyStack.isEmpty()) { lootManager.addCollarKey(keyStack);
this.collarKeys.add(keyStack.copy());
}
} }
} }

View File

@@ -12,7 +12,7 @@ import com.tiedup.remake.entities.skins.LaborGuardSkinManager;
import com.tiedup.remake.labor.LaborTask; import com.tiedup.remake.labor.LaborTask;
import com.tiedup.remake.prison.LaborRecord; import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.prison.PrisonerManager; import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.service.PrisonerService; import com.tiedup.remake.prison.service.EscapeMonitorService;
import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.MessageDispatcher; import com.tiedup.remake.util.MessageDispatcher;
@@ -520,7 +520,7 @@ public class EntityLaborGuard extends EntityDamsel {
labor.setGuardId(null); labor.setGuardId(null);
// Trigger escape // Trigger escape
PrisonerService.get().escape(level, prisonerUUID, reason); EscapeMonitorService.get().escape(level, prisonerUUID, reason);
// Notify prisoner // Notify prisoner
ServerPlayer prisoner = level ServerPlayer prisoner = level

View File

@@ -8,7 +8,7 @@ import com.tiedup.remake.labor.LaborTask;
import com.tiedup.remake.prison.LaborRecord; import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.prison.PrisonerManager; import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord; import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.service.PrisonerService; import com.tiedup.remake.prison.service.EscapeMonitorService;
import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.MessageDispatcher; import com.tiedup.remake.util.MessageDispatcher;
@@ -562,7 +562,7 @@ public class GuardMonitorGoal extends Goal {
LaborRecord labor = manager.getLaborRecord(prisoner.getUUID()); LaborRecord labor = manager.getLaborRecord(prisoner.getUUID());
labor.setGuardId(null); labor.setGuardId(null);
PrisonerService.get().escape(level, prisoner.getUUID(), reason); EscapeMonitorService.get().escape(level, prisoner.getUUID(), reason);
// Discard the guard entity itself // Discard the guard entity itself
guard.discard(); guard.discard();

View File

@@ -8,7 +8,7 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory; import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.AbstractTiedUpNpc; import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityKidnapper; import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityKidnapper.CaptivePriority; import com.tiedup.remake.entities.kidnapper.CaptivePriority;
import com.tiedup.remake.entities.ai.StuckDetector; import com.tiedup.remake.entities.ai.StuckDetector;
import com.tiedup.remake.entities.ai.WaypointNavigator; import com.tiedup.remake.entities.ai.WaypointNavigator;
import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.v2.bondage.CollarHelper;

View File

@@ -8,7 +8,7 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory; import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
import com.tiedup.remake.entities.EntityKidnapper; import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.prison.service.PrisonerService; import com.tiedup.remake.prison.service.EscapeMonitorService;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.BodyRegionV2;
import java.util.ArrayList; import java.util.ArrayList;
@@ -574,7 +574,7 @@ public class KidnapperDecideNextActionGoal extends Goal {
captive.free(false); // false = don't drop leash (we take it) captive.free(false); // false = don't drop leash (we take it)
// 3b. Clean up PrisonerManager state (CAPTURED -> FREE) // 3b. Clean up PrisonerManager state (CAPTURED -> FREE)
PrisonerService.get().escape( EscapeMonitorService.get().escape(
(ServerLevel) player.level(), (ServerLevel) player.level(),
player.getUUID(), player.getUUID(),
"theft_release" "theft_release"

View File

@@ -0,0 +1,46 @@
package com.tiedup.remake.entities.kidnapper;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.EntityDamselShiny;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
/**
* Priority levels for captives when replacing prisoners in cells.
* Higher priority captives will cause lower priority prisoners to be released.
*/
public enum CaptivePriority {
DAMSEL(1),
DAMSEL_SHINY(2),
PLAYER(3);
private final int priority;
CaptivePriority(int priority) {
this.priority = priority;
}
public int getPriority() {
return priority;
}
/**
* Get the priority for an entity.
*
* @param entity The entity to check
* @return The captive priority
*/
public static CaptivePriority fromEntity(LivingEntity entity) {
if (entity instanceof Player) return PLAYER;
if (entity instanceof EntityDamselShiny) return DAMSEL_SHINY;
if (entity instanceof EntityDamsel) return DAMSEL;
return DAMSEL; // Default for unknown entities
}
/**
* Check if this priority is higher than another.
*/
public boolean isHigherThan(CaptivePriority other) {
return this.priority > other.priority;
}
}

View File

@@ -1,5 +1,7 @@
package com.tiedup.remake.entities.kidnapper.components; package com.tiedup.remake.entities.kidnapper.components;
import java.util.UUID;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -26,4 +28,18 @@ public interface IAggressionHost {
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category, com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius int radius
); );
/**
* Get the associated camp structure UUID.
*/
@Nullable
UUID getAssociatedStructure();
/**
* Punish a prisoner who attacks this kidnapper.
*
* @param player The player who attacked
* @return true if punishment was applied
*/
boolean punishAttackingPrisoner(ServerPlayer player);
} }

View File

@@ -0,0 +1,46 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.KidnapperTheme;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.personality.PersonalityType;
import com.tiedup.remake.state.IRestrainable;
import net.minecraft.world.entity.LivingEntity;
import org.jetbrains.annotations.Nullable;
/**
* Host interface for KidnapperDialogue callbacks.
* Provides access to entity state needed for dialogue speaker implementation.
*/
public interface IDialogueHost {
/**
* Get the NPC's display name.
*/
String getNpcName();
/**
* Get the current theme.
*/
@Nullable
KidnapperTheme getTheme();
/**
* Check if kidnapper has any captives.
*/
boolean hasCaptives();
/**
* Get the current AI state.
*/
KidnapperState getCurrentState();
/**
* Get the current captive.
*/
@Nullable
IRestrainable getCaptive();
/**
* Get the entity as LivingEntity.
*/
LivingEntity asEntity();
}

View File

@@ -0,0 +1,52 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.KidnapperItemSelector;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Host interface for KidnapperLootManager callbacks.
* Provides access to entity methods needed for loot drop and equipment management.
*/
public interface ILootHost {
/**
* Get the entity's level/world.
*/
Level level();
/**
* Get item in the given equipment slot.
*/
ItemStack getItemBySlot(EquipmentSlot slot);
/**
* Set item in the given equipment slot.
*/
void setItemSlot(EquipmentSlot slot, ItemStack stack);
/**
* Get the entity's random source.
*/
RandomSource getRandom();
/**
* Spawn an item at the entity's location.
*/
@Nullable
ItemEntity spawnAtLocation(ItemStack stack);
/**
* Get the NPC's display name for logging.
*/
String getNpcName();
/**
* Get the item selection for this kidnapper (themed items).
*/
@Nullable
KidnapperItemSelector.SelectionResult getItemSelection();
}

View File

@@ -6,6 +6,9 @@ import com.tiedup.remake.state.IBondageState;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -232,6 +235,69 @@ public class KidnapperAggressionSystem {
this.robbedImmunity.clear(); this.robbedImmunity.clear();
} }
// DAMAGE PROCESSING
/** Damage reduction multiplier against monsters (50% damage taken) */
private static final float MONSTER_DAMAGE_REDUCTION = 0.5f;
/**
* Process incoming damage: track attacker, punish prisoners, expire protection,
* apply monster damage reduction for camp kidnappers.
*
* @param source The damage source
* @param amount The raw damage amount
* @return The modified damage amount (0 or less means cancel the hurt)
*/
public float processIncomingDamage(DamageSource source, float amount) {
float finalAmount = amount;
// Track the attacker for fight back system
if (source.getEntity() instanceof LivingEntity attacker) {
this.setLastAttacker(attacker);
// Punish prisoners who dare to attack us
if (
host.level() != null &&
!host.level().isClientSide &&
attacker instanceof ServerPlayer player
) {
host.punishAttackingPrisoner(player);
// Expire PROTECTED status if player attacks a kidnapper
// Attacking a kidnapper voids your safe-exit window
if (host.level() instanceof ServerLevel serverLevel) {
com.tiedup.remake.prison.PrisonerManager pm =
com.tiedup.remake.prison.PrisonerManager.get(
serverLevel
);
if (
pm.getState(player.getUUID()) ==
com.tiedup.remake.prison.PrisonerState.PROTECTED
) {
pm.expireProtection(
player.getUUID(),
serverLevel.getGameTime()
);
TiedUpMod.LOGGER.debug(
"[KidnapperAggressionSystem] Expired PROTECTED status for {} (attacked kidnapper)",
player.getName().getString()
);
}
}
}
// Camp kidnappers take reduced damage from monsters (trained fighters)
if (
host.getAssociatedStructure() != null &&
attacker instanceof net.minecraft.world.entity.monster.Monster
) {
finalAmount = amount * MONSTER_DAMAGE_REDUCTION;
}
}
return finalAmount;
}
// HELPER METHODS // HELPER METHODS
/** /**

View File

@@ -508,7 +508,7 @@ public class KidnapperCaptiveManager {
if (record != null && record.isImprisoned()) { if (record != null && record.isImprisoned()) {
// Clear captivity state - prisoner escaped // Clear captivity state - prisoner escaped
// Use centralized escape service for complete cleanup // Use centralized escape service for complete cleanup
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
serverLevel, serverLevel,
captiveUUID, captiveUUID,
"kidnapper tied up" "kidnapper tied up"
@@ -545,7 +545,7 @@ public class KidnapperCaptiveManager {
if (record != null && record.isImprisoned()) { if (record != null && record.isImprisoned()) {
// Clear captivity state - captor died // Clear captivity state - captor died
// Use centralized escape service for complete cleanup // Use centralized escape service for complete cleanup
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
serverLevel, serverLevel,
captiveUUID, captiveUUID,
"kidnapper died" "kidnapper died"
@@ -670,7 +670,7 @@ public class KidnapperCaptiveManager {
if (record != null && record.isImprisoned()) { if (record != null && record.isImprisoned()) {
// Clear captivity state - prisoner freed // Clear captivity state - prisoner freed
// Use centralized escape service for complete cleanup // Use centralized escape service for complete cleanup
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
serverLevel, serverLevel,
captiveUUID, captiveUUID,
"abandoned by kidnapper" "abandoned by kidnapper"

View File

@@ -0,0 +1,129 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.dialogue.IDialogueSpeaker;
import com.tiedup.remake.dialogue.SpeakerType;
import com.tiedup.remake.entities.KidnapperTheme;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.personality.PersonalityType;
import com.tiedup.remake.state.IRestrainable;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import org.jetbrains.annotations.Nullable;
/**
* KidnapperDialogue - Implements IDialogueSpeaker methods for kidnapper entities.
*
* Handles:
* 1. **Speaker Identity** - Name, type, personality mapping from theme
* 2. **Mood Calculation** - State-based mood for dialogue selection
* 3. **Target Relation** - Captor/captive relationship detection
* 4. **Cooldown Tracking** - Dialogue spam prevention
*
* <p><b>Low complexity</b> - Pure state queries with no side effects.</p>
*/
public class KidnapperDialogue {
// FIELDS
/** Host callbacks */
private final IDialogueHost host;
/** Dialogue cooldown timer (ticks remaining before next dialogue) */
private int dialogueCooldown = 0;
// CONSTRUCTOR
public KidnapperDialogue(IDialogueHost host) {
this.host = host;
}
// IDIALOGUE SPEAKER METHODS
public String getDialogueName() {
return host.getNpcName();
}
public SpeakerType getSpeakerType() {
return SpeakerType.KIDNAPPER;
}
@Nullable
public PersonalityType getSpeakerPersonality() {
// Map kidnapper theme to a personality-like behavior
KidnapperTheme theme = host.getTheme();
if (theme == null) {
return PersonalityType.CALM;
}
return switch (theme) {
case ROPE, SHIBARI -> PersonalityType.CALM; // Traditional, methodical
case TAPE, LEATHER, CHAIN -> PersonalityType.FIERCE; // Rough, aggressive
case MEDICAL, ASYLUM -> PersonalityType.PROUD; // Clinical, professional
case LATEX, RIBBON -> PersonalityType.PLAYFUL; // Playful, teasing
case BEAM, WRAP -> PersonalityType.CURIOUS; // Experimental, modern
};
}
public int getSpeakerMood() {
// Kidnappers mood is based on:
// - Having a captive (+20)
// - Current state (varies)
int mood = 50;
if (host.hasCaptives()) {
mood += 20;
}
// State-based adjustment
KidnapperState state = host.getCurrentState();
if (state != null) {
mood += switch (state) {
case SELLING -> 10; // Excited about sale
case JOB_WATCH -> 5;
case GUARD -> 0;
case CAPTURE -> 15; // Hunting excitement
case PUNISH -> -10; // Stern
case PATROL, IDLE, HUNT -> 0; // Neutral for patrolling/hunting
case ALERT -> -5; // Concerned
case TRANSPORT -> 5;
};
}
return Math.max(0, Math.min(100, mood));
}
@Nullable
public String getTargetRelation(Player player) {
// Check if this kidnapper is holding the player captive
IRestrainable captive = host.getCaptive();
if (captive != null && captive.asLivingEntity() == player) {
return "captor";
}
return null;
}
public LivingEntity asEntity() {
return host.asEntity();
}
// COOLDOWN
public int getDialogueCooldown() {
return this.dialogueCooldown;
}
public void setDialogueCooldown(int ticks) {
this.dialogueCooldown = ticks;
}
/**
* Tick the dialogue cooldown.
* Called from the main tick method.
*/
public void tickDialogueCooldown() {
if (this.dialogueCooldown > 0) {
this.dialogueCooldown--;
}
}
}

View File

@@ -0,0 +1,247 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.KidnapperItemSelector;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.item.ItemStack;
/**
* KidnapperLootManager - Manages stolen items, collar keys, and death drops.
*
* Handles:
* 1. **Stolen Items** - Items taken from players via KidnapperThiefGoal (100% drop on death)
* 2. **Collar Keys** - Keys generated when collaring captives (20% drop on death)
* 3. **Equipment Drops** - Taser removal, token drop (5%), themed item drops (15%)
*
* <p><b>Low complexity</b> - List management with NBT persistence.</p>
*/
public class KidnapperLootManager {
// CONSTANTS
/** Token drop chance (5%) */
private static final float TOKEN_DROP_CHANCE = 0.05f;
/** Themed item drop chance (15%) */
private static final float THEMED_ITEM_DROP_CHANCE = 0.15f;
/** Collar key drop chance (20%) */
private static final float COLLAR_KEY_DROP_CHANCE = 0.20f;
// FIELDS
/** Host callbacks */
private final ILootHost host;
/** Items stolen from players via KidnapperThiefGoal. Dropped at 100% on death. */
private final List<ItemStack> stolenItems = new ArrayList<>();
/** Collar keys generated when collaring captives. Dropped at 20% on death. */
private final List<ItemStack> collarKeys = new ArrayList<>();
// CONSTRUCTOR
public KidnapperLootManager(ILootHost host) {
this.host = host;
}
// STOLEN ITEMS
/**
* Add an item to the stolen items list.
* Called by KidnapperThiefGoal when stealing from a player.
*/
public void addStolenItem(ItemStack stack) {
if (!stack.isEmpty()) {
this.stolenItems.add(stack.copy());
}
}
// COLLAR KEYS
/**
* Add a collar key to be stored on this kidnapper.
* Called by KidnapperCaptureGoal when collaring a captive.
*/
public void addCollarKey(ItemStack keyStack) {
if (!keyStack.isEmpty()) {
this.collarKeys.add(keyStack.copy());
}
}
// EQUIPMENT DROP LOGIC
/**
* Pre-super drop: removes taser from hand to prevent vanilla drop.
* Must be called BEFORE super.dropEquipment().
* @see #dropPostEquipment() for the actual item drops
*/
public void dropEquipment() {
// Check main hand for taser - don't drop it
ItemStack mainHand = host.getItemBySlot(EquipmentSlot.MAINHAND);
if (
!mainHand.isEmpty() &&
mainHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
) {
host.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
}
// Check off hand too
ItemStack offHand = host.getItemBySlot(EquipmentSlot.OFFHAND);
if (
!offHand.isEmpty() &&
offHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
) {
host.setItemSlot(EquipmentSlot.OFFHAND, ItemStack.EMPTY);
}
// Note: super.dropEquipment() is called by EntityKidnapper between
// taser removal and the rest of the drops.
}
/**
* Handle post-super drops: token, themed items, stolen items, collar keys.
* Called from EntityKidnapper.dropEquipment() AFTER super.dropEquipment().
*/
public void dropPostEquipment() {
if (host.level().isClientSide) return;
// Token drop: 5% chance when killed
if (host.getRandom().nextFloat() < TOKEN_DROP_CHANCE) {
ItemStack token = new ItemStack(
com.tiedup.remake.items.ModItems.TOKEN.get()
);
host.spawnAtLocation(token);
TiedUpMod.LOGGER.info(
"[KidnapperLootManager] {} dropped a token on death!",
host.getNpcName()
);
}
// Themed item drops (15% per item)
KidnapperItemSelector.SelectionResult selection =
host.getItemSelection();
if (selection != null) {
if (
!selection.bind.isEmpty() &&
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
) {
host.spawnAtLocation(selection.bind.copy());
}
if (
selection.hasGag() &&
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
) {
host.spawnAtLocation(selection.gag.copy());
}
if (
selection.hasMittens() &&
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
) {
host.spawnAtLocation(selection.mittens.copy());
}
if (
selection.hasEarplugs() &&
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
) {
host.spawnAtLocation(selection.earplugs.copy());
}
if (
selection.hasBlindfold() &&
host.getRandom().nextFloat() < THEMED_ITEM_DROP_CHANCE
) {
host.spawnAtLocation(selection.blindfold.copy());
}
}
// Drop stolen items at 100% rate (player's property)
for (ItemStack stolen : this.stolenItems) {
if (!stolen.isEmpty()) {
host.spawnAtLocation(stolen);
}
}
if (!this.stolenItems.isEmpty()) {
TiedUpMod.LOGGER.info(
"[KidnapperLootManager] {} dropped {} stolen item(s) on death",
host.getNpcName(),
this.stolenItems.size()
);
}
this.stolenItems.clear();
// Drop collar keys at 20% rate
for (ItemStack key : this.collarKeys) {
if (!key.isEmpty() && host.getRandom().nextFloat() < COLLAR_KEY_DROP_CHANCE) {
host.spawnAtLocation(key);
TiedUpMod.LOGGER.info(
"[KidnapperLootManager] {} dropped a collar key on death",
host.getNpcName()
);
}
}
this.collarKeys.clear();
}
// NBT SERIALIZATION
/**
* Save loot data to NBT.
*/
public void saveToNBT(CompoundTag tag) {
// Save stolen items
if (!this.stolenItems.isEmpty()) {
ListTag stolenTag = new ListTag();
for (ItemStack stack : this.stolenItems) {
if (!stack.isEmpty()) {
stolenTag.add(stack.save(new CompoundTag()));
}
}
tag.put("StolenItems", stolenTag);
}
// Save collar keys
if (!this.collarKeys.isEmpty()) {
ListTag keysTag = new ListTag();
for (ItemStack key : this.collarKeys) {
if (!key.isEmpty()) {
keysTag.add(key.save(new CompoundTag()));
}
}
tag.put("CollarKeys", keysTag);
}
}
/**
* Load loot data from NBT.
*/
public void loadFromNBT(CompoundTag tag) {
// Load stolen items
this.stolenItems.clear();
if (tag.contains("StolenItems", Tag.TAG_LIST)) {
ListTag stolenTag = tag.getList("StolenItems", Tag.TAG_COMPOUND);
for (int i = 0; i < stolenTag.size(); i++) {
ItemStack stack = ItemStack.of(stolenTag.getCompound(i));
if (!stack.isEmpty()) {
this.stolenItems.add(stack);
}
}
}
// Load collar keys
this.collarKeys.clear();
if (tag.contains("CollarKeys", Tag.TAG_LIST)) {
ListTag keysTag = tag.getList("CollarKeys", Tag.TAG_COMPOUND);
for (int i = 0; i < keysTag.size(); i++) {
ItemStack key = ItemStack.of(keysTag.getCompound(i));
if (!key.isEmpty()) {
this.collarKeys.add(key);
}
}
}
}
}

View File

@@ -3,6 +3,8 @@ package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.dialogue.EntityDialogueManager; import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.entities.EntityKidnapper; import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.kidnapper.components.IAggressionHost; import com.tiedup.remake.entities.kidnapper.components.IAggressionHost;
import java.util.UUID;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -35,4 +37,15 @@ public class AggressionHost implements IAggressionHost {
) { ) {
entity.talkToPlayersInRadius(category, radius); entity.talkToPlayersInRadius(category, radius);
} }
@Override
@Nullable
public UUID getAssociatedStructure() {
return entity.getAssociatedStructure();
}
@Override
public boolean punishAttackingPrisoner(ServerPlayer player) {
return entity.punishAttackingPrisoner(player);
}
} }

View File

@@ -0,0 +1,53 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.KidnapperTheme;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.entities.kidnapper.components.IDialogueHost;
import com.tiedup.remake.state.IRestrainable;
import net.minecraft.world.entity.LivingEntity;
import org.jetbrains.annotations.Nullable;
/**
* Host implementation for KidnapperDialogue callbacks.
*/
public class DialogueHost implements IDialogueHost {
private final EntityKidnapper entity;
public DialogueHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
@Override
@Nullable
public KidnapperTheme getTheme() {
return entity.getTheme();
}
@Override
public boolean hasCaptives() {
return entity.hasCaptives();
}
@Override
public KidnapperState getCurrentState() {
return entity.getCurrentState();
}
@Override
@Nullable
public IRestrainable getCaptive() {
return entity.getCaptive();
}
@Override
public LivingEntity asEntity() {
return entity;
}
}

View File

@@ -0,0 +1,60 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.KidnapperItemSelector;
import com.tiedup.remake.entities.kidnapper.components.ILootHost;
import net.minecraft.util.RandomSource;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.item.ItemEntity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
/**
* Host implementation for KidnapperLootManager callbacks.
*/
public class LootHost implements ILootHost {
private final EntityKidnapper entity;
public LootHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public Level level() {
return entity.level();
}
@Override
public ItemStack getItemBySlot(EquipmentSlot slot) {
return entity.getItemBySlot(slot);
}
@Override
public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
entity.setItemSlot(slot, stack);
}
@Override
public RandomSource getRandom() {
return entity.getRandom();
}
@Override
@Nullable
public ItemEntity spawnAtLocation(ItemStack stack) {
return entity.spawnAtLocation(stack);
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
@Override
@Nullable
public KidnapperItemSelector.SelectionResult getItemSelection() {
return entity.getItemSelection();
}
}

View File

@@ -8,7 +8,7 @@ import com.tiedup.remake.entities.EntitySlaveTrader;
import com.tiedup.remake.entities.ModEntities; import com.tiedup.remake.entities.ModEntities;
// Prison system v2 // Prison system v2
import com.tiedup.remake.prison.PrisonerManager; import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.service.PrisonerService; import com.tiedup.remake.prison.service.EscapeMonitorService;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import net.minecraft.ChatFormatting; import net.minecraft.ChatFormatting;
@@ -75,7 +75,7 @@ public class CampManagementHandler {
} }
// Prison system v2 - tick escape service (handles escape detection) // Prison system v2 - tick escape service (handles escape detection)
PrisonerService.get().tick(level.getServer(), currentTime); EscapeMonitorService.get().tick(level.getServer(), currentTime);
// Prison system v2 - tick protection expiry // Prison system v2 - tick protection expiry
PrisonerManager.get(level).tickProtectionExpiry(currentTime); PrisonerManager.get(level).tickProtectionExpiry(currentTime);

View File

@@ -288,7 +288,7 @@ public class PlayerStateEventHandler {
// Transition to FREE state via escape (clears all data) // Transition to FREE state via escape (clears all data)
long currentTime = player.serverLevel().getGameTime(); long currentTime = player.serverLevel().getGameTime();
// Use centralized escape service for complete cleanup // Use centralized escape service for complete cleanup
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
player.serverLevel(), player.serverLevel(),
player.getUUID(), player.getUUID(),
"death" "death"

View File

@@ -280,7 +280,7 @@ public class PacketBuyCaptive {
} }
// Release via PrisonerService (state transition + cell cleanup + restraint removal) // Release via PrisonerService (state transition + cell cleanup + restraint removal)
com.tiedup.remake.prison.service.PrisonerService.get().release( com.tiedup.remake.prison.service.EscapeMonitorService.get().release(
level, level,
captiveId, captiveId,
6000L // 5 minutes grace period 6000L // 5 minutes grace period

View File

@@ -113,6 +113,14 @@ public class PrisonerManager extends SavedData {
return laborRecords.computeIfAbsent(playerId, id -> new LaborRecord()); return laborRecords.computeIfAbsent(playerId, id -> new LaborRecord());
} }
/**
* Get labor record only if one exists. Does not create ghost entries.
*/
@Nullable
public LaborRecord getLaborRecordIfExists(UUID playerId) {
return laborRecords.get(playerId);
}
/** /**
* Set the labor record for a player. * Set the labor record for a player.
*/ */

View File

@@ -0,0 +1,617 @@
package com.tiedup.remake.prison.service;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.*;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Escape monitoring service.
*
* Handles all escape detection and processing:
* - Periodic tick-based escape checks (distance, collar, offline timeout)
* - On-demand escape checks (player movement)
* - Central escape/release methods with full cleanup
*
* Split from PrisonerService to separate lifecycle transitions from escape monitoring.
*/
public class EscapeMonitorService {
private static final EscapeMonitorService INSTANCE = new EscapeMonitorService();
public static EscapeMonitorService get() {
return INSTANCE;
}
private EscapeMonitorService() {}
// ==================== CONSTANTS (from EscapeService) ====================
/** Maximum distance from cell for IMPRISONED prisoners (blocks) */
public static final double CELL_ESCAPE_DISTANCE = 20.0;
/** Maximum distance from camp for WORKING prisoners (blocks) */
public static final double WORK_ESCAPE_DISTANCE = 100.0;
/** Maximum time offline before escape (30 minutes in ticks) */
public static final long OFFLINE_TIMEOUT_TICKS = 30 * 60 * 20L;
/** Maximum time in WORKING state before forced return (10 minutes in ticks) */
public static final long WORK_TIMEOUT_TICKS = 10 * 60 * 20L;
/** Maximum time in RETURNING phase before escape (5 minutes in ticks) */
public static final long RETURN_TIMEOUT_TICKS = 5 * 60 * 20L;
/** Maximum time in PENDING_RETURN phase before forced return (2.5 minutes in ticks) */
public static final long PENDING_RETURN_TIMEOUT_TICKS = 3000;
/** Check interval in ticks (every 5 seconds) */
public static final int CHECK_INTERVAL_TICKS = 100;
// ==================== ESCAPE ====================
/**
* CENTRAL ESCAPE METHOD - All escapes must go through here.
*
* Handles complete cleanup:
* - State transition (PrisonerManager)
* - Cell registry cleanup
* - Guard despawn
* - Restraints removal (if online)
* - Inventory NOT restored (stays in camp chest as punishment)
*
* @param level The server level
* @param playerId Player UUID
* @param reason Reason for escape (for logging)
* @return true if escape was processed successfully
*/
public boolean escape(ServerLevel level, UUID playerId, String reason) {
long currentTime = level.getGameTime();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Processing escape for {} - reason: {}",
playerId.toString().substring(0, 8),
reason
);
// Step 1: Save guard ID BEFORE state transition (manager.escape() removes the LaborRecord)
// Use direct map lookup to avoid creating ghost LaborRecord entries for non-WORKING prisoners
LaborRecord laborBeforeEscape = manager.getLaborRecordIfExists(playerId);
UUID guardId = laborBeforeEscape != null ? laborBeforeEscape.getGuardId() : null;
// Step 2: Transition prisoner state to FREE
boolean stateChanged = manager.escape(playerId, currentTime, reason);
if (!stateChanged) {
TiedUpMod.LOGGER.warn(
"[EscapeMonitorService] Failed to change state for {} - invalid transition",
playerId.toString().substring(0, 8)
);
return false;
}
// Step 3: Cleanup CellRegistryV2 - remove from all cells
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsCleared > 0) {
TiedUpMod.LOGGER.debug(
"[EscapeMonitorService] Cleared {} from {} cells",
playerId.toString().substring(0, 8),
cellsCleared
);
}
// Step 4: Cleanup guard using saved reference (LaborRecord was removed by manager.escape())
if (guardId != null) {
net.minecraft.world.entity.Entity guardEntity = level.getEntity(
guardId
);
if (guardEntity != null) {
guardEntity.discard();
TiedUpMod.LOGGER.debug(
"[EscapeMonitorService] Despawned guard {} during escape",
guardId.toString().substring(0, 8)
);
}
}
// Step 5: Free from restraints (if player is online)
// NOTE: Inventory is NOT restored on escape - items remain in camp chest as punishment
ServerPlayer player = level
.getServer()
.getPlayerList()
.getPlayer(playerId);
if (player != null) {
IBondageState cap = KidnappedHelper.getKidnappedState(player);
if (cap != null) {
cap.free(false);
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Freed {} from restraints (inventory remains in camp chest)",
player.getName().getString()
);
}
} else {
TiedUpMod.LOGGER.debug(
"[EscapeMonitorService] Player {} offline - restraints cleanup deferred",
playerId.toString().substring(0, 8)
);
}
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Escape complete for {} - items stay in camp chest",
playerId.toString().substring(0, 8)
);
return true;
}
// ==================== RELEASE ====================
/**
* CENTRAL RELEASE METHOD - All legitimate releases must go through here.
*
* Similar to escape() but transitions to PROTECTED state with grace period.
* Handles complete cleanup:
* - State transition to PROTECTED (PrisonerManager)
* - Cell registry cleanup
* - Inventory restoration
* - Restraints removal (if online)
*
* @param level The server level
* @param playerId Player UUID
* @param gracePeriodTicks Grace period before returning to FREE (0 = instant FREE)
* @return true if release was processed successfully
*/
public boolean release(
ServerLevel level,
UUID playerId,
long gracePeriodTicks
) {
long currentTime = level.getGameTime();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
com.tiedup.remake.cells.ConfiscatedInventoryRegistry inventoryRegistry =
com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level);
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Processing release for {} - grace period: {} ticks",
playerId.toString().substring(0, 8),
gracePeriodTicks
);
// Step 1: Transition prisoner state to PROTECTED (or FREE if gracePeriod = 0)
boolean stateChanged = manager.release(
playerId,
currentTime,
gracePeriodTicks
);
if (!stateChanged) {
TiedUpMod.LOGGER.warn(
"[EscapeMonitorService] Failed to change state for {} - invalid transition",
playerId.toString().substring(0, 8)
);
return false;
}
// Step 2: Cleanup CellRegistryV2 - remove from all cells
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsCleared > 0) {
TiedUpMod.LOGGER.debug(
"[EscapeMonitorService] Cleared {} from {} cells",
playerId.toString().substring(0, 8),
cellsCleared
);
}
// Step 3: Restore confiscated inventory (if player is online)
ServerPlayer player = level
.getServer()
.getPlayerList()
.getPlayer(playerId);
if (player != null) {
// Restore inventory
if (inventoryRegistry.hasConfiscatedInventory(playerId)) {
boolean restored = inventoryRegistry.restoreInventory(player);
if (restored) {
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Restored confiscated inventory for {}",
player.getName().getString()
);
}
}
// Free from restraints
IBondageState cap = KidnappedHelper.getKidnappedState(player);
if (cap != null) {
cap.free(false);
TiedUpMod.LOGGER.debug(
"[EscapeMonitorService] Freed {} from restraints",
player.getName().getString()
);
}
} else {
TiedUpMod.LOGGER.debug(
"[EscapeMonitorService] Player {} offline - inventory cleanup deferred",
playerId.toString().substring(0, 8)
);
}
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Release complete for {} - full cleanup done",
playerId.toString().substring(0, 8)
);
return true;
}
// ==================== TICK (escape detection) ====================
/**
* Tick escape detection.
* Called from CampManagementHandler.
*
* @param server The server
* @param currentTime Current game time
*/
public void tick(MinecraftServer server, long currentTime) {
if (currentTime % CHECK_INTERVAL_TICKS != 0) {
return;
}
ServerLevel level = server.overworld();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cells = CellRegistryV2.get(level);
CollarRegistry collars = CollarRegistry.get(level);
List<EscapeCandidate> escapees = new ArrayList<>();
for (UUID playerId : manager.getAllPrisonerIds()) {
PrisonerRecord record = manager.getRecord(playerId);
PrisonerState state = record.getState();
if (!state.isCaptive()) {
continue;
}
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
// === OFFLINE CHECK ===
if (player == null) {
long timeInState = record.getTimeInState(currentTime);
if (timeInState > OFFLINE_TIMEOUT_TICKS) {
escapees.add(
new EscapeCandidate(playerId, "offline timeout")
);
}
continue;
}
// === COLLAR CHECK ===
if (state != PrisonerState.CAPTURED) {
boolean hasCollarOwners = collars.hasOwners(playerId);
if (!hasCollarOwners) {
IBondageState cap = KidnappedHelper.getKidnappedState(
player
);
ItemStack collar =
cap != null
? cap.getEquipment(BodyRegionV2.NECK)
: ItemStack.EMPTY;
if (
!collar.isEmpty() &&
com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)
) {
List<UUID> nbtOwners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
if (!nbtOwners.isEmpty()) {
for (UUID ownerUUID : nbtOwners) {
collars.registerCollar(playerId, ownerUUID);
}
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Re-synced collar registry for {} - found {} owners in NBT",
playerId.toString().substring(0, 8),
nbtOwners.size()
);
continue;
}
}
TiedUpMod.LOGGER.warn(
"[EscapeMonitorService] Collar check failed for {} (state={}): collarEquipped={}, collarItem={}",
playerId.toString().substring(0, 8),
state,
!collar.isEmpty(),
collar.isEmpty() ? "none" : collar.getItem().toString()
);
escapees.add(
new EscapeCandidate(playerId, "collar removed")
);
continue;
}
}
// === STATE-SPECIFIC CHECKS ===
switch (state) {
case IMPRISONED -> {
UUID cellId = record.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell == null) {
escapees.add(
new EscapeCandidate(
playerId,
"cell no longer exists"
)
);
TiedUpMod.LOGGER.warn(
"[EscapeMonitorService] Prisoner {} has orphaned cellId {} - triggering escape",
playerId.toString().substring(0, 8),
cellId.toString().substring(0, 8)
);
} else {
double distance = Math.sqrt(
player
.blockPosition()
.distSqr(cell.getCorePos())
);
if (distance > CELL_ESCAPE_DISTANCE) {
escapees.add(
new EscapeCandidate(
playerId,
String.format(
"too far from cell (%.1f blocks)",
distance
)
)
);
}
}
}
}
case WORKING -> {
LaborRecord labor = manager.getLaborRecord(playerId);
long timeInPhase = labor.getTimeInPhase(currentTime);
if (
labor.getPhase() == LaborRecord.WorkPhase.WORKING &&
timeInPhase > WORK_TIMEOUT_TICKS
) {
labor.failTask(currentTime);
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] {} work timeout - forcing return",
playerId.toString().substring(0, 8)
);
continue;
}
if (
labor.getPhase() == LaborRecord.WorkPhase.RETURNING &&
timeInPhase > RETURN_TIMEOUT_TICKS
) {
escapees.add(
new EscapeCandidate(playerId, "return timeout")
);
continue;
}
// PENDING_RETURN timeout: maid failed to pick up prisoner — force return to cell
if (
labor.getPhase() ==
LaborRecord.WorkPhase.PENDING_RETURN &&
timeInPhase > PENDING_RETURN_TIMEOUT_TICKS
) {
PrisonerRecord rec = manager.getRecord(playerId);
UUID cellId = rec.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell != null) {
BlockPos teleportPos =
cell.getSpawnPoint() != null
? cell.getSpawnPoint()
: cell.getCorePos().above();
player.teleportTo(
teleportPos.getX() + 0.5,
teleportPos.getY(),
teleportPos.getZ() + 0.5
);
IBondageState cap =
KidnappedHelper.getKidnappedState(player);
if (cap != null) {
CompoundTag snapshot =
labor.getBondageSnapshot();
if (snapshot != null) {
BondageService.get().restoreSnapshot(
cap,
snapshot
);
}
}
PrisonerService.get().returnToCell(level, player, null, cell);
labor.startRest(currentTime);
if (labor.getGuardId() != null) {
net.minecraft.world.entity.Entity guardEntity =
level.getEntity(labor.getGuardId());
if (
guardEntity != null
) guardEntity.discard();
}
player.sendSystemMessage(
Component.translatable(
"msg.tiedup.prison.returned_to_cell"
).withStyle(ChatFormatting.GRAY)
);
TiedUpMod.LOGGER.info(
"[EscapeMonitorService] Force-returned {} to cell (PENDING_RETURN timeout)",
playerId.toString().substring(0, 8)
);
continue;
}
}
// Cell gone → trigger escape
escapees.add(
new EscapeCandidate(
playerId,
"pending_return timeout, cell gone"
)
);
continue;
}
if (labor.getGuardId() != null) {
net.minecraft.world.entity.Entity guardEntity =
level.getEntity(labor.getGuardId());
if (guardEntity != null && guardEntity.isAlive()) {
continue;
}
}
UUID campId = record.getCampId();
if (campId != null) {
List<CellDataV2> campCells = cells.getCellsByCamp(
campId
);
if (campCells.isEmpty()) {
escapees.add(
new EscapeCandidate(
playerId,
"camp no longer exists"
)
);
TiedUpMod.LOGGER.warn(
"[EscapeMonitorService] Worker {} has orphaned campId {} with no cells - triggering escape",
playerId.toString().substring(0, 8),
campId.toString().substring(0, 8)
);
} else {
BlockPos campCenter = campCells.get(0).getCorePos();
double distance = Math.sqrt(
player.blockPosition().distSqr(campCenter)
);
if (distance > WORK_ESCAPE_DISTANCE) {
escapees.add(
new EscapeCandidate(
playerId,
String.format(
"too far from camp (%.1f blocks)",
distance
)
)
);
}
}
}
}
case CAPTURED -> {
// During transport - handled by kidnapper goals
}
default -> {
// Other states don't need distance checks
}
}
}
for (EscapeCandidate candidate : escapees) {
escape(level, candidate.playerId, candidate.reason);
}
}
// ==================== VALIDATION ====================
/**
* Check if a player should be considered escaped.
* Called on-demand (e.g., when player moves).
*/
@Nullable
public String checkEscape(ServerLevel level, ServerPlayer player) {
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cells = CellRegistryV2.get(level);
CollarRegistry collars = CollarRegistry.get(level);
UUID playerId = player.getUUID();
PrisonerRecord record = manager.getRecord(playerId);
PrisonerState state = record.getState();
if (!state.isCaptive()) {
return null;
}
if (!collars.hasOwners(playerId)) {
return "collar removed";
}
switch (state) {
case IMPRISONED -> {
UUID cellId = record.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell != null) {
double distance = Math.sqrt(
player.blockPosition().distSqr(cell.getCorePos())
);
if (distance > CELL_ESCAPE_DISTANCE) {
return String.format(
"too far from cell (%.1f blocks)",
distance
);
}
}
}
}
case WORKING -> {
UUID campId = record.getCampId();
if (campId != null) {
List<CellDataV2> campCells = cells.getCellsByCamp(campId);
if (!campCells.isEmpty()) {
BlockPos campCenter = campCells.get(0).getCorePos();
double distance = Math.sqrt(
player.blockPosition().distSqr(campCenter)
);
if (distance > WORK_ESCAPE_DISTANCE) {
return String.format(
"too far from camp (%.1f blocks)",
distance
);
}
}
}
}
default -> {
// Other states don't have distance limits
}
}
return null;
}
/**
* Handle player movement event.
* Called from event handler to check distance-based escapes.
*/
public void onPlayerMove(ServerPlayer player, ServerLevel level) {
String escapeReason = checkEscape(level, player);
if (escapeReason != null) {
escape(level, player.getUUID(), escapeReason);
}
}
// ==================== HELPER ====================
private record EscapeCandidate(UUID playerId, String reason) {}
}

View File

@@ -6,40 +6,28 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc; import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityDamsel; import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.personality.PersonalityState; import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.prison.PrisonerManager; import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord; import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState; import com.tiedup.remake.prison.PrisonerState;
import com.tiedup.remake.prison.service.BondageService;
import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor; import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.*; import java.util.*;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
/** /**
* Centralized prisoner lifecycle service. * Prisoner lifecycle service.
* *
* Manages ALL prisoner state transitions atomically: * Manages prisoner state transitions atomically:
* - PrisonerManager (SavedData state) * - PrisonerManager (SavedData state)
* - CellRegistryV2 (cell assignment) * - CellRegistryV2 (cell assignment)
* - Leash/captor refs (IBondageState / ICaptor) * - Leash/captor refs (IBondageState / ICaptor)
* - Escape detection (tick)
* *
* Replaces EscapeService and unifies scattered capture/imprison/extract/return/transfer logic. * Handles: capture, imprison, extractFromCell, returnToCell, transferCaptive.
* Escape detection and release are handled by {@link EscapeMonitorService}.
* *
* AI goals manage: Navigation, Teleport, Equipment, Dialogue. * AI goals manage: Navigation, Teleport, Equipment, Dialogue.
* PrisonerService manages: State transitions + leash coordination. * PrisonerService manages: State transitions + leash coordination.
@@ -54,29 +42,6 @@ public class PrisonerService {
private PrisonerService() {} private PrisonerService() {}
// ==================== CONSTANTS (from EscapeService) ====================
/** Maximum distance from cell for IMPRISONED prisoners (blocks) */
public static final double CELL_ESCAPE_DISTANCE = 20.0;
/** Maximum distance from camp for WORKING prisoners (blocks) */
public static final double WORK_ESCAPE_DISTANCE = 100.0;
/** Maximum time offline before escape (30 minutes in ticks) */
public static final long OFFLINE_TIMEOUT_TICKS = 30 * 60 * 20L;
/** Maximum time in WORKING state before forced return (10 minutes in ticks) */
public static final long WORK_TIMEOUT_TICKS = 10 * 60 * 20L;
/** Maximum time in RETURNING phase before escape (5 minutes in ticks) */
public static final long RETURN_TIMEOUT_TICKS = 5 * 60 * 20L;
/** Maximum time in PENDING_RETURN phase before forced return (2.5 minutes in ticks) */
public static final long PENDING_RETURN_TIMEOUT_TICKS = 3000;
/** Check interval in ticks (every 5 seconds) */
public static final int CHECK_INTERVAL_TICKS = 100;
// ==================== CAPTURE ==================== // ==================== CAPTURE ====================
/** /**
@@ -506,552 +471,4 @@ public class PrisonerService {
return true; return true;
} }
// ==================== ESCAPE (from EscapeService) ====================
/**
* CENTRAL ESCAPE METHOD - All escapes must go through here.
*
* Handles complete cleanup:
* - State transition (PrisonerManager)
* - Cell registry cleanup
* - Guard despawn
* - Restraints removal (if online)
* - Inventory NOT restored (stays in camp chest as punishment)
*
* @param level The server level
* @param playerId Player UUID
* @param reason Reason for escape (for logging)
* @return true if escape was processed successfully
*/
public boolean escape(ServerLevel level, UUID playerId, String reason) {
long currentTime = level.getGameTime();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
TiedUpMod.LOGGER.info(
"[PrisonerService] Processing escape for {} - reason: {}",
playerId.toString().substring(0, 8),
reason
);
// Step 1: Save guard ID BEFORE state transition (manager.escape() removes the LaborRecord)
LaborRecord laborBeforeEscape = manager.getLaborRecord(playerId);
UUID guardId = laborBeforeEscape.getGuardId();
// Step 2: Transition prisoner state to FREE
boolean stateChanged = manager.escape(playerId, currentTime, reason);
if (!stateChanged) {
TiedUpMod.LOGGER.warn(
"[PrisonerService] Failed to change state for {} - invalid transition",
playerId.toString().substring(0, 8)
);
return false;
}
// Step 3: Cleanup CellRegistryV2 - remove from all cells
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsCleared > 0) {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Cleared {} from {} cells",
playerId.toString().substring(0, 8),
cellsCleared
);
}
// Step 4: Cleanup guard using saved reference (LaborRecord was removed by manager.escape())
if (guardId != null) {
net.minecraft.world.entity.Entity guardEntity = level.getEntity(
guardId
);
if (guardEntity != null) {
guardEntity.discard();
TiedUpMod.LOGGER.debug(
"[PrisonerService] Despawned guard {} during escape",
guardId.toString().substring(0, 8)
);
}
}
// Step 5: Free from restraints (if player is online)
// NOTE: Inventory is NOT restored on escape - items remain in camp chest as punishment
ServerPlayer player = level
.getServer()
.getPlayerList()
.getPlayer(playerId);
if (player != null) {
IBondageState cap = KidnappedHelper.getKidnappedState(player);
if (cap != null) {
cap.free(false);
TiedUpMod.LOGGER.info(
"[PrisonerService] Freed {} from restraints (inventory remains in camp chest)",
player.getName().getString()
);
}
} else {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Player {} offline - restraints cleanup deferred",
playerId.toString().substring(0, 8)
);
}
TiedUpMod.LOGGER.info(
"[PrisonerService] Escape complete for {} - items stay in camp chest",
playerId.toString().substring(0, 8)
);
return true;
}
// ==================== RELEASE (from EscapeService) ====================
/**
* CENTRAL RELEASE METHOD - All legitimate releases must go through here.
*
* Similar to escape() but transitions to PROTECTED state with grace period.
* Handles complete cleanup:
* - State transition to PROTECTED (PrisonerManager)
* - Cell registry cleanup
* - Inventory restoration
* - Restraints removal (if online)
*
* @param level The server level
* @param playerId Player UUID
* @param gracePeriodTicks Grace period before returning to FREE (0 = instant FREE)
* @return true if release was processed successfully
*/
public boolean release(
ServerLevel level,
UUID playerId,
long gracePeriodTicks
) {
long currentTime = level.getGameTime();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
com.tiedup.remake.cells.ConfiscatedInventoryRegistry inventoryRegistry =
com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level);
TiedUpMod.LOGGER.info(
"[PrisonerService] Processing release for {} - grace period: {} ticks",
playerId.toString().substring(0, 8),
gracePeriodTicks
);
// Step 1: Transition prisoner state to PROTECTED (or FREE if gracePeriod = 0)
boolean stateChanged = manager.release(
playerId,
currentTime,
gracePeriodTicks
);
if (!stateChanged) {
TiedUpMod.LOGGER.warn(
"[PrisonerService] Failed to change state for {} - invalid transition",
playerId.toString().substring(0, 8)
);
return false;
}
// Step 2: Cleanup CellRegistryV2 - remove from all cells
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsCleared > 0) {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Cleared {} from {} cells",
playerId.toString().substring(0, 8),
cellsCleared
);
}
// Step 3: Restore confiscated inventory (if player is online)
ServerPlayer player = level
.getServer()
.getPlayerList()
.getPlayer(playerId);
if (player != null) {
// Restore inventory
if (inventoryRegistry.hasConfiscatedInventory(playerId)) {
boolean restored = inventoryRegistry.restoreInventory(player);
if (restored) {
TiedUpMod.LOGGER.info(
"[PrisonerService] Restored confiscated inventory for {}",
player.getName().getString()
);
}
}
// Free from restraints
IBondageState cap = KidnappedHelper.getKidnappedState(player);
if (cap != null) {
cap.free(false);
TiedUpMod.LOGGER.debug(
"[PrisonerService] Freed {} from restraints",
player.getName().getString()
);
}
} else {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Player {} offline - inventory cleanup deferred",
playerId.toString().substring(0, 8)
);
}
TiedUpMod.LOGGER.info(
"[PrisonerService] Release complete for {} - full cleanup done",
playerId.toString().substring(0, 8)
);
return true;
}
// ==================== TICK (escape detection, from EscapeService) ====================
/**
* Tick escape detection.
* Called from CampManagementHandler.
*
* @param server The server
* @param currentTime Current game time
*/
public void tick(MinecraftServer server, long currentTime) {
if (currentTime % CHECK_INTERVAL_TICKS != 0) {
return;
}
ServerLevel level = server.overworld();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cells = CellRegistryV2.get(level);
CollarRegistry collars = CollarRegistry.get(level);
List<EscapeCandidate> escapees = new ArrayList<>();
for (UUID playerId : manager.getAllPrisonerIds()) {
PrisonerRecord record = manager.getRecord(playerId);
PrisonerState state = record.getState();
if (!state.isCaptive()) {
continue;
}
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
// === OFFLINE CHECK ===
if (player == null) {
long timeInState = record.getTimeInState(currentTime);
if (timeInState > OFFLINE_TIMEOUT_TICKS) {
escapees.add(
new EscapeCandidate(playerId, "offline timeout")
);
}
continue;
}
// === COLLAR CHECK ===
if (state != PrisonerState.CAPTURED) {
boolean hasCollarOwners = collars.hasOwners(playerId);
if (!hasCollarOwners) {
IBondageState cap = KidnappedHelper.getKidnappedState(
player
);
ItemStack collar =
cap != null
? cap.getEquipment(BodyRegionV2.NECK)
: ItemStack.EMPTY;
if (
!collar.isEmpty() &&
com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)
) {
List<UUID> nbtOwners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
if (!nbtOwners.isEmpty()) {
for (UUID ownerUUID : nbtOwners) {
collars.registerCollar(playerId, ownerUUID);
}
TiedUpMod.LOGGER.info(
"[PrisonerService] Re-synced collar registry for {} - found {} owners in NBT",
playerId.toString().substring(0, 8),
nbtOwners.size()
);
continue;
}
}
TiedUpMod.LOGGER.warn(
"[PrisonerService] Collar check failed for {} (state={}): collarEquipped={}, collarItem={}",
playerId.toString().substring(0, 8),
state,
!collar.isEmpty(),
collar.isEmpty() ? "none" : collar.getItem().toString()
);
escapees.add(
new EscapeCandidate(playerId, "collar removed")
);
continue;
}
}
// === STATE-SPECIFIC CHECKS ===
switch (state) {
case IMPRISONED -> {
UUID cellId = record.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell == null) {
escapees.add(
new EscapeCandidate(
playerId,
"cell no longer exists"
)
);
TiedUpMod.LOGGER.warn(
"[PrisonerService] Prisoner {} has orphaned cellId {} - triggering escape",
playerId.toString().substring(0, 8),
cellId.toString().substring(0, 8)
);
} else {
double distance = Math.sqrt(
player
.blockPosition()
.distSqr(cell.getCorePos())
);
if (distance > CELL_ESCAPE_DISTANCE) {
escapees.add(
new EscapeCandidate(
playerId,
String.format(
"too far from cell (%.1f blocks)",
distance
)
)
);
}
}
}
}
case WORKING -> {
LaborRecord labor = manager.getLaborRecord(playerId);
long timeInPhase = labor.getTimeInPhase(currentTime);
if (
labor.getPhase() == LaborRecord.WorkPhase.WORKING &&
timeInPhase > WORK_TIMEOUT_TICKS
) {
labor.failTask(currentTime);
TiedUpMod.LOGGER.info(
"[PrisonerService] {} work timeout - forcing return",
playerId.toString().substring(0, 8)
);
continue;
}
if (
labor.getPhase() == LaborRecord.WorkPhase.RETURNING &&
timeInPhase > RETURN_TIMEOUT_TICKS
) {
escapees.add(
new EscapeCandidate(playerId, "return timeout")
);
continue;
}
// PENDING_RETURN timeout: maid failed to pick up prisoner — force return to cell
if (
labor.getPhase() ==
LaborRecord.WorkPhase.PENDING_RETURN &&
timeInPhase > PENDING_RETURN_TIMEOUT_TICKS
) {
PrisonerRecord rec = manager.getRecord(playerId);
UUID cellId = rec.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell != null) {
BlockPos teleportPos =
cell.getSpawnPoint() != null
? cell.getSpawnPoint()
: cell.getCorePos().above();
player.teleportTo(
teleportPos.getX() + 0.5,
teleportPos.getY(),
teleportPos.getZ() + 0.5
);
IBondageState cap =
KidnappedHelper.getKidnappedState(player);
if (cap != null) {
CompoundTag snapshot =
labor.getBondageSnapshot();
if (snapshot != null) {
BondageService.get().restoreSnapshot(
cap,
snapshot
);
}
}
returnToCell(level, player, null, cell);
labor.startRest(currentTime);
if (labor.getGuardId() != null) {
net.minecraft.world.entity.Entity guardEntity =
level.getEntity(labor.getGuardId());
if (
guardEntity != null
) guardEntity.discard();
}
player.sendSystemMessage(
Component.translatable(
"msg.tiedup.prison.returned_to_cell"
).withStyle(ChatFormatting.GRAY)
);
TiedUpMod.LOGGER.info(
"[PrisonerService] Force-returned {} to cell (PENDING_RETURN timeout)",
playerId.toString().substring(0, 8)
);
continue;
}
}
// Cell gone → trigger escape
escapees.add(
new EscapeCandidate(
playerId,
"pending_return timeout, cell gone"
)
);
continue;
}
if (labor.getGuardId() != null) {
net.minecraft.world.entity.Entity guardEntity =
level.getEntity(labor.getGuardId());
if (guardEntity != null && guardEntity.isAlive()) {
continue;
}
}
UUID campId = record.getCampId();
if (campId != null) {
List<CellDataV2> campCells = cells.getCellsByCamp(
campId
);
if (campCells.isEmpty()) {
escapees.add(
new EscapeCandidate(
playerId,
"camp no longer exists"
)
);
TiedUpMod.LOGGER.warn(
"[PrisonerService] Worker {} has orphaned campId {} with no cells - triggering escape",
playerId.toString().substring(0, 8),
campId.toString().substring(0, 8)
);
} else {
BlockPos campCenter = campCells.get(0).getCorePos();
double distance = Math.sqrt(
player.blockPosition().distSqr(campCenter)
);
if (distance > WORK_ESCAPE_DISTANCE) {
escapees.add(
new EscapeCandidate(
playerId,
String.format(
"too far from camp (%.1f blocks)",
distance
)
)
);
}
}
}
}
case CAPTURED -> {
// During transport - handled by kidnapper goals
}
default -> {
// Other states don't need distance checks
}
}
}
for (EscapeCandidate candidate : escapees) {
escape(level, candidate.playerId, candidate.reason);
}
}
// ==================== VALIDATION ====================
/**
* Check if a player should be considered escaped.
* Called on-demand (e.g., when player moves).
*/
@Nullable
public String checkEscape(ServerLevel level, ServerPlayer player) {
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cells = CellRegistryV2.get(level);
CollarRegistry collars = CollarRegistry.get(level);
UUID playerId = player.getUUID();
PrisonerRecord record = manager.getRecord(playerId);
PrisonerState state = record.getState();
if (!state.isCaptive()) {
return null;
}
if (!collars.hasOwners(playerId)) {
return "collar removed";
}
switch (state) {
case IMPRISONED -> {
UUID cellId = record.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell != null) {
double distance = Math.sqrt(
player.blockPosition().distSqr(cell.getCorePos())
);
if (distance > CELL_ESCAPE_DISTANCE) {
return String.format(
"too far from cell (%.1f blocks)",
distance
);
}
}
}
}
case WORKING -> {
UUID campId = record.getCampId();
if (campId != null) {
List<CellDataV2> campCells = cells.getCellsByCamp(campId);
if (!campCells.isEmpty()) {
BlockPos campCenter = campCells.get(0).getCorePos();
double distance = Math.sqrt(
player.blockPosition().distSqr(campCenter)
);
if (distance > WORK_ESCAPE_DISTANCE) {
return String.format(
"too far from camp (%.1f blocks)",
distance
);
}
}
}
}
default -> {
// Other states don't have distance limits
}
}
return null;
}
/**
* Handle player movement event.
* Called from event handler to check distance-based escapes.
*/
public void onPlayerMove(ServerPlayer player, ServerLevel level) {
String escapeReason = checkEscape(level, player);
if (escapeReason != null) {
escape(level, player.getUUID(), escapeReason);
}
}
// ==================== HELPER ====================
private record EscapeCandidate(UUID playerId, String reason) {}
} }

View File

@@ -122,7 +122,7 @@ public class PlayerLifecycle {
state == com.tiedup.remake.prison.PrisonerState.WORKING state == com.tiedup.remake.prison.PrisonerState.WORKING
) { ) {
// Use centralized escape service for complete cleanup // Use centralized escape service for complete cleanup
com.tiedup.remake.prison.service.PrisonerService.get().escape( com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
serverLevel, serverLevel,
playerId, playerId,
"player_death" "player_death"