refactor/god-class-decomposition #17
@@ -94,7 +94,7 @@ public final class CampLifecycleManager {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Offline: full escape via PrisonerService (no grace period needed)
|
// Offline: full escape via PrisonerService (no grace period needed)
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
level,
|
level,
|
||||||
prisonerId,
|
prisonerId,
|
||||||
"camp death"
|
"camp death"
|
||||||
|
|||||||
@@ -640,7 +640,7 @@ public class CellRegistryV2 extends SavedData {
|
|||||||
currentState ==
|
currentState ==
|
||||||
com.tiedup.remake.prison.PrisonerState.IMPRISONED
|
com.tiedup.remake.prison.PrisonerState.IMPRISONED
|
||||||
) {
|
) {
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
level,
|
level,
|
||||||
id,
|
id,
|
||||||
"offline_cleanup"
|
"offline_cleanup"
|
||||||
|
|||||||
@@ -772,7 +772,7 @@ public class EntityDamsel
|
|||||||
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
|
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
|
||||||
state == com.tiedup.remake.prison.PrisonerState.WORKING
|
state == com.tiedup.remake.prison.PrisonerState.WORKING
|
||||||
) {
|
) {
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
serverLevel,
|
serverLevel,
|
||||||
uuid,
|
uuid,
|
||||||
"player_death"
|
"player_death"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.tiedup.remake.entities;
|
package com.tiedup.remake.entities;
|
||||||
|
|
||||||
import com.tiedup.remake.cells.CellDataV2;
|
import com.tiedup.remake.cells.CellDataV2;
|
||||||
import com.tiedup.remake.cells.CellRegistryV2;
|
|
||||||
import com.tiedup.remake.core.SettingsAccessor;
|
import com.tiedup.remake.core.SettingsAccessor;
|
||||||
import com.tiedup.remake.core.TiedUpMod;
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
import com.tiedup.remake.dialogue.IDialogueSpeaker;
|
import com.tiedup.remake.dialogue.IDialogueSpeaker;
|
||||||
@@ -11,31 +10,23 @@ import com.tiedup.remake.entities.ai.kidnapper.*;
|
|||||||
import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem;
|
import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem;
|
||||||
import com.tiedup.remake.entities.skins.Gender;
|
import com.tiedup.remake.entities.skins.Gender;
|
||||||
import com.tiedup.remake.entities.skins.KidnapperSkinManager;
|
import com.tiedup.remake.entities.skins.KidnapperSkinManager;
|
||||||
import com.tiedup.remake.items.ModItems;
|
|
||||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||||
import com.tiedup.remake.personality.PersonalityType;
|
import com.tiedup.remake.personality.PersonalityType;
|
||||||
import com.tiedup.remake.state.IBondageState;
|
import com.tiedup.remake.state.IBondageState;
|
||||||
import com.tiedup.remake.state.ICaptor;
|
import com.tiedup.remake.state.ICaptor;
|
||||||
import com.tiedup.remake.state.IRestrainable;
|
import com.tiedup.remake.state.IRestrainable;
|
||||||
import com.tiedup.remake.util.KidnappedHelper;
|
|
||||||
import com.tiedup.remake.util.tasks.ItemTask;
|
import com.tiedup.remake.util.tasks.ItemTask;
|
||||||
import com.tiedup.remake.util.tasks.SaleLoader;
|
|
||||||
import com.tiedup.remake.util.teleport.Position;
|
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.nbt.CompoundTag;
|
import net.minecraft.nbt.CompoundTag;
|
||||||
import net.minecraft.nbt.ListTag;
|
|
||||||
import net.minecraft.nbt.Tag;
|
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.network.syncher.EntityDataAccessor;
|
import net.minecraft.network.syncher.EntityDataAccessor;
|
||||||
import net.minecraft.network.syncher.EntityDataSerializers;
|
import net.minecraft.network.syncher.EntityDataSerializers;
|
||||||
import net.minecraft.network.syncher.SynchedEntityData;
|
import net.minecraft.network.syncher.SynchedEntityData;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.server.level.ServerLevel;
|
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.entity.*;
|
import net.minecraft.world.entity.*;
|
||||||
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
|
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
|
||||||
@@ -44,7 +35,6 @@ import net.minecraft.world.entity.ai.goal.*;
|
|||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import net.minecraft.world.phys.AABB;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EntityKidnapper - Aggressive NPC that captures and enslaves players.
|
* EntityKidnapper - Aggressive NPC that captures and enslaves players.
|
||||||
@@ -87,48 +77,6 @@ public class EntityKidnapper
|
|||||||
implements ICaptor, IDialogueSpeaker
|
implements ICaptor, IDialogueSpeaker
|
||||||
{
|
{
|
||||||
|
|
||||||
// CAPTIVE PRIORITY (for prisoner replacement)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Priority levels for captives when replacing prisoners in cells.
|
|
||||||
* Higher priority captives will cause lower priority prisoners to be released.
|
|
||||||
*/
|
|
||||||
public enum CaptivePriority {
|
|
||||||
DAMSEL(1),
|
|
||||||
DAMSEL_SHINY(2),
|
|
||||||
PLAYER(3);
|
|
||||||
|
|
||||||
private final int priority;
|
|
||||||
|
|
||||||
CaptivePriority(int priority) {
|
|
||||||
this.priority = priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPriority() {
|
|
||||||
return priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the priority for an entity.
|
|
||||||
*
|
|
||||||
* @param entity The entity to check
|
|
||||||
* @return The captive priority
|
|
||||||
*/
|
|
||||||
public static CaptivePriority fromEntity(LivingEntity entity) {
|
|
||||||
if (entity instanceof Player) return PLAYER;
|
|
||||||
if (entity instanceof EntityDamselShiny) return DAMSEL_SHINY;
|
|
||||||
if (entity instanceof EntityDamsel) return DAMSEL;
|
|
||||||
return DAMSEL; // Default for unknown entities
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this priority is higher than another.
|
|
||||||
*/
|
|
||||||
public boolean isHigherThan(CaptivePriority other) {
|
|
||||||
return this.priority > other.priority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DATA SYNC (Client-Server)
|
// DATA SYNC (Client-Server)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,11 +142,11 @@ public class EntityKidnapper
|
|||||||
/** Whether this kidnapper is currently dogwalking a prisoner. */
|
/** Whether this kidnapper is currently dogwalking a prisoner. */
|
||||||
private boolean dogwalking = false;
|
private boolean dogwalking = false;
|
||||||
|
|
||||||
/** Items stolen from players via KidnapperThiefGoal. Dropped at 100% on death. */
|
/** Loot manager for stolen items, collar keys, and death drops. */
|
||||||
private final List<ItemStack> stolenItems = new ArrayList<>();
|
private final com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager lootManager;
|
||||||
|
|
||||||
/** Collar keys generated when collaring captives. Dropped at 20% on death. */
|
/** Dialogue speaker implementation for IDialogueSpeaker delegation. */
|
||||||
private final List<ItemStack> collarKeys = new ArrayList<>();
|
private final com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue dialogue;
|
||||||
|
|
||||||
/** Job manager handles job assignment and tracking. */
|
/** Job manager handles job assignment and tracking. */
|
||||||
private final KidnapperJobManager jobManager = new KidnapperJobManager(
|
private final KidnapperJobManager jobManager = new KidnapperJobManager(
|
||||||
@@ -275,6 +223,18 @@ public class EntityKidnapper
|
|||||||
DATA_THEME_COLOR
|
DATA_THEME_COLOR
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Initialize loot manager
|
||||||
|
this.lootManager =
|
||||||
|
new com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager(
|
||||||
|
new com.tiedup.remake.entities.kidnapper.hosts.LootHost(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize dialogue component
|
||||||
|
this.dialogue =
|
||||||
|
new com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue(
|
||||||
|
new com.tiedup.remake.entities.kidnapper.hosts.DialogueHost(this)
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize state manager
|
// Initialize state manager
|
||||||
this.stateManager =
|
this.stateManager =
|
||||||
new com.tiedup.remake.entities.kidnapper.components.KidnapperStateManager(
|
new com.tiedup.remake.entities.kidnapper.components.KidnapperStateManager(
|
||||||
@@ -585,40 +545,35 @@ public class EntityKidnapper
|
|||||||
// All real IBondageState instances are IRestrainable, so the cast is safe.
|
// All real IBondageState instances are IRestrainable, so the cast is safe.
|
||||||
// Log a warning if the invariant is ever broken (future-proofing).
|
// Log a warning if the invariant is ever broken (future-proofing).
|
||||||
|
|
||||||
|
private void withRestrainable(String method, IBondageState captive, java.util.function.Consumer<IRestrainable> action) {
|
||||||
|
if (captive instanceof IRestrainable r) action.accept(r);
|
||||||
|
else logBridgeWarning(method, captive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean testRestrainable(String method, IBondageState captive, java.util.function.Predicate<IRestrainable> test) {
|
||||||
|
if (captive instanceof IRestrainable r) return test.test(r);
|
||||||
|
logBridgeWarning(method, captive);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCaptive(IBondageState captive) {
|
public void addCaptive(IBondageState captive) {
|
||||||
if (captive instanceof IRestrainable r) {
|
withRestrainable("addCaptive", captive, captiveManager::addCaptive);
|
||||||
captiveManager.addCaptive(r);
|
|
||||||
} else {
|
|
||||||
logBridgeWarning("addCaptive", captive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeCaptive(IBondageState captive, boolean transportState) {
|
public void removeCaptive(IBondageState captive, boolean transportState) {
|
||||||
if (captive instanceof IRestrainable r) {
|
withRestrainable("removeCaptive", captive, r -> captiveManager.removeCaptive(r, transportState));
|
||||||
captiveManager.removeCaptive(r, transportState);
|
|
||||||
} else {
|
|
||||||
logBridgeWarning("removeCaptive", captive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canCapture(IBondageState captive) {
|
public boolean canCapture(IBondageState captive) {
|
||||||
if (
|
return testRestrainable("canCapture", captive, captiveManager::canCapture);
|
||||||
captive instanceof IRestrainable r
|
|
||||||
) return captiveManager.canCapture(r);
|
|
||||||
logBridgeWarning("canCapture", captive);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean canRelease(IBondageState captive) {
|
public boolean canRelease(IBondageState captive) {
|
||||||
if (
|
return testRestrainable("canRelease", captive, captiveManager::canRelease);
|
||||||
captive instanceof IRestrainable r
|
|
||||||
) return captiveManager.canRelease(r);
|
|
||||||
logBridgeWarning("canRelease", captive);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -633,29 +588,17 @@ public class EntityKidnapper
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCaptiveLogout(IBondageState captive) {
|
public void onCaptiveLogout(IBondageState captive) {
|
||||||
if (captive instanceof IRestrainable r) {
|
withRestrainable("onCaptiveLogout", captive, captiveManager::onCaptiveLogout);
|
||||||
captiveManager.onCaptiveLogout(r);
|
|
||||||
} else {
|
|
||||||
logBridgeWarning("onCaptiveLogout", captive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCaptiveReleased(IBondageState captive) {
|
public void onCaptiveReleased(IBondageState captive) {
|
||||||
if (captive instanceof IRestrainable r) {
|
withRestrainable("onCaptiveReleased", captive, captiveManager::onCaptiveReleased);
|
||||||
captiveManager.onCaptiveReleased(r);
|
|
||||||
} else {
|
|
||||||
logBridgeWarning("onCaptiveReleased", captive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCaptiveStruggle(IBondageState captive) {
|
public void onCaptiveStruggle(IBondageState captive) {
|
||||||
if (captive instanceof IRestrainable r) {
|
withRestrainable("onCaptiveStruggle", captive, captiveManager::onCaptiveStruggle);
|
||||||
captiveManager.onCaptiveStruggle(r);
|
|
||||||
} else {
|
|
||||||
logBridgeWarning("onCaptiveStruggle", captive);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logBridgeWarning(String method, IBondageState captive) {
|
private void logBridgeWarning(String method, IBondageState captive) {
|
||||||
@@ -936,130 +879,15 @@ public class EntityKidnapper
|
|||||||
super.die(damageSource);
|
super.die(damageSource);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Token drop chance (5%) */
|
|
||||||
private static final float TOKEN_DROP_CHANCE = 0.05f;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prevent taser from dropping when kidnapper dies.
|
* Prevent taser from dropping when kidnapper dies.
|
||||||
* Taser is unique to kidnappers and should not be obtainable by players.
|
* Delegates loot logic to KidnapperLootManager.
|
||||||
* Also handles token drop (5% chance).
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void dropEquipment() {
|
protected void dropEquipment() {
|
||||||
// Check main hand for taser - don't drop it
|
lootManager.dropEquipment();
|
||||||
ItemStack mainHand = this.getItemBySlot(
|
|
||||||
net.minecraft.world.entity.EquipmentSlot.MAINHAND
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
!mainHand.isEmpty() &&
|
|
||||||
mainHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
|
|
||||||
) {
|
|
||||||
this.setItemSlot(
|
|
||||||
net.minecraft.world.entity.EquipmentSlot.MAINHAND,
|
|
||||||
ItemStack.EMPTY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check off hand too
|
|
||||||
ItemStack offHand = this.getItemBySlot(
|
|
||||||
net.minecraft.world.entity.EquipmentSlot.OFFHAND
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
!offHand.isEmpty() &&
|
|
||||||
offHand.getItem() instanceof com.tiedup.remake.items.ItemTaser
|
|
||||||
) {
|
|
||||||
this.setItemSlot(
|
|
||||||
net.minecraft.world.entity.EquipmentSlot.OFFHAND,
|
|
||||||
ItemStack.EMPTY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.dropEquipment();
|
super.dropEquipment();
|
||||||
|
lootManager.dropPostEquipment();
|
||||||
// Token drop: 5% chance when killed
|
|
||||||
if (
|
|
||||||
!this.level().isClientSide &&
|
|
||||||
this.getRandom().nextFloat() < TOKEN_DROP_CHANCE
|
|
||||||
) {
|
|
||||||
ItemStack token = new ItemStack(
|
|
||||||
com.tiedup.remake.items.ModItems.TOKEN.get()
|
|
||||||
);
|
|
||||||
this.spawnAtLocation(token);
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[EntityKidnapper] {} dropped a token on death!",
|
|
||||||
this.getNpcName()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind item drops from kidnapper inventory
|
|
||||||
if (!this.level().isClientSide) {
|
|
||||||
KidnapperItemSelector.SelectionResult selection =
|
|
||||||
getItemSelection();
|
|
||||||
if (selection != null) {
|
|
||||||
float dropChance = 0.15f;
|
|
||||||
if (
|
|
||||||
!selection.bind.isEmpty() &&
|
|
||||||
this.getRandom().nextFloat() < dropChance
|
|
||||||
) {
|
|
||||||
this.spawnAtLocation(selection.bind.copy());
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
selection.hasGag() &&
|
|
||||||
this.getRandom().nextFloat() < dropChance
|
|
||||||
) {
|
|
||||||
this.spawnAtLocation(selection.gag.copy());
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
selection.hasMittens() &&
|
|
||||||
this.getRandom().nextFloat() < dropChance
|
|
||||||
) {
|
|
||||||
this.spawnAtLocation(selection.mittens.copy());
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
selection.hasEarplugs() &&
|
|
||||||
this.getRandom().nextFloat() < dropChance
|
|
||||||
) {
|
|
||||||
this.spawnAtLocation(selection.earplugs.copy());
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
selection.hasBlindfold() &&
|
|
||||||
this.getRandom().nextFloat() < dropChance
|
|
||||||
) {
|
|
||||||
this.spawnAtLocation(selection.blindfold.copy());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop stolen items at 100% rate (player's property)
|
|
||||||
if (!this.level().isClientSide) {
|
|
||||||
for (ItemStack stolen : this.stolenItems) {
|
|
||||||
if (!stolen.isEmpty()) {
|
|
||||||
this.spawnAtLocation(stolen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.stolenItems.isEmpty()) {
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[EntityKidnapper] {} dropped {} stolen item(s) on death",
|
|
||||||
this.getNpcName(),
|
|
||||||
this.stolenItems.size()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.stolenItems.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drop collar keys at 20% rate
|
|
||||||
if (!this.level().isClientSide) {
|
|
||||||
for (ItemStack key : this.collarKeys) {
|
|
||||||
if (!key.isEmpty() && this.getRandom().nextFloat() < 0.20f) {
|
|
||||||
this.spawnAtLocation(key);
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[EntityKidnapper] {} dropped a collar key on death",
|
|
||||||
this.getNpcName()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.collarKeys.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1255,27 +1083,8 @@ public class EntityKidnapper
|
|||||||
// Delegate to data serializer
|
// Delegate to data serializer
|
||||||
dataSerializer.saveToNBT(tag);
|
dataSerializer.saveToNBT(tag);
|
||||||
|
|
||||||
// Save stolen items
|
// Delegate stolen items and collar keys to loot manager
|
||||||
if (!this.stolenItems.isEmpty()) {
|
lootManager.saveToNBT(tag);
|
||||||
ListTag stolenTag = new ListTag();
|
|
||||||
for (ItemStack stack : this.stolenItems) {
|
|
||||||
if (!stack.isEmpty()) {
|
|
||||||
stolenTag.add(stack.save(new CompoundTag()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag.put("StolenItems", stolenTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save collar keys
|
|
||||||
if (!this.collarKeys.isEmpty()) {
|
|
||||||
ListTag keysTag = new ListTag();
|
|
||||||
for (ItemStack key : this.collarKeys) {
|
|
||||||
if (!key.isEmpty()) {
|
|
||||||
keysTag.add(key.save(new CompoundTag()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tag.put("CollarKeys", keysTag);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1285,29 +1094,8 @@ public class EntityKidnapper
|
|||||||
// Delegate to data serializer
|
// Delegate to data serializer
|
||||||
dataSerializer.loadFromNBT(tag);
|
dataSerializer.loadFromNBT(tag);
|
||||||
|
|
||||||
// Load stolen items
|
// Delegate stolen items and collar keys to loot manager
|
||||||
this.stolenItems.clear();
|
lootManager.loadFromNBT(tag);
|
||||||
if (tag.contains("StolenItems", Tag.TAG_LIST)) {
|
|
||||||
ListTag stolenTag = tag.getList("StolenItems", Tag.TAG_COMPOUND);
|
|
||||||
for (int i = 0; i < stolenTag.size(); i++) {
|
|
||||||
ItemStack stack = ItemStack.of(stolenTag.getCompound(i));
|
|
||||||
if (!stack.isEmpty()) {
|
|
||||||
this.stolenItems.add(stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load collar keys
|
|
||||||
this.collarKeys.clear();
|
|
||||||
if (tag.contains("CollarKeys", Tag.TAG_LIST)) {
|
|
||||||
ListTag keysTag = tag.getList("CollarKeys", Tag.TAG_COMPOUND);
|
|
||||||
for (int i = 0; i < keysTag.size(); i++) {
|
|
||||||
ItemStack key = ItemStack.of(keysTag.getCompound(i));
|
|
||||||
if (!key.isEmpty()) {
|
|
||||||
this.collarKeys.add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESCAPE TRACKING METHODS
|
// ESCAPE TRACKING METHODS
|
||||||
@@ -1370,59 +1158,14 @@ public class EntityKidnapper
|
|||||||
return CollarHelper.isOwner(collar, player);
|
return CollarHelper.isOwner(collar, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Damage reduction multiplier against monsters (50% damage taken) */
|
|
||||||
private static final float MONSTER_DAMAGE_REDUCTION = 0.5f;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hurt(
|
public boolean hurt(
|
||||||
net.minecraft.world.damagesource.DamageSource source,
|
net.minecraft.world.damagesource.DamageSource source,
|
||||||
float amount
|
float amount
|
||||||
) {
|
) {
|
||||||
float finalAmount = amount;
|
float modified = aggressionSystem.processIncomingDamage(source, amount);
|
||||||
|
if (modified <= 0) return false;
|
||||||
// Track the attacker for fight back system
|
return super.hurt(source, modified);
|
||||||
if (source.getEntity() instanceof LivingEntity attacker) {
|
|
||||||
aggressionSystem.setLastAttacker(attacker);
|
|
||||||
|
|
||||||
// Punish prisoners who dare to attack us
|
|
||||||
if (
|
|
||||||
!this.level().isClientSide &&
|
|
||||||
attacker instanceof ServerPlayer player
|
|
||||||
) {
|
|
||||||
punishAttackingPrisoner(player);
|
|
||||||
|
|
||||||
// Expire PROTECTED status if player attacks a kidnapper
|
|
||||||
// Attacking a kidnapper voids your safe-exit window
|
|
||||||
if (this.level() instanceof ServerLevel serverLevel) {
|
|
||||||
com.tiedup.remake.prison.PrisonerManager pm =
|
|
||||||
com.tiedup.remake.prison.PrisonerManager.get(
|
|
||||||
serverLevel
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
pm.getState(player.getUUID()) ==
|
|
||||||
com.tiedup.remake.prison.PrisonerState.PROTECTED
|
|
||||||
) {
|
|
||||||
pm.expireProtection(
|
|
||||||
player.getUUID(),
|
|
||||||
serverLevel.getGameTime()
|
|
||||||
);
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[EntityKidnapper] Expired PROTECTED status for {} (attacked kidnapper)",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Camp kidnappers take reduced damage from monsters (trained fighters)
|
|
||||||
if (
|
|
||||||
this.getAssociatedStructure() != null &&
|
|
||||||
attacker instanceof net.minecraft.world.entity.monster.Monster
|
|
||||||
) {
|
|
||||||
finalAmount = amount * MONSTER_DAMAGE_REDUCTION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.hurt(source, finalAmount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1432,7 +1175,7 @@ public class EntityKidnapper
|
|||||||
* @param player The player who attacked
|
* @param player The player who attacked
|
||||||
* @return true if punishment was applied
|
* @return true if punishment was applied
|
||||||
*/
|
*/
|
||||||
protected boolean punishAttackingPrisoner(ServerPlayer player) {
|
public boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||||
return captiveManager.punishAttackingPrisoner(player);
|
return captiveManager.punishAttackingPrisoner(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1912,76 +1655,31 @@ public class EntityKidnapper
|
|||||||
com.tiedup.remake.util.MessageDispatcher.actionTo(this, player, action);
|
com.tiedup.remake.util.MessageDispatcher.actionTo(this, player, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dialogue cooldown timer (ticks remaining before next dialogue) */
|
|
||||||
private int dialogueCooldown = 0;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDialogueName() {
|
public String getDialogueName() {
|
||||||
return this.getNpcName();
|
return dialogue.getDialogueName();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpeakerType getSpeakerType() {
|
public SpeakerType getSpeakerType() {
|
||||||
return SpeakerType.KIDNAPPER;
|
return dialogue.getSpeakerType();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public PersonalityType getSpeakerPersonality() {
|
public PersonalityType getSpeakerPersonality() {
|
||||||
// Map kidnapper theme to a personality-like behavior
|
return dialogue.getSpeakerPersonality();
|
||||||
KidnapperTheme theme = this.getTheme();
|
|
||||||
if (theme == null) {
|
|
||||||
return PersonalityType.CALM;
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (theme) {
|
|
||||||
case ROPE, SHIBARI -> PersonalityType.CALM; // Traditional, methodical
|
|
||||||
case TAPE, LEATHER, CHAIN -> PersonalityType.FIERCE; // Rough, aggressive
|
|
||||||
case MEDICAL, ASYLUM -> PersonalityType.PROUD; // Clinical, professional
|
|
||||||
case LATEX, RIBBON -> PersonalityType.PLAYFUL; // Playful, teasing
|
|
||||||
case BEAM, WRAP -> PersonalityType.CURIOUS; // Experimental, modern
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSpeakerMood() {
|
public int getSpeakerMood() {
|
||||||
// Kidnappers mood is based on:
|
return dialogue.getSpeakerMood();
|
||||||
// - Having a captive (+20)
|
|
||||||
// - Current state (varies)
|
|
||||||
int mood = 50;
|
|
||||||
|
|
||||||
if (this.hasCaptives()) {
|
|
||||||
mood += 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State-based adjustment
|
|
||||||
KidnapperState state = this.getCurrentState();
|
|
||||||
if (state != null) {
|
|
||||||
mood += switch (state) {
|
|
||||||
case SELLING -> 10; // Excited about sale
|
|
||||||
case JOB_WATCH -> 5;
|
|
||||||
case GUARD -> 0;
|
|
||||||
case CAPTURE -> 15; // Hunting excitement
|
|
||||||
case PUNISH -> -10; // Stern
|
|
||||||
case PATROL, IDLE, HUNT -> 0; // Neutral for patrolling/hunting
|
|
||||||
case ALERT -> -5; // Concerned
|
|
||||||
case TRANSPORT -> 5;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.max(0, Math.min(100, mood));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getTargetRelation(Player player) {
|
public String getTargetRelation(Player player) {
|
||||||
// Check if this kidnapper is holding the player captive
|
return dialogue.getTargetRelation(player);
|
||||||
IRestrainable captive = this.getCaptive();
|
|
||||||
if (captive != null && captive.asLivingEntity() == player) {
|
|
||||||
return "captor";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1991,12 +1689,12 @@ public class EntityKidnapper
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getDialogueCooldown() {
|
public int getDialogueCooldown() {
|
||||||
return this.dialogueCooldown;
|
return dialogue.getDialogueCooldown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDialogueCooldown(int ticks) {
|
public void setDialogueCooldown(int ticks) {
|
||||||
this.dialogueCooldown = ticks;
|
dialogue.setDialogueCooldown(ticks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2004,9 +1702,7 @@ public class EntityKidnapper
|
|||||||
* Called from the main tick method.
|
* Called from the main tick method.
|
||||||
*/
|
*/
|
||||||
protected void tickDialogueCooldown() {
|
protected void tickDialogueCooldown() {
|
||||||
if (this.dialogueCooldown > 0) {
|
dialogue.tickDialogueCooldown();
|
||||||
this.dialogueCooldown--;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// STOLEN ITEMS (Thief Goal)
|
// STOLEN ITEMS (Thief Goal)
|
||||||
@@ -2016,9 +1712,7 @@ public class EntityKidnapper
|
|||||||
* Called by KidnapperThiefGoal when stealing from a player.
|
* Called by KidnapperThiefGoal when stealing from a player.
|
||||||
*/
|
*/
|
||||||
public void addStolenItem(ItemStack stack) {
|
public void addStolenItem(ItemStack stack) {
|
||||||
if (!stack.isEmpty()) {
|
lootManager.addStolenItem(stack);
|
||||||
this.stolenItems.add(stack.copy());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// COLLAR KEYS (Capture Goal)
|
// COLLAR KEYS (Capture Goal)
|
||||||
@@ -2028,8 +1722,6 @@ public class EntityKidnapper
|
|||||||
* Called by KidnapperCaptureGoal when collaring a captive.
|
* Called by KidnapperCaptureGoal when collaring a captive.
|
||||||
*/
|
*/
|
||||||
public void addCollarKey(ItemStack keyStack) {
|
public void addCollarKey(ItemStack keyStack) {
|
||||||
if (!keyStack.isEmpty()) {
|
lootManager.addCollarKey(keyStack);
|
||||||
this.collarKeys.add(keyStack.copy());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import com.tiedup.remake.entities.skins.LaborGuardSkinManager;
|
|||||||
import com.tiedup.remake.labor.LaborTask;
|
import com.tiedup.remake.labor.LaborTask;
|
||||||
import com.tiedup.remake.prison.LaborRecord;
|
import com.tiedup.remake.prison.LaborRecord;
|
||||||
import com.tiedup.remake.prison.PrisonerManager;
|
import com.tiedup.remake.prison.PrisonerManager;
|
||||||
import com.tiedup.remake.prison.service.PrisonerService;
|
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||||
import com.tiedup.remake.state.IRestrainable;
|
import com.tiedup.remake.state.IRestrainable;
|
||||||
import com.tiedup.remake.util.KidnappedHelper;
|
import com.tiedup.remake.util.KidnappedHelper;
|
||||||
import com.tiedup.remake.util.MessageDispatcher;
|
import com.tiedup.remake.util.MessageDispatcher;
|
||||||
@@ -520,7 +520,7 @@ public class EntityLaborGuard extends EntityDamsel {
|
|||||||
labor.setGuardId(null);
|
labor.setGuardId(null);
|
||||||
|
|
||||||
// Trigger escape
|
// Trigger escape
|
||||||
PrisonerService.get().escape(level, prisonerUUID, reason);
|
EscapeMonitorService.get().escape(level, prisonerUUID, reason);
|
||||||
|
|
||||||
// Notify prisoner
|
// Notify prisoner
|
||||||
ServerPlayer prisoner = level
|
ServerPlayer prisoner = level
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.labor.LaborTask;
|
|||||||
import com.tiedup.remake.prison.LaborRecord;
|
import com.tiedup.remake.prison.LaborRecord;
|
||||||
import com.tiedup.remake.prison.PrisonerManager;
|
import com.tiedup.remake.prison.PrisonerManager;
|
||||||
import com.tiedup.remake.prison.PrisonerRecord;
|
import com.tiedup.remake.prison.PrisonerRecord;
|
||||||
import com.tiedup.remake.prison.service.PrisonerService;
|
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||||
import com.tiedup.remake.state.IRestrainable;
|
import com.tiedup.remake.state.IRestrainable;
|
||||||
import com.tiedup.remake.util.KidnappedHelper;
|
import com.tiedup.remake.util.KidnappedHelper;
|
||||||
import com.tiedup.remake.util.MessageDispatcher;
|
import com.tiedup.remake.util.MessageDispatcher;
|
||||||
@@ -562,7 +562,7 @@ public class GuardMonitorGoal extends Goal {
|
|||||||
LaborRecord labor = manager.getLaborRecord(prisoner.getUUID());
|
LaborRecord labor = manager.getLaborRecord(prisoner.getUUID());
|
||||||
labor.setGuardId(null);
|
labor.setGuardId(null);
|
||||||
|
|
||||||
PrisonerService.get().escape(level, prisoner.getUUID(), reason);
|
EscapeMonitorService.get().escape(level, prisoner.getUUID(), reason);
|
||||||
|
|
||||||
// Discard the guard entity itself
|
// Discard the guard entity itself
|
||||||
guard.discard();
|
guard.discard();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.core.TiedUpMod;
|
|||||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||||
import com.tiedup.remake.entities.EntityKidnapper;
|
import com.tiedup.remake.entities.EntityKidnapper;
|
||||||
import com.tiedup.remake.entities.EntityKidnapper.CaptivePriority;
|
import com.tiedup.remake.entities.kidnapper.CaptivePriority;
|
||||||
import com.tiedup.remake.entities.ai.StuckDetector;
|
import com.tiedup.remake.entities.ai.StuckDetector;
|
||||||
import com.tiedup.remake.entities.ai.WaypointNavigator;
|
import com.tiedup.remake.entities.ai.WaypointNavigator;
|
||||||
import com.tiedup.remake.v2.bondage.CollarHelper;
|
import com.tiedup.remake.v2.bondage.CollarHelper;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory;
|
|||||||
import com.tiedup.remake.core.TiedUpMod;
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||||
import com.tiedup.remake.entities.EntityKidnapper;
|
import com.tiedup.remake.entities.EntityKidnapper;
|
||||||
import com.tiedup.remake.prison.service.PrisonerService;
|
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||||
import com.tiedup.remake.state.IBondageState;
|
import com.tiedup.remake.state.IBondageState;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -574,7 +574,7 @@ public class KidnapperDecideNextActionGoal extends Goal {
|
|||||||
captive.free(false); // false = don't drop leash (we take it)
|
captive.free(false); // false = don't drop leash (we take it)
|
||||||
|
|
||||||
// 3b. Clean up PrisonerManager state (CAPTURED -> FREE)
|
// 3b. Clean up PrisonerManager state (CAPTURED -> FREE)
|
||||||
PrisonerService.get().escape(
|
EscapeMonitorService.get().escape(
|
||||||
(ServerLevel) player.level(),
|
(ServerLevel) player.level(),
|
||||||
player.getUUID(),
|
player.getUUID(),
|
||||||
"theft_release"
|
"theft_release"
|
||||||
|
|||||||
@@ -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;
|
package com.tiedup.remake.entities.kidnapper.components;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -26,4 +28,18 @@ public interface IAggressionHost {
|
|||||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
|
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
|
||||||
int radius
|
int radius
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the associated camp structure UUID.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
UUID getAssociatedStructure();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Punish a prisoner who attacks this kidnapper.
|
||||||
|
*
|
||||||
|
* @param player The player who attacked
|
||||||
|
* @return true if punishment was applied
|
||||||
|
*/
|
||||||
|
boolean punishAttackingPrisoner(ServerPlayer player);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.damagesource.DamageSource;
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -232,6 +235,69 @@ public class KidnapperAggressionSystem {
|
|||||||
this.robbedImmunity.clear();
|
this.robbedImmunity.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DAMAGE PROCESSING
|
||||||
|
|
||||||
|
/** Damage reduction multiplier against monsters (50% damage taken) */
|
||||||
|
private static final float MONSTER_DAMAGE_REDUCTION = 0.5f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process incoming damage: track attacker, punish prisoners, expire protection,
|
||||||
|
* apply monster damage reduction for camp kidnappers.
|
||||||
|
*
|
||||||
|
* @param source The damage source
|
||||||
|
* @param amount The raw damage amount
|
||||||
|
* @return The modified damage amount (0 or less means cancel the hurt)
|
||||||
|
*/
|
||||||
|
public float processIncomingDamage(DamageSource source, float amount) {
|
||||||
|
float finalAmount = amount;
|
||||||
|
|
||||||
|
// Track the attacker for fight back system
|
||||||
|
if (source.getEntity() instanceof LivingEntity attacker) {
|
||||||
|
this.setLastAttacker(attacker);
|
||||||
|
|
||||||
|
// Punish prisoners who dare to attack us
|
||||||
|
if (
|
||||||
|
host.level() != null &&
|
||||||
|
!host.level().isClientSide &&
|
||||||
|
attacker instanceof ServerPlayer player
|
||||||
|
) {
|
||||||
|
host.punishAttackingPrisoner(player);
|
||||||
|
|
||||||
|
// Expire PROTECTED status if player attacks a kidnapper
|
||||||
|
// Attacking a kidnapper voids your safe-exit window
|
||||||
|
if (host.level() instanceof ServerLevel serverLevel) {
|
||||||
|
com.tiedup.remake.prison.PrisonerManager pm =
|
||||||
|
com.tiedup.remake.prison.PrisonerManager.get(
|
||||||
|
serverLevel
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
pm.getState(player.getUUID()) ==
|
||||||
|
com.tiedup.remake.prison.PrisonerState.PROTECTED
|
||||||
|
) {
|
||||||
|
pm.expireProtection(
|
||||||
|
player.getUUID(),
|
||||||
|
serverLevel.getGameTime()
|
||||||
|
);
|
||||||
|
TiedUpMod.LOGGER.debug(
|
||||||
|
"[KidnapperAggressionSystem] Expired PROTECTED status for {} (attacked kidnapper)",
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camp kidnappers take reduced damage from monsters (trained fighters)
|
||||||
|
if (
|
||||||
|
host.getAssociatedStructure() != null &&
|
||||||
|
attacker instanceof net.minecraft.world.entity.monster.Monster
|
||||||
|
) {
|
||||||
|
finalAmount = amount * MONSTER_DAMAGE_REDUCTION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
// HELPER METHODS
|
// HELPER METHODS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ public class KidnapperCaptiveManager {
|
|||||||
if (record != null && record.isImprisoned()) {
|
if (record != null && record.isImprisoned()) {
|
||||||
// Clear captivity state - prisoner escaped
|
// Clear captivity state - prisoner escaped
|
||||||
// Use centralized escape service for complete cleanup
|
// Use centralized escape service for complete cleanup
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
serverLevel,
|
serverLevel,
|
||||||
captiveUUID,
|
captiveUUID,
|
||||||
"kidnapper tied up"
|
"kidnapper tied up"
|
||||||
@@ -545,7 +545,7 @@ public class KidnapperCaptiveManager {
|
|||||||
if (record != null && record.isImprisoned()) {
|
if (record != null && record.isImprisoned()) {
|
||||||
// Clear captivity state - captor died
|
// Clear captivity state - captor died
|
||||||
// Use centralized escape service for complete cleanup
|
// Use centralized escape service for complete cleanup
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
serverLevel,
|
serverLevel,
|
||||||
captiveUUID,
|
captiveUUID,
|
||||||
"kidnapper died"
|
"kidnapper died"
|
||||||
@@ -670,7 +670,7 @@ public class KidnapperCaptiveManager {
|
|||||||
if (record != null && record.isImprisoned()) {
|
if (record != null && record.isImprisoned()) {
|
||||||
// Clear captivity state - prisoner freed
|
// Clear captivity state - prisoner freed
|
||||||
// Use centralized escape service for complete cleanup
|
// Use centralized escape service for complete cleanup
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
serverLevel,
|
serverLevel,
|
||||||
captiveUUID,
|
captiveUUID,
|
||||||
"abandoned by kidnapper"
|
"abandoned by kidnapper"
|
||||||
|
|||||||
@@ -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.dialogue.EntityDialogueManager;
|
||||||
import com.tiedup.remake.entities.EntityKidnapper;
|
import com.tiedup.remake.entities.EntityKidnapper;
|
||||||
import com.tiedup.remake.entities.kidnapper.components.IAggressionHost;
|
import com.tiedup.remake.entities.kidnapper.components.IAggressionHost;
|
||||||
|
import java.util.UUID;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.level.Level;
|
import net.minecraft.world.level.Level;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@@ -35,4 +37,15 @@ public class AggressionHost implements IAggressionHost {
|
|||||||
) {
|
) {
|
||||||
entity.talkToPlayersInRadius(category, radius);
|
entity.talkToPlayersInRadius(category, radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public UUID getAssociatedStructure() {
|
||||||
|
return entity.getAssociatedStructure();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean punishAttackingPrisoner(ServerPlayer player) {
|
||||||
|
return entity.punishAttackingPrisoner(player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
import com.tiedup.remake.entities.ModEntities;
|
||||||
// Prison system v2
|
// Prison system v2
|
||||||
import com.tiedup.remake.prison.PrisonerManager;
|
import com.tiedup.remake.prison.PrisonerManager;
|
||||||
import com.tiedup.remake.prison.service.PrisonerService;
|
import com.tiedup.remake.prison.service.EscapeMonitorService;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
@@ -75,7 +75,7 @@ public class CampManagementHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prison system v2 - tick escape service (handles escape detection)
|
// Prison system v2 - tick escape service (handles escape detection)
|
||||||
PrisonerService.get().tick(level.getServer(), currentTime);
|
EscapeMonitorService.get().tick(level.getServer(), currentTime);
|
||||||
|
|
||||||
// Prison system v2 - tick protection expiry
|
// Prison system v2 - tick protection expiry
|
||||||
PrisonerManager.get(level).tickProtectionExpiry(currentTime);
|
PrisonerManager.get(level).tickProtectionExpiry(currentTime);
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ public class PlayerStateEventHandler {
|
|||||||
// Transition to FREE state via escape (clears all data)
|
// Transition to FREE state via escape (clears all data)
|
||||||
long currentTime = player.serverLevel().getGameTime();
|
long currentTime = player.serverLevel().getGameTime();
|
||||||
// Use centralized escape service for complete cleanup
|
// Use centralized escape service for complete cleanup
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
player.serverLevel(),
|
player.serverLevel(),
|
||||||
player.getUUID(),
|
player.getUUID(),
|
||||||
"death"
|
"death"
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ public class PacketBuyCaptive {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Release via PrisonerService (state transition + cell cleanup + restraint removal)
|
// Release via PrisonerService (state transition + cell cleanup + restraint removal)
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().release(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().release(
|
||||||
level,
|
level,
|
||||||
captiveId,
|
captiveId,
|
||||||
6000L // 5 minutes grace period
|
6000L // 5 minutes grace period
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ public class PrisonerManager extends SavedData {
|
|||||||
return laborRecords.computeIfAbsent(playerId, id -> new LaborRecord());
|
return laborRecords.computeIfAbsent(playerId, id -> new LaborRecord());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get labor record only if one exists. Does not create ghost entries.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public LaborRecord getLaborRecordIfExists(UUID playerId) {
|
||||||
|
return laborRecords.get(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the labor record for a player.
|
* Set the labor record for a player.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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.AbstractTiedUpNpc;
|
||||||
import com.tiedup.remake.entities.EntityDamsel;
|
import com.tiedup.remake.entities.EntityDamsel;
|
||||||
import com.tiedup.remake.personality.PersonalityState;
|
import com.tiedup.remake.personality.PersonalityState;
|
||||||
import com.tiedup.remake.prison.LaborRecord;
|
|
||||||
import com.tiedup.remake.prison.PrisonerManager;
|
import com.tiedup.remake.prison.PrisonerManager;
|
||||||
import com.tiedup.remake.prison.PrisonerRecord;
|
import com.tiedup.remake.prison.PrisonerRecord;
|
||||||
import com.tiedup.remake.prison.PrisonerState;
|
import com.tiedup.remake.prison.PrisonerState;
|
||||||
import com.tiedup.remake.prison.service.BondageService;
|
|
||||||
import com.tiedup.remake.state.CollarRegistry;
|
|
||||||
import com.tiedup.remake.state.IBondageState;
|
import com.tiedup.remake.state.IBondageState;
|
||||||
import com.tiedup.remake.state.ICaptor;
|
import com.tiedup.remake.state.ICaptor;
|
||||||
import com.tiedup.remake.state.PlayerBindState;
|
|
||||||
import com.tiedup.remake.util.KidnappedHelper;
|
import com.tiedup.remake.util.KidnappedHelper;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import net.minecraft.ChatFormatting;
|
|
||||||
import net.minecraft.core.BlockPos;
|
|
||||||
import net.minecraft.nbt.CompoundTag;
|
|
||||||
import net.minecraft.network.chat.Component;
|
|
||||||
import net.minecraft.server.MinecraftServer;
|
|
||||||
import net.minecraft.server.level.ServerLevel;
|
import net.minecraft.server.level.ServerLevel;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralized prisoner lifecycle service.
|
* Prisoner lifecycle service.
|
||||||
*
|
*
|
||||||
* Manages ALL prisoner state transitions atomically:
|
* Manages prisoner state transitions atomically:
|
||||||
* - PrisonerManager (SavedData state)
|
* - PrisonerManager (SavedData state)
|
||||||
* - CellRegistryV2 (cell assignment)
|
* - CellRegistryV2 (cell assignment)
|
||||||
* - Leash/captor refs (IBondageState / ICaptor)
|
* - Leash/captor refs (IBondageState / ICaptor)
|
||||||
* - Escape detection (tick)
|
|
||||||
*
|
*
|
||||||
* Replaces EscapeService and unifies scattered capture/imprison/extract/return/transfer logic.
|
* Handles: capture, imprison, extractFromCell, returnToCell, transferCaptive.
|
||||||
|
* Escape detection and release are handled by {@link EscapeMonitorService}.
|
||||||
*
|
*
|
||||||
* AI goals manage: Navigation, Teleport, Equipment, Dialogue.
|
* AI goals manage: Navigation, Teleport, Equipment, Dialogue.
|
||||||
* PrisonerService manages: State transitions + leash coordination.
|
* PrisonerService manages: State transitions + leash coordination.
|
||||||
@@ -54,29 +42,6 @@ public class PrisonerService {
|
|||||||
|
|
||||||
private PrisonerService() {}
|
private PrisonerService() {}
|
||||||
|
|
||||||
// ==================== CONSTANTS (from EscapeService) ====================
|
|
||||||
|
|
||||||
/** Maximum distance from cell for IMPRISONED prisoners (blocks) */
|
|
||||||
public static final double CELL_ESCAPE_DISTANCE = 20.0;
|
|
||||||
|
|
||||||
/** Maximum distance from camp for WORKING prisoners (blocks) */
|
|
||||||
public static final double WORK_ESCAPE_DISTANCE = 100.0;
|
|
||||||
|
|
||||||
/** Maximum time offline before escape (30 minutes in ticks) */
|
|
||||||
public static final long OFFLINE_TIMEOUT_TICKS = 30 * 60 * 20L;
|
|
||||||
|
|
||||||
/** Maximum time in WORKING state before forced return (10 minutes in ticks) */
|
|
||||||
public static final long WORK_TIMEOUT_TICKS = 10 * 60 * 20L;
|
|
||||||
|
|
||||||
/** Maximum time in RETURNING phase before escape (5 minutes in ticks) */
|
|
||||||
public static final long RETURN_TIMEOUT_TICKS = 5 * 60 * 20L;
|
|
||||||
|
|
||||||
/** Maximum time in PENDING_RETURN phase before forced return (2.5 minutes in ticks) */
|
|
||||||
public static final long PENDING_RETURN_TIMEOUT_TICKS = 3000;
|
|
||||||
|
|
||||||
/** Check interval in ticks (every 5 seconds) */
|
|
||||||
public static final int CHECK_INTERVAL_TICKS = 100;
|
|
||||||
|
|
||||||
// ==================== CAPTURE ====================
|
// ==================== CAPTURE ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -506,552 +471,4 @@ public class PrisonerService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== ESCAPE (from EscapeService) ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CENTRAL ESCAPE METHOD - All escapes must go through here.
|
|
||||||
*
|
|
||||||
* Handles complete cleanup:
|
|
||||||
* - State transition (PrisonerManager)
|
|
||||||
* - Cell registry cleanup
|
|
||||||
* - Guard despawn
|
|
||||||
* - Restraints removal (if online)
|
|
||||||
* - Inventory NOT restored (stays in camp chest as punishment)
|
|
||||||
*
|
|
||||||
* @param level The server level
|
|
||||||
* @param playerId Player UUID
|
|
||||||
* @param reason Reason for escape (for logging)
|
|
||||||
* @return true if escape was processed successfully
|
|
||||||
*/
|
|
||||||
public boolean escape(ServerLevel level, UUID playerId, String reason) {
|
|
||||||
long currentTime = level.getGameTime();
|
|
||||||
PrisonerManager manager = PrisonerManager.get(level);
|
|
||||||
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
|
|
||||||
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Processing escape for {} - reason: {}",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
reason
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 1: Save guard ID BEFORE state transition (manager.escape() removes the LaborRecord)
|
|
||||||
LaborRecord laborBeforeEscape = manager.getLaborRecord(playerId);
|
|
||||||
UUID guardId = laborBeforeEscape.getGuardId();
|
|
||||||
|
|
||||||
// Step 2: Transition prisoner state to FREE
|
|
||||||
boolean stateChanged = manager.escape(playerId, currentTime, reason);
|
|
||||||
if (!stateChanged) {
|
|
||||||
TiedUpMod.LOGGER.warn(
|
|
||||||
"[PrisonerService] Failed to change state for {} - invalid transition",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Cleanup CellRegistryV2 - remove from all cells
|
|
||||||
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
|
|
||||||
if (cellsCleared > 0) {
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[PrisonerService] Cleared {} from {} cells",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
cellsCleared
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Cleanup guard using saved reference (LaborRecord was removed by manager.escape())
|
|
||||||
if (guardId != null) {
|
|
||||||
net.minecraft.world.entity.Entity guardEntity = level.getEntity(
|
|
||||||
guardId
|
|
||||||
);
|
|
||||||
if (guardEntity != null) {
|
|
||||||
guardEntity.discard();
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[PrisonerService] Despawned guard {} during escape",
|
|
||||||
guardId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5: Free from restraints (if player is online)
|
|
||||||
// NOTE: Inventory is NOT restored on escape - items remain in camp chest as punishment
|
|
||||||
ServerPlayer player = level
|
|
||||||
.getServer()
|
|
||||||
.getPlayerList()
|
|
||||||
.getPlayer(playerId);
|
|
||||||
if (player != null) {
|
|
||||||
IBondageState cap = KidnappedHelper.getKidnappedState(player);
|
|
||||||
if (cap != null) {
|
|
||||||
cap.free(false);
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Freed {} from restraints (inventory remains in camp chest)",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[PrisonerService] Player {} offline - restraints cleanup deferred",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Escape complete for {} - items stay in camp chest",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== RELEASE (from EscapeService) ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CENTRAL RELEASE METHOD - All legitimate releases must go through here.
|
|
||||||
*
|
|
||||||
* Similar to escape() but transitions to PROTECTED state with grace period.
|
|
||||||
* Handles complete cleanup:
|
|
||||||
* - State transition to PROTECTED (PrisonerManager)
|
|
||||||
* - Cell registry cleanup
|
|
||||||
* - Inventory restoration
|
|
||||||
* - Restraints removal (if online)
|
|
||||||
*
|
|
||||||
* @param level The server level
|
|
||||||
* @param playerId Player UUID
|
|
||||||
* @param gracePeriodTicks Grace period before returning to FREE (0 = instant FREE)
|
|
||||||
* @return true if release was processed successfully
|
|
||||||
*/
|
|
||||||
public boolean release(
|
|
||||||
ServerLevel level,
|
|
||||||
UUID playerId,
|
|
||||||
long gracePeriodTicks
|
|
||||||
) {
|
|
||||||
long currentTime = level.getGameTime();
|
|
||||||
PrisonerManager manager = PrisonerManager.get(level);
|
|
||||||
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
|
|
||||||
com.tiedup.remake.cells.ConfiscatedInventoryRegistry inventoryRegistry =
|
|
||||||
com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level);
|
|
||||||
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Processing release for {} - grace period: {} ticks",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
gracePeriodTicks
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 1: Transition prisoner state to PROTECTED (or FREE if gracePeriod = 0)
|
|
||||||
boolean stateChanged = manager.release(
|
|
||||||
playerId,
|
|
||||||
currentTime,
|
|
||||||
gracePeriodTicks
|
|
||||||
);
|
|
||||||
if (!stateChanged) {
|
|
||||||
TiedUpMod.LOGGER.warn(
|
|
||||||
"[PrisonerService] Failed to change state for {} - invalid transition",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Cleanup CellRegistryV2 - remove from all cells
|
|
||||||
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
|
|
||||||
if (cellsCleared > 0) {
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[PrisonerService] Cleared {} from {} cells",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
cellsCleared
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Restore confiscated inventory (if player is online)
|
|
||||||
ServerPlayer player = level
|
|
||||||
.getServer()
|
|
||||||
.getPlayerList()
|
|
||||||
.getPlayer(playerId);
|
|
||||||
if (player != null) {
|
|
||||||
// Restore inventory
|
|
||||||
if (inventoryRegistry.hasConfiscatedInventory(playerId)) {
|
|
||||||
boolean restored = inventoryRegistry.restoreInventory(player);
|
|
||||||
if (restored) {
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Restored confiscated inventory for {}",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free from restraints
|
|
||||||
IBondageState cap = KidnappedHelper.getKidnappedState(player);
|
|
||||||
if (cap != null) {
|
|
||||||
cap.free(false);
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[PrisonerService] Freed {} from restraints",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TiedUpMod.LOGGER.debug(
|
|
||||||
"[PrisonerService] Player {} offline - inventory cleanup deferred",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Release complete for {} - full cleanup done",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== TICK (escape detection, from EscapeService) ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tick escape detection.
|
|
||||||
* Called from CampManagementHandler.
|
|
||||||
*
|
|
||||||
* @param server The server
|
|
||||||
* @param currentTime Current game time
|
|
||||||
*/
|
|
||||||
public void tick(MinecraftServer server, long currentTime) {
|
|
||||||
if (currentTime % CHECK_INTERVAL_TICKS != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerLevel level = server.overworld();
|
|
||||||
PrisonerManager manager = PrisonerManager.get(level);
|
|
||||||
CellRegistryV2 cells = CellRegistryV2.get(level);
|
|
||||||
CollarRegistry collars = CollarRegistry.get(level);
|
|
||||||
|
|
||||||
List<EscapeCandidate> escapees = new ArrayList<>();
|
|
||||||
|
|
||||||
for (UUID playerId : manager.getAllPrisonerIds()) {
|
|
||||||
PrisonerRecord record = manager.getRecord(playerId);
|
|
||||||
PrisonerState state = record.getState();
|
|
||||||
|
|
||||||
if (!state.isCaptive()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
|
|
||||||
|
|
||||||
// === OFFLINE CHECK ===
|
|
||||||
if (player == null) {
|
|
||||||
long timeInState = record.getTimeInState(currentTime);
|
|
||||||
if (timeInState > OFFLINE_TIMEOUT_TICKS) {
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(playerId, "offline timeout")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// === COLLAR CHECK ===
|
|
||||||
if (state != PrisonerState.CAPTURED) {
|
|
||||||
boolean hasCollarOwners = collars.hasOwners(playerId);
|
|
||||||
if (!hasCollarOwners) {
|
|
||||||
IBondageState cap = KidnappedHelper.getKidnappedState(
|
|
||||||
player
|
|
||||||
);
|
|
||||||
ItemStack collar =
|
|
||||||
cap != null
|
|
||||||
? cap.getEquipment(BodyRegionV2.NECK)
|
|
||||||
: ItemStack.EMPTY;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!collar.isEmpty() &&
|
|
||||||
com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)
|
|
||||||
) {
|
|
||||||
List<UUID> nbtOwners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
|
|
||||||
if (!nbtOwners.isEmpty()) {
|
|
||||||
for (UUID ownerUUID : nbtOwners) {
|
|
||||||
collars.registerCollar(playerId, ownerUUID);
|
|
||||||
}
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Re-synced collar registry for {} - found {} owners in NBT",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
nbtOwners.size()
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TiedUpMod.LOGGER.warn(
|
|
||||||
"[PrisonerService] Collar check failed for {} (state={}): collarEquipped={}, collarItem={}",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
state,
|
|
||||||
!collar.isEmpty(),
|
|
||||||
collar.isEmpty() ? "none" : collar.getItem().toString()
|
|
||||||
);
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(playerId, "collar removed")
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === STATE-SPECIFIC CHECKS ===
|
|
||||||
switch (state) {
|
|
||||||
case IMPRISONED -> {
|
|
||||||
UUID cellId = record.getCellId();
|
|
||||||
if (cellId != null) {
|
|
||||||
CellDataV2 cell = cells.getCell(cellId);
|
|
||||||
if (cell == null) {
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(
|
|
||||||
playerId,
|
|
||||||
"cell no longer exists"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
TiedUpMod.LOGGER.warn(
|
|
||||||
"[PrisonerService] Prisoner {} has orphaned cellId {} - triggering escape",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
cellId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
double distance = Math.sqrt(
|
|
||||||
player
|
|
||||||
.blockPosition()
|
|
||||||
.distSqr(cell.getCorePos())
|
|
||||||
);
|
|
||||||
if (distance > CELL_ESCAPE_DISTANCE) {
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(
|
|
||||||
playerId,
|
|
||||||
String.format(
|
|
||||||
"too far from cell (%.1f blocks)",
|
|
||||||
distance
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case WORKING -> {
|
|
||||||
LaborRecord labor = manager.getLaborRecord(playerId);
|
|
||||||
|
|
||||||
long timeInPhase = labor.getTimeInPhase(currentTime);
|
|
||||||
if (
|
|
||||||
labor.getPhase() == LaborRecord.WorkPhase.WORKING &&
|
|
||||||
timeInPhase > WORK_TIMEOUT_TICKS
|
|
||||||
) {
|
|
||||||
labor.failTask(currentTime);
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] {} work timeout - forcing return",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
labor.getPhase() == LaborRecord.WorkPhase.RETURNING &&
|
|
||||||
timeInPhase > RETURN_TIMEOUT_TICKS
|
|
||||||
) {
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(playerId, "return timeout")
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PENDING_RETURN timeout: maid failed to pick up prisoner — force return to cell
|
|
||||||
if (
|
|
||||||
labor.getPhase() ==
|
|
||||||
LaborRecord.WorkPhase.PENDING_RETURN &&
|
|
||||||
timeInPhase > PENDING_RETURN_TIMEOUT_TICKS
|
|
||||||
) {
|
|
||||||
PrisonerRecord rec = manager.getRecord(playerId);
|
|
||||||
UUID cellId = rec.getCellId();
|
|
||||||
if (cellId != null) {
|
|
||||||
CellDataV2 cell = cells.getCell(cellId);
|
|
||||||
if (cell != null) {
|
|
||||||
BlockPos teleportPos =
|
|
||||||
cell.getSpawnPoint() != null
|
|
||||||
? cell.getSpawnPoint()
|
|
||||||
: cell.getCorePos().above();
|
|
||||||
player.teleportTo(
|
|
||||||
teleportPos.getX() + 0.5,
|
|
||||||
teleportPos.getY(),
|
|
||||||
teleportPos.getZ() + 0.5
|
|
||||||
);
|
|
||||||
IBondageState cap =
|
|
||||||
KidnappedHelper.getKidnappedState(player);
|
|
||||||
if (cap != null) {
|
|
||||||
CompoundTag snapshot =
|
|
||||||
labor.getBondageSnapshot();
|
|
||||||
if (snapshot != null) {
|
|
||||||
BondageService.get().restoreSnapshot(
|
|
||||||
cap,
|
|
||||||
snapshot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
returnToCell(level, player, null, cell);
|
|
||||||
labor.startRest(currentTime);
|
|
||||||
if (labor.getGuardId() != null) {
|
|
||||||
net.minecraft.world.entity.Entity guardEntity =
|
|
||||||
level.getEntity(labor.getGuardId());
|
|
||||||
if (
|
|
||||||
guardEntity != null
|
|
||||||
) guardEntity.discard();
|
|
||||||
}
|
|
||||||
player.sendSystemMessage(
|
|
||||||
Component.translatable(
|
|
||||||
"msg.tiedup.prison.returned_to_cell"
|
|
||||||
).withStyle(ChatFormatting.GRAY)
|
|
||||||
);
|
|
||||||
TiedUpMod.LOGGER.info(
|
|
||||||
"[PrisonerService] Force-returned {} to cell (PENDING_RETURN timeout)",
|
|
||||||
playerId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cell gone → trigger escape
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(
|
|
||||||
playerId,
|
|
||||||
"pending_return timeout, cell gone"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labor.getGuardId() != null) {
|
|
||||||
net.minecraft.world.entity.Entity guardEntity =
|
|
||||||
level.getEntity(labor.getGuardId());
|
|
||||||
if (guardEntity != null && guardEntity.isAlive()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UUID campId = record.getCampId();
|
|
||||||
if (campId != null) {
|
|
||||||
List<CellDataV2> campCells = cells.getCellsByCamp(
|
|
||||||
campId
|
|
||||||
);
|
|
||||||
if (campCells.isEmpty()) {
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(
|
|
||||||
playerId,
|
|
||||||
"camp no longer exists"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
TiedUpMod.LOGGER.warn(
|
|
||||||
"[PrisonerService] Worker {} has orphaned campId {} with no cells - triggering escape",
|
|
||||||
playerId.toString().substring(0, 8),
|
|
||||||
campId.toString().substring(0, 8)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
BlockPos campCenter = campCells.get(0).getCorePos();
|
|
||||||
double distance = Math.sqrt(
|
|
||||||
player.blockPosition().distSqr(campCenter)
|
|
||||||
);
|
|
||||||
if (distance > WORK_ESCAPE_DISTANCE) {
|
|
||||||
escapees.add(
|
|
||||||
new EscapeCandidate(
|
|
||||||
playerId,
|
|
||||||
String.format(
|
|
||||||
"too far from camp (%.1f blocks)",
|
|
||||||
distance
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case CAPTURED -> {
|
|
||||||
// During transport - handled by kidnapper goals
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
// Other states don't need distance checks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (EscapeCandidate candidate : escapees) {
|
|
||||||
escape(level, candidate.playerId, candidate.reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== VALIDATION ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a player should be considered escaped.
|
|
||||||
* Called on-demand (e.g., when player moves).
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public String checkEscape(ServerLevel level, ServerPlayer player) {
|
|
||||||
PrisonerManager manager = PrisonerManager.get(level);
|
|
||||||
CellRegistryV2 cells = CellRegistryV2.get(level);
|
|
||||||
CollarRegistry collars = CollarRegistry.get(level);
|
|
||||||
|
|
||||||
UUID playerId = player.getUUID();
|
|
||||||
PrisonerRecord record = manager.getRecord(playerId);
|
|
||||||
PrisonerState state = record.getState();
|
|
||||||
|
|
||||||
if (!state.isCaptive()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!collars.hasOwners(playerId)) {
|
|
||||||
return "collar removed";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (state) {
|
|
||||||
case IMPRISONED -> {
|
|
||||||
UUID cellId = record.getCellId();
|
|
||||||
if (cellId != null) {
|
|
||||||
CellDataV2 cell = cells.getCell(cellId);
|
|
||||||
if (cell != null) {
|
|
||||||
double distance = Math.sqrt(
|
|
||||||
player.blockPosition().distSqr(cell.getCorePos())
|
|
||||||
);
|
|
||||||
if (distance > CELL_ESCAPE_DISTANCE) {
|
|
||||||
return String.format(
|
|
||||||
"too far from cell (%.1f blocks)",
|
|
||||||
distance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case WORKING -> {
|
|
||||||
UUID campId = record.getCampId();
|
|
||||||
if (campId != null) {
|
|
||||||
List<CellDataV2> campCells = cells.getCellsByCamp(campId);
|
|
||||||
if (!campCells.isEmpty()) {
|
|
||||||
BlockPos campCenter = campCells.get(0).getCorePos();
|
|
||||||
double distance = Math.sqrt(
|
|
||||||
player.blockPosition().distSqr(campCenter)
|
|
||||||
);
|
|
||||||
if (distance > WORK_ESCAPE_DISTANCE) {
|
|
||||||
return String.format(
|
|
||||||
"too far from camp (%.1f blocks)",
|
|
||||||
distance
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default -> {
|
|
||||||
// Other states don't have distance limits
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle player movement event.
|
|
||||||
* Called from event handler to check distance-based escapes.
|
|
||||||
*/
|
|
||||||
public void onPlayerMove(ServerPlayer player, ServerLevel level) {
|
|
||||||
String escapeReason = checkEscape(level, player);
|
|
||||||
if (escapeReason != null) {
|
|
||||||
escape(level, player.getUUID(), escapeReason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== HELPER ====================
|
|
||||||
|
|
||||||
private record EscapeCandidate(UUID playerId, String reason) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ public class PlayerLifecycle {
|
|||||||
state == com.tiedup.remake.prison.PrisonerState.WORKING
|
state == com.tiedup.remake.prison.PrisonerState.WORKING
|
||||||
) {
|
) {
|
||||||
// Use centralized escape service for complete cleanup
|
// Use centralized escape service for complete cleanup
|
||||||
com.tiedup.remake.prison.service.PrisonerService.get().escape(
|
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
|
||||||
serverLevel,
|
serverLevel,
|
||||||
playerId,
|
playerId,
|
||||||
"player_death"
|
"player_death"
|
||||||
|
|||||||
Reference in New Issue
Block a user