refactor/god-class-decomposition #17
@@ -94,7 +94,7 @@ public final class CampLifecycleManager {
|
||||
);
|
||||
} else {
|
||||
// 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,
|
||||
prisonerId,
|
||||
"camp death"
|
||||
|
||||
@@ -640,7 +640,7 @@ public class CellRegistryV2 extends SavedData {
|
||||
currentState ==
|
||||
com.tiedup.remake.prison.PrisonerState.IMPRISONED
|
||||
) {
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
level,
|
||||
id,
|
||||
"offline_cleanup"
|
||||
|
||||
@@ -772,7 +772,7 @@ public class EntityDamsel
|
||||
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
|
||||
state == com.tiedup.remake.prison.PrisonerState.WORKING
|
||||
) {
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
uuid,
|
||||
"player_death"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.tiedup.remake.entities;
|
||||
|
||||
import com.tiedup.remake.cells.CellDataV2;
|
||||
import com.tiedup.remake.cells.CellRegistryV2;
|
||||
import com.tiedup.remake.core.SettingsAccessor;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
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.skins.Gender;
|
||||
import com.tiedup.remake.entities.skins.KidnapperSkinManager;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||
import com.tiedup.remake.personality.PersonalityType;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
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.SaleLoader;
|
||||
import com.tiedup.remake.util.teleport.Position;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import net.minecraft.core.BlockPos;
|
||||
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.syncher.EntityDataAccessor;
|
||||
import net.minecraft.network.syncher.EntityDataSerializers;
|
||||
import net.minecraft.network.syncher.SynchedEntityData;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.*;
|
||||
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.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/**
|
||||
* EntityKidnapper - Aggressive NPC that captures and enslaves players.
|
||||
@@ -87,48 +77,6 @@ public class EntityKidnapper
|
||||
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)
|
||||
|
||||
/**
|
||||
@@ -194,11 +142,11 @@ public class EntityKidnapper
|
||||
/** Whether this kidnapper is currently dogwalking a prisoner. */
|
||||
private boolean dogwalking = false;
|
||||
|
||||
/** Items stolen from players via KidnapperThiefGoal. Dropped at 100% on death. */
|
||||
private final List<ItemStack> stolenItems = new ArrayList<>();
|
||||
/** Loot manager for stolen items, collar keys, and death drops. */
|
||||
private final com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager lootManager;
|
||||
|
||||
/** Collar keys generated when collaring captives. Dropped at 20% on death. */
|
||||
private final List<ItemStack> collarKeys = new ArrayList<>();
|
||||
/** Dialogue speaker implementation for IDialogueSpeaker delegation. */
|
||||
private final com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue dialogue;
|
||||
|
||||
/** Job manager handles job assignment and tracking. */
|
||||
private final KidnapperJobManager jobManager = new KidnapperJobManager(
|
||||
@@ -275,6 +223,18 @@ public class EntityKidnapper
|
||||
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
|
||||
this.stateManager =
|
||||
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.
|
||||
// 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
|
||||
public void addCaptive(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.addCaptive(r);
|
||||
} else {
|
||||
logBridgeWarning("addCaptive", captive);
|
||||
}
|
||||
withRestrainable("addCaptive", captive, captiveManager::addCaptive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCaptive(IBondageState captive, boolean transportState) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.removeCaptive(r, transportState);
|
||||
} else {
|
||||
logBridgeWarning("removeCaptive", captive);
|
||||
}
|
||||
withRestrainable("removeCaptive", captive, r -> captiveManager.removeCaptive(r, transportState));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCapture(IBondageState captive) {
|
||||
if (
|
||||
captive instanceof IRestrainable r
|
||||
) return captiveManager.canCapture(r);
|
||||
logBridgeWarning("canCapture", captive);
|
||||
return false;
|
||||
return testRestrainable("canCapture", captive, captiveManager::canCapture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRelease(IBondageState captive) {
|
||||
if (
|
||||
captive instanceof IRestrainable r
|
||||
) return captiveManager.canRelease(r);
|
||||
logBridgeWarning("canRelease", captive);
|
||||
return false;
|
||||
return testRestrainable("canRelease", captive, captiveManager::canRelease);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -633,29 +588,17 @@ public class EntityKidnapper
|
||||
|
||||
@Override
|
||||
public void onCaptiveLogout(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.onCaptiveLogout(r);
|
||||
} else {
|
||||
logBridgeWarning("onCaptiveLogout", captive);
|
||||
}
|
||||
withRestrainable("onCaptiveLogout", captive, captiveManager::onCaptiveLogout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCaptiveReleased(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.onCaptiveReleased(r);
|
||||
} else {
|
||||
logBridgeWarning("onCaptiveReleased", captive);
|
||||
}
|
||||
withRestrainable("onCaptiveReleased", captive, captiveManager::onCaptiveReleased);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCaptiveStruggle(IBondageState captive) {
|
||||
if (captive instanceof IRestrainable r) {
|
||||
captiveManager.onCaptiveStruggle(r);
|
||||
} else {
|
||||
logBridgeWarning("onCaptiveStruggle", captive);
|
||||
}
|
||||
withRestrainable("onCaptiveStruggle", captive, captiveManager::onCaptiveStruggle);
|
||||
}
|
||||
|
||||
private void logBridgeWarning(String method, IBondageState captive) {
|
||||
@@ -936,130 +879,15 @@ public class EntityKidnapper
|
||||
super.die(damageSource);
|
||||
}
|
||||
|
||||
/** Token drop chance (5%) */
|
||||
private static final float TOKEN_DROP_CHANCE = 0.05f;
|
||||
|
||||
/**
|
||||
* Prevent taser from dropping when kidnapper dies.
|
||||
* Taser is unique to kidnappers and should not be obtainable by players.
|
||||
* Also handles token drop (5% chance).
|
||||
* Delegates loot logic to KidnapperLootManager.
|
||||
*/
|
||||
@Override
|
||||
protected void dropEquipment() {
|
||||
// Check main hand for taser - don't drop it
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
lootManager.dropEquipment();
|
||||
super.dropEquipment();
|
||||
|
||||
// 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();
|
||||
}
|
||||
lootManager.dropPostEquipment();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1255,27 +1083,8 @@ public class EntityKidnapper
|
||||
// Delegate to data serializer
|
||||
dataSerializer.saveToNBT(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);
|
||||
}
|
||||
// Delegate stolen items and collar keys to loot manager
|
||||
lootManager.saveToNBT(tag);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1285,29 +1094,8 @@ public class EntityKidnapper
|
||||
// Delegate to data serializer
|
||||
dataSerializer.loadFromNBT(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Delegate stolen items and collar keys to loot manager
|
||||
lootManager.loadFromNBT(tag);
|
||||
}
|
||||
|
||||
// ESCAPE TRACKING METHODS
|
||||
@@ -1370,59 +1158,14 @@ public class EntityKidnapper
|
||||
return CollarHelper.isOwner(collar, player);
|
||||
}
|
||||
|
||||
/** Damage reduction multiplier against monsters (50% damage taken) */
|
||||
private static final float MONSTER_DAMAGE_REDUCTION = 0.5f;
|
||||
|
||||
@Override
|
||||
public boolean hurt(
|
||||
net.minecraft.world.damagesource.DamageSource source,
|
||||
float amount
|
||||
) {
|
||||
float finalAmount = amount;
|
||||
|
||||
// Track the attacker for fight back system
|
||||
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);
|
||||
float modified = aggressionSystem.processIncomingDamage(source, amount);
|
||||
if (modified <= 0) return false;
|
||||
return super.hurt(source, modified);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1432,7 +1175,7 @@ public class EntityKidnapper
|
||||
* @param player The player who attacked
|
||||
* @return true if punishment was applied
|
||||
*/
|
||||
protected boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||
public boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||
return captiveManager.punishAttackingPrisoner(player);
|
||||
}
|
||||
|
||||
@@ -1912,76 +1655,31 @@ public class EntityKidnapper
|
||||
com.tiedup.remake.util.MessageDispatcher.actionTo(this, player, action);
|
||||
}
|
||||
|
||||
/** Dialogue cooldown timer (ticks remaining before next dialogue) */
|
||||
private int dialogueCooldown = 0;
|
||||
|
||||
@Override
|
||||
public String getDialogueName() {
|
||||
return this.getNpcName();
|
||||
return dialogue.getDialogueName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpeakerType getSpeakerType() {
|
||||
return SpeakerType.KIDNAPPER;
|
||||
return dialogue.getSpeakerType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public PersonalityType getSpeakerPersonality() {
|
||||
// Map kidnapper theme to a personality-like behavior
|
||||
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
|
||||
};
|
||||
return dialogue.getSpeakerPersonality();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpeakerMood() {
|
||||
// Kidnappers mood is based on:
|
||||
// - 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));
|
||||
return dialogue.getSpeakerMood();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getTargetRelation(Player player) {
|
||||
// Check if this kidnapper is holding the player captive
|
||||
IRestrainable captive = this.getCaptive();
|
||||
if (captive != null && captive.asLivingEntity() == player) {
|
||||
return "captor";
|
||||
}
|
||||
|
||||
return null;
|
||||
return dialogue.getTargetRelation(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1991,12 +1689,12 @@ public class EntityKidnapper
|
||||
|
||||
@Override
|
||||
public int getDialogueCooldown() {
|
||||
return this.dialogueCooldown;
|
||||
return dialogue.getDialogueCooldown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDialogueCooldown(int ticks) {
|
||||
this.dialogueCooldown = ticks;
|
||||
dialogue.setDialogueCooldown(ticks);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2004,9 +1702,7 @@ public class EntityKidnapper
|
||||
* Called from the main tick method.
|
||||
*/
|
||||
protected void tickDialogueCooldown() {
|
||||
if (this.dialogueCooldown > 0) {
|
||||
this.dialogueCooldown--;
|
||||
}
|
||||
dialogue.tickDialogueCooldown();
|
||||
}
|
||||
|
||||
// STOLEN ITEMS (Thief Goal)
|
||||
@@ -2016,9 +1712,7 @@ public class EntityKidnapper
|
||||
* Called by KidnapperThiefGoal when stealing from a player.
|
||||
*/
|
||||
public void addStolenItem(ItemStack stack) {
|
||||
if (!stack.isEmpty()) {
|
||||
this.stolenItems.add(stack.copy());
|
||||
}
|
||||
lootManager.addStolenItem(stack);
|
||||
}
|
||||
|
||||
// COLLAR KEYS (Capture Goal)
|
||||
@@ -2028,8 +1722,6 @@ public class EntityKidnapper
|
||||
* Called by KidnapperCaptureGoal when collaring a captive.
|
||||
*/
|
||||
public void addCollarKey(ItemStack keyStack) {
|
||||
if (!keyStack.isEmpty()) {
|
||||
this.collarKeys.add(keyStack.copy());
|
||||
}
|
||||
lootManager.addCollarKey(keyStack);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.tiedup.remake.entities.skins.LaborGuardSkinManager;
|
||||
import com.tiedup.remake.labor.LaborTask;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
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.util.KidnappedHelper;
|
||||
import com.tiedup.remake.util.MessageDispatcher;
|
||||
@@ -520,7 +520,7 @@ public class EntityLaborGuard extends EntityDamsel {
|
||||
labor.setGuardId(null);
|
||||
|
||||
// Trigger escape
|
||||
PrisonerService.get().escape(level, prisonerUUID, reason);
|
||||
EscapeMonitorService.get().escape(level, prisonerUUID, reason);
|
||||
|
||||
// Notify prisoner
|
||||
ServerPlayer prisoner = level
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.labor.LaborTask;
|
||||
import com.tiedup.remake.prison.LaborRecord;
|
||||
import com.tiedup.remake.prison.PrisonerManager;
|
||||
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.util.KidnappedHelper;
|
||||
import com.tiedup.remake.util.MessageDispatcher;
|
||||
@@ -562,7 +562,7 @@ public class GuardMonitorGoal extends Goal {
|
||||
LaborRecord labor = manager.getLaborRecord(prisoner.getUUID());
|
||||
labor.setGuardId(null);
|
||||
|
||||
PrisonerService.get().escape(level, prisoner.getUUID(), reason);
|
||||
EscapeMonitorService.get().escape(level, prisoner.getUUID(), reason);
|
||||
|
||||
// Discard the guard entity itself
|
||||
guard.discard();
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
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.WaypointNavigator;
|
||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
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.v2.BodyRegionV2;
|
||||
import java.util.ArrayList;
|
||||
@@ -574,7 +574,7 @@ public class KidnapperDecideNextActionGoal extends Goal {
|
||||
captive.free(false); // false = don't drop leash (we take it)
|
||||
|
||||
// 3b. Clean up PrisonerManager state (CAPTURED -> FREE)
|
||||
PrisonerService.get().escape(
|
||||
EscapeMonitorService.get().escape(
|
||||
(ServerLevel) player.level(),
|
||||
player.getUUID(),
|
||||
"theft_release"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.tiedup.remake.entities.kidnapper.components;
|
||||
|
||||
import java.util.UUID;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -26,4 +28,18 @@ public interface IAggressionHost {
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import com.tiedup.remake.state.IBondageState;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -232,6 +235,69 @@ public class KidnapperAggressionSystem {
|
||||
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
|
||||
|
||||
/**
|
||||
|
||||
@@ -508,7 +508,7 @@ public class KidnapperCaptiveManager {
|
||||
if (record != null && record.isImprisoned()) {
|
||||
// Clear captivity state - prisoner escaped
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
captiveUUID,
|
||||
"kidnapper tied up"
|
||||
@@ -545,7 +545,7 @@ public class KidnapperCaptiveManager {
|
||||
if (record != null && record.isImprisoned()) {
|
||||
// Clear captivity state - captor died
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
captiveUUID,
|
||||
"kidnapper died"
|
||||
@@ -670,7 +670,7 @@ public class KidnapperCaptiveManager {
|
||||
if (record != null && record.isImprisoned()) {
|
||||
// Clear captivity state - prisoner freed
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
captiveUUID,
|
||||
"abandoned by kidnapper"
|
||||
|
||||
@@ -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--;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.tiedup.remake.entities.kidnapper.hosts;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
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 org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -35,4 +37,15 @@ public class AggressionHost implements IAggressionHost {
|
||||
) {
|
||||
entity.talkToPlayersInRadius(category, radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public UUID getAssociatedStructure() {
|
||||
return entity.getAssociatedStructure();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||
return entity.punishAttackingPrisoner(player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.entities.EntitySlaveTrader;
|
||||
import com.tiedup.remake.entities.ModEntities;
|
||||
// Prison system v2
|
||||
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.UUID;
|
||||
import net.minecraft.ChatFormatting;
|
||||
@@ -75,7 +75,7 @@ public class CampManagementHandler {
|
||||
}
|
||||
|
||||
// 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
|
||||
PrisonerManager.get(level).tickProtectionExpiry(currentTime);
|
||||
|
||||
@@ -288,7 +288,7 @@ public class PlayerStateEventHandler {
|
||||
// Transition to FREE state via escape (clears all data)
|
||||
long currentTime = player.serverLevel().getGameTime();
|
||||
// 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.getUUID(),
|
||||
"death"
|
||||
|
||||
@@ -280,7 +280,7 @@ public class PacketBuyCaptive {
|
||||
}
|
||||
|
||||
// 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,
|
||||
captiveId,
|
||||
6000L // 5 minutes grace period
|
||||
|
||||
@@ -113,6 +113,14 @@ public class PrisonerManager extends SavedData {
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -6,40 +6,28 @@ import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.personality.PersonalityState;
|
||||
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.prison.service.BondageService;
|
||||
import com.tiedup.remake.state.CollarRegistry;
|
||||
import com.tiedup.remake.state.IBondageState;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
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.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
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)
|
||||
* - CellRegistryV2 (cell assignment)
|
||||
* - 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.
|
||||
* PrisonerService manages: State transitions + leash coordination.
|
||||
@@ -54,29 +42,6 @@ public class 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 ====================
|
||||
|
||||
/**
|
||||
@@ -506,552 +471,4 @@ public class PrisonerService {
|
||||
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) {}
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ public class PlayerLifecycle {
|
||||
state == com.tiedup.remake.prison.PrisonerState.WORKING
|
||||
) {
|
||||
// Use centralized escape service for complete cleanup
|
||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
||||
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||
serverLevel,
|
||||
playerId,
|
||||
"player_death"
|
||||
|
||||
Reference in New Issue
Block a user