From f4aa5ffdc54ba29547c2160d4600b61bb32367b7 Mon Sep 17 00:00:00 2001 From: NotEvil Date: Thu, 16 Apr 2026 14:08:52 +0200 Subject: [PATCH] split PrisonerService + decompose EntityKidnapper PrisonerService 1057L -> 474L lifecycle + 616L EscapeMonitorService EntityKidnapper 2035L -> 1727L via LootManager, Dialogue, CaptivePriority extraction --- .../remake/cells/CampLifecycleManager.java | 2 +- .../tiedup/remake/cells/CellRegistryV2.java | 2 +- .../tiedup/remake/entities/EntityDamsel.java | 2 +- .../remake/entities/EntityKidnapper.java | 418 ++---------- .../remake/entities/EntityLaborGuard.java | 4 +- .../entities/ai/guard/GuardMonitorGoal.java | 4 +- .../kidnapper/KidnapperBringToCellGoal.java | 2 +- .../KidnapperDecideNextActionGoal.java | 4 +- .../entities/kidnapper/CaptivePriority.java | 46 ++ .../kidnapper/components/IAggressionHost.java | 16 + .../kidnapper/components/IDialogueHost.java | 46 ++ .../kidnapper/components/ILootHost.java | 52 ++ .../components/KidnapperAggressionSystem.java | 66 ++ .../components/KidnapperCaptiveManager.java | 6 +- .../components/KidnapperDialogue.java | 129 ++++ .../components/KidnapperLootManager.java | 247 +++++++ .../kidnapper/hosts/AggressionHost.java | 13 + .../kidnapper/hosts/DialogueHost.java | 53 ++ .../entities/kidnapper/hosts/LootHost.java | 60 ++ .../events/camp/CampManagementHandler.java | 4 +- .../lifecycle/PlayerStateEventHandler.java | 2 +- .../network/trader/PacketBuyCaptive.java | 2 +- .../prison/service/EscapeMonitorService.java | 616 ++++++++++++++++++ .../prison/service/PrisonerService.java | 591 +---------------- .../state/components/PlayerLifecycle.java | 2 +- 25 files changed, 1421 insertions(+), 968 deletions(-) create mode 100644 src/main/java/com/tiedup/remake/entities/kidnapper/CaptivePriority.java create mode 100644 src/main/java/com/tiedup/remake/entities/kidnapper/components/IDialogueHost.java create mode 100644 src/main/java/com/tiedup/remake/entities/kidnapper/components/ILootHost.java create mode 100644 src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperDialogue.java create mode 100644 src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperLootManager.java create mode 100644 src/main/java/com/tiedup/remake/entities/kidnapper/hosts/DialogueHost.java create mode 100644 src/main/java/com/tiedup/remake/entities/kidnapper/hosts/LootHost.java create mode 100644 src/main/java/com/tiedup/remake/prison/service/EscapeMonitorService.java diff --git a/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java b/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java index 4a18263..cfcb9bc 100644 --- a/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java +++ b/src/main/java/com/tiedup/remake/cells/CampLifecycleManager.java @@ -94,7 +94,7 @@ public final class CampLifecycleManager { ); } else { // Offline: full escape via PrisonerService (no grace period needed) - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( level, prisonerId, "camp death" diff --git a/src/main/java/com/tiedup/remake/cells/CellRegistryV2.java b/src/main/java/com/tiedup/remake/cells/CellRegistryV2.java index 122baf1..3369fa1 100644 --- a/src/main/java/com/tiedup/remake/cells/CellRegistryV2.java +++ b/src/main/java/com/tiedup/remake/cells/CellRegistryV2.java @@ -640,7 +640,7 @@ public class CellRegistryV2 extends SavedData { currentState == com.tiedup.remake.prison.PrisonerState.IMPRISONED ) { - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( level, id, "offline_cleanup" diff --git a/src/main/java/com/tiedup/remake/entities/EntityDamsel.java b/src/main/java/com/tiedup/remake/entities/EntityDamsel.java index ec80697..b4c55b8 100644 --- a/src/main/java/com/tiedup/remake/entities/EntityDamsel.java +++ b/src/main/java/com/tiedup/remake/entities/EntityDamsel.java @@ -772,7 +772,7 @@ public class EntityDamsel state == com.tiedup.remake.prison.PrisonerState.IMPRISONED || state == com.tiedup.remake.prison.PrisonerState.WORKING ) { - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( serverLevel, uuid, "player_death" diff --git a/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java b/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java index 30f0104..cefa414 100644 --- a/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java +++ b/src/main/java/com/tiedup/remake/entities/EntityKidnapper.java @@ -1,7 +1,6 @@ package com.tiedup.remake.entities; import com.tiedup.remake.cells.CellDataV2; -import com.tiedup.remake.cells.CellRegistryV2; import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.dialogue.IDialogueSpeaker; @@ -11,31 +10,23 @@ import com.tiedup.remake.entities.ai.kidnapper.*; import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem; import com.tiedup.remake.entities.skins.Gender; import com.tiedup.remake.entities.skins.KidnapperSkinManager; -import com.tiedup.remake.items.ModItems; import com.tiedup.remake.v2.bondage.CollarHelper; import com.tiedup.remake.personality.PersonalityType; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.ICaptor; import com.tiedup.remake.state.IRestrainable; -import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.tasks.ItemTask; -import com.tiedup.remake.util.tasks.SaleLoader; -import com.tiedup.remake.util.teleport.Position; import com.tiedup.remake.v2.BodyRegionV2; -import java.util.ArrayList; import java.util.List; import java.util.UUID; import javax.annotation.Nullable; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.*; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; @@ -44,7 +35,6 @@ import net.minecraft.world.entity.ai.goal.*; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; -import net.minecraft.world.phys.AABB; /** * EntityKidnapper - Aggressive NPC that captures and enslaves players. @@ -87,48 +77,6 @@ public class EntityKidnapper implements ICaptor, IDialogueSpeaker { - // CAPTIVE PRIORITY (for prisoner replacement) - - /** - * Priority levels for captives when replacing prisoners in cells. - * Higher priority captives will cause lower priority prisoners to be released. - */ - public enum CaptivePriority { - DAMSEL(1), - DAMSEL_SHINY(2), - PLAYER(3); - - private final int priority; - - CaptivePriority(int priority) { - this.priority = priority; - } - - public int getPriority() { - return priority; - } - - /** - * Get the priority for an entity. - * - * @param entity The entity to check - * @return The captive priority - */ - public static CaptivePriority fromEntity(LivingEntity entity) { - if (entity instanceof Player) return PLAYER; - if (entity instanceof EntityDamselShiny) return DAMSEL_SHINY; - if (entity instanceof EntityDamsel) return DAMSEL; - return DAMSEL; // Default for unknown entities - } - - /** - * Check if this priority is higher than another. - */ - public boolean isHigherThan(CaptivePriority other) { - return this.priority > other.priority; - } - } - // DATA SYNC (Client-Server) /** @@ -194,11 +142,11 @@ public class EntityKidnapper /** Whether this kidnapper is currently dogwalking a prisoner. */ private boolean dogwalking = false; - /** Items stolen from players via KidnapperThiefGoal. Dropped at 100% on death. */ - private final List stolenItems = new ArrayList<>(); + /** Loot manager for stolen items, collar keys, and death drops. */ + private final com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager lootManager; - /** Collar keys generated when collaring captives. Dropped at 20% on death. */ - private final List collarKeys = new ArrayList<>(); + /** Dialogue speaker implementation for IDialogueSpeaker delegation. */ + private final com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue dialogue; /** Job manager handles job assignment and tracking. */ private final KidnapperJobManager jobManager = new KidnapperJobManager( @@ -275,6 +223,18 @@ public class EntityKidnapper DATA_THEME_COLOR ); + // Initialize loot manager + this.lootManager = + new com.tiedup.remake.entities.kidnapper.components.KidnapperLootManager( + new com.tiedup.remake.entities.kidnapper.hosts.LootHost(this) + ); + + // Initialize dialogue component + this.dialogue = + new com.tiedup.remake.entities.kidnapper.components.KidnapperDialogue( + new com.tiedup.remake.entities.kidnapper.hosts.DialogueHost(this) + ); + // Initialize state manager this.stateManager = new com.tiedup.remake.entities.kidnapper.components.KidnapperStateManager( @@ -585,40 +545,35 @@ public class EntityKidnapper // All real IBondageState instances are IRestrainable, so the cast is safe. // Log a warning if the invariant is ever broken (future-proofing). + private void withRestrainable(String method, IBondageState captive, java.util.function.Consumer action) { + if (captive instanceof IRestrainable r) action.accept(r); + else logBridgeWarning(method, captive); + } + + private boolean testRestrainable(String method, IBondageState captive, java.util.function.Predicate test) { + if (captive instanceof IRestrainable r) return test.test(r); + logBridgeWarning(method, captive); + return false; + } + @Override public void addCaptive(IBondageState captive) { - if (captive instanceof IRestrainable r) { - captiveManager.addCaptive(r); - } else { - logBridgeWarning("addCaptive", captive); - } + withRestrainable("addCaptive", captive, captiveManager::addCaptive); } @Override public void removeCaptive(IBondageState captive, boolean transportState) { - if (captive instanceof IRestrainable r) { - captiveManager.removeCaptive(r, transportState); - } else { - logBridgeWarning("removeCaptive", captive); - } + withRestrainable("removeCaptive", captive, r -> captiveManager.removeCaptive(r, transportState)); } @Override public boolean canCapture(IBondageState captive) { - if ( - captive instanceof IRestrainable r - ) return captiveManager.canCapture(r); - logBridgeWarning("canCapture", captive); - return false; + return testRestrainable("canCapture", captive, captiveManager::canCapture); } @Override public boolean canRelease(IBondageState captive) { - if ( - captive instanceof IRestrainable r - ) return captiveManager.canRelease(r); - logBridgeWarning("canRelease", captive); - return false; + return testRestrainable("canRelease", captive, captiveManager::canRelease); } @Override @@ -633,29 +588,17 @@ public class EntityKidnapper @Override public void onCaptiveLogout(IBondageState captive) { - if (captive instanceof IRestrainable r) { - captiveManager.onCaptiveLogout(r); - } else { - logBridgeWarning("onCaptiveLogout", captive); - } + withRestrainable("onCaptiveLogout", captive, captiveManager::onCaptiveLogout); } @Override public void onCaptiveReleased(IBondageState captive) { - if (captive instanceof IRestrainable r) { - captiveManager.onCaptiveReleased(r); - } else { - logBridgeWarning("onCaptiveReleased", captive); - } + withRestrainable("onCaptiveReleased", captive, captiveManager::onCaptiveReleased); } @Override public void onCaptiveStruggle(IBondageState captive) { - if (captive instanceof IRestrainable r) { - captiveManager.onCaptiveStruggle(r); - } else { - logBridgeWarning("onCaptiveStruggle", captive); - } + withRestrainable("onCaptiveStruggle", captive, captiveManager::onCaptiveStruggle); } private void logBridgeWarning(String method, IBondageState captive) { @@ -936,130 +879,15 @@ public class EntityKidnapper super.die(damageSource); } - /** Token drop chance (5%) */ - private static final float TOKEN_DROP_CHANCE = 0.05f; - /** * Prevent taser from dropping when kidnapper dies. - * Taser is unique to kidnappers and should not be obtainable by players. - * Also handles token drop (5% chance). + * Delegates loot logic to KidnapperLootManager. */ @Override protected void dropEquipment() { - // Check main hand for taser - don't drop it - ItemStack mainHand = this.getItemBySlot( - net.minecraft.world.entity.EquipmentSlot.MAINHAND - ); - if ( - !mainHand.isEmpty() && - mainHand.getItem() instanceof com.tiedup.remake.items.ItemTaser - ) { - this.setItemSlot( - net.minecraft.world.entity.EquipmentSlot.MAINHAND, - ItemStack.EMPTY - ); - } - - // Check off hand too - ItemStack offHand = this.getItemBySlot( - net.minecraft.world.entity.EquipmentSlot.OFFHAND - ); - if ( - !offHand.isEmpty() && - offHand.getItem() instanceof com.tiedup.remake.items.ItemTaser - ) { - this.setItemSlot( - net.minecraft.world.entity.EquipmentSlot.OFFHAND, - ItemStack.EMPTY - ); - } - + lootManager.dropEquipment(); super.dropEquipment(); - - // Token drop: 5% chance when killed - if ( - !this.level().isClientSide && - this.getRandom().nextFloat() < TOKEN_DROP_CHANCE - ) { - ItemStack token = new ItemStack( - com.tiedup.remake.items.ModItems.TOKEN.get() - ); - this.spawnAtLocation(token); - TiedUpMod.LOGGER.info( - "[EntityKidnapper] {} dropped a token on death!", - this.getNpcName() - ); - } - - // Bind item drops from kidnapper inventory - if (!this.level().isClientSide) { - KidnapperItemSelector.SelectionResult selection = - getItemSelection(); - if (selection != null) { - float dropChance = 0.15f; - if ( - !selection.bind.isEmpty() && - this.getRandom().nextFloat() < dropChance - ) { - this.spawnAtLocation(selection.bind.copy()); - } - if ( - selection.hasGag() && - this.getRandom().nextFloat() < dropChance - ) { - this.spawnAtLocation(selection.gag.copy()); - } - if ( - selection.hasMittens() && - this.getRandom().nextFloat() < dropChance - ) { - this.spawnAtLocation(selection.mittens.copy()); - } - if ( - selection.hasEarplugs() && - this.getRandom().nextFloat() < dropChance - ) { - this.spawnAtLocation(selection.earplugs.copy()); - } - if ( - selection.hasBlindfold() && - this.getRandom().nextFloat() < dropChance - ) { - this.spawnAtLocation(selection.blindfold.copy()); - } - } - } - - // Drop stolen items at 100% rate (player's property) - if (!this.level().isClientSide) { - for (ItemStack stolen : this.stolenItems) { - if (!stolen.isEmpty()) { - this.spawnAtLocation(stolen); - } - } - if (!this.stolenItems.isEmpty()) { - TiedUpMod.LOGGER.info( - "[EntityKidnapper] {} dropped {} stolen item(s) on death", - this.getNpcName(), - this.stolenItems.size() - ); - } - this.stolenItems.clear(); - } - - // Drop collar keys at 20% rate - if (!this.level().isClientSide) { - for (ItemStack key : this.collarKeys) { - if (!key.isEmpty() && this.getRandom().nextFloat() < 0.20f) { - this.spawnAtLocation(key); - TiedUpMod.LOGGER.info( - "[EntityKidnapper] {} dropped a collar key on death", - this.getNpcName() - ); - } - } - this.collarKeys.clear(); - } + lootManager.dropPostEquipment(); } /** @@ -1255,27 +1083,8 @@ public class EntityKidnapper // Delegate to data serializer dataSerializer.saveToNBT(tag); - // Save stolen items - if (!this.stolenItems.isEmpty()) { - ListTag stolenTag = new ListTag(); - for (ItemStack stack : this.stolenItems) { - if (!stack.isEmpty()) { - stolenTag.add(stack.save(new CompoundTag())); - } - } - tag.put("StolenItems", stolenTag); - } - - // Save collar keys - if (!this.collarKeys.isEmpty()) { - ListTag keysTag = new ListTag(); - for (ItemStack key : this.collarKeys) { - if (!key.isEmpty()) { - keysTag.add(key.save(new CompoundTag())); - } - } - tag.put("CollarKeys", keysTag); - } + // Delegate stolen items and collar keys to loot manager + lootManager.saveToNBT(tag); } @Override @@ -1285,29 +1094,8 @@ public class EntityKidnapper // Delegate to data serializer dataSerializer.loadFromNBT(tag); - // Load stolen items - this.stolenItems.clear(); - if (tag.contains("StolenItems", Tag.TAG_LIST)) { - ListTag stolenTag = tag.getList("StolenItems", Tag.TAG_COMPOUND); - for (int i = 0; i < stolenTag.size(); i++) { - ItemStack stack = ItemStack.of(stolenTag.getCompound(i)); - if (!stack.isEmpty()) { - this.stolenItems.add(stack); - } - } - } - - // Load collar keys - this.collarKeys.clear(); - if (tag.contains("CollarKeys", Tag.TAG_LIST)) { - ListTag keysTag = tag.getList("CollarKeys", Tag.TAG_COMPOUND); - for (int i = 0; i < keysTag.size(); i++) { - ItemStack key = ItemStack.of(keysTag.getCompound(i)); - if (!key.isEmpty()) { - this.collarKeys.add(key); - } - } - } + // Delegate stolen items and collar keys to loot manager + lootManager.loadFromNBT(tag); } // ESCAPE TRACKING METHODS @@ -1370,59 +1158,14 @@ public class EntityKidnapper return CollarHelper.isOwner(collar, player); } - /** Damage reduction multiplier against monsters (50% damage taken) */ - private static final float MONSTER_DAMAGE_REDUCTION = 0.5f; - @Override public boolean hurt( net.minecraft.world.damagesource.DamageSource source, float amount ) { - float finalAmount = amount; - - // Track the attacker for fight back system - if (source.getEntity() instanceof LivingEntity attacker) { - aggressionSystem.setLastAttacker(attacker); - - // Punish prisoners who dare to attack us - if ( - !this.level().isClientSide && - attacker instanceof ServerPlayer player - ) { - punishAttackingPrisoner(player); - - // Expire PROTECTED status if player attacks a kidnapper - // Attacking a kidnapper voids your safe-exit window - if (this.level() instanceof ServerLevel serverLevel) { - com.tiedup.remake.prison.PrisonerManager pm = - com.tiedup.remake.prison.PrisonerManager.get( - serverLevel - ); - if ( - pm.getState(player.getUUID()) == - com.tiedup.remake.prison.PrisonerState.PROTECTED - ) { - pm.expireProtection( - player.getUUID(), - serverLevel.getGameTime() - ); - TiedUpMod.LOGGER.debug( - "[EntityKidnapper] Expired PROTECTED status for {} (attacked kidnapper)", - player.getName().getString() - ); - } - } - } - - // Camp kidnappers take reduced damage from monsters (trained fighters) - if ( - this.getAssociatedStructure() != null && - attacker instanceof net.minecraft.world.entity.monster.Monster - ) { - finalAmount = amount * MONSTER_DAMAGE_REDUCTION; - } - } - return super.hurt(source, finalAmount); + float modified = aggressionSystem.processIncomingDamage(source, amount); + if (modified <= 0) return false; + return super.hurt(source, modified); } /** @@ -1432,7 +1175,7 @@ public class EntityKidnapper * @param player The player who attacked * @return true if punishment was applied */ - protected boolean punishAttackingPrisoner(ServerPlayer player) { + public boolean punishAttackingPrisoner(ServerPlayer player) { return captiveManager.punishAttackingPrisoner(player); } @@ -1912,76 +1655,31 @@ public class EntityKidnapper com.tiedup.remake.util.MessageDispatcher.actionTo(this, player, action); } - /** Dialogue cooldown timer (ticks remaining before next dialogue) */ - private int dialogueCooldown = 0; - @Override public String getDialogueName() { - return this.getNpcName(); + return dialogue.getDialogueName(); } @Override public SpeakerType getSpeakerType() { - return SpeakerType.KIDNAPPER; + return dialogue.getSpeakerType(); } @Override @Nullable public PersonalityType getSpeakerPersonality() { - // Map kidnapper theme to a personality-like behavior - KidnapperTheme theme = this.getTheme(); - if (theme == null) { - return PersonalityType.CALM; - } - - return switch (theme) { - case ROPE, SHIBARI -> PersonalityType.CALM; // Traditional, methodical - case TAPE, LEATHER, CHAIN -> PersonalityType.FIERCE; // Rough, aggressive - case MEDICAL, ASYLUM -> PersonalityType.PROUD; // Clinical, professional - case LATEX, RIBBON -> PersonalityType.PLAYFUL; // Playful, teasing - case BEAM, WRAP -> PersonalityType.CURIOUS; // Experimental, modern - }; + return dialogue.getSpeakerPersonality(); } @Override public int getSpeakerMood() { - // Kidnappers mood is based on: - // - Having a captive (+20) - // - Current state (varies) - int mood = 50; - - if (this.hasCaptives()) { - mood += 20; - } - - // State-based adjustment - KidnapperState state = this.getCurrentState(); - if (state != null) { - mood += switch (state) { - case SELLING -> 10; // Excited about sale - case JOB_WATCH -> 5; - case GUARD -> 0; - case CAPTURE -> 15; // Hunting excitement - case PUNISH -> -10; // Stern - case PATROL, IDLE, HUNT -> 0; // Neutral for patrolling/hunting - case ALERT -> -5; // Concerned - case TRANSPORT -> 5; - }; - } - - return Math.max(0, Math.min(100, mood)); + return dialogue.getSpeakerMood(); } @Override @Nullable public String getTargetRelation(Player player) { - // Check if this kidnapper is holding the player captive - IRestrainable captive = this.getCaptive(); - if (captive != null && captive.asLivingEntity() == player) { - return "captor"; - } - - return null; + return dialogue.getTargetRelation(player); } @Override @@ -1991,12 +1689,12 @@ public class EntityKidnapper @Override public int getDialogueCooldown() { - return this.dialogueCooldown; + return dialogue.getDialogueCooldown(); } @Override public void setDialogueCooldown(int ticks) { - this.dialogueCooldown = ticks; + dialogue.setDialogueCooldown(ticks); } /** @@ -2004,9 +1702,7 @@ public class EntityKidnapper * Called from the main tick method. */ protected void tickDialogueCooldown() { - if (this.dialogueCooldown > 0) { - this.dialogueCooldown--; - } + dialogue.tickDialogueCooldown(); } // STOLEN ITEMS (Thief Goal) @@ -2016,9 +1712,7 @@ public class EntityKidnapper * Called by KidnapperThiefGoal when stealing from a player. */ public void addStolenItem(ItemStack stack) { - if (!stack.isEmpty()) { - this.stolenItems.add(stack.copy()); - } + lootManager.addStolenItem(stack); } // COLLAR KEYS (Capture Goal) @@ -2028,8 +1722,6 @@ public class EntityKidnapper * Called by KidnapperCaptureGoal when collaring a captive. */ public void addCollarKey(ItemStack keyStack) { - if (!keyStack.isEmpty()) { - this.collarKeys.add(keyStack.copy()); - } + lootManager.addCollarKey(keyStack); } } diff --git a/src/main/java/com/tiedup/remake/entities/EntityLaborGuard.java b/src/main/java/com/tiedup/remake/entities/EntityLaborGuard.java index e85f8ab..8031c3e 100644 --- a/src/main/java/com/tiedup/remake/entities/EntityLaborGuard.java +++ b/src/main/java/com/tiedup/remake/entities/EntityLaborGuard.java @@ -12,7 +12,7 @@ import com.tiedup.remake.entities.skins.LaborGuardSkinManager; import com.tiedup.remake.labor.LaborTask; import com.tiedup.remake.prison.LaborRecord; import com.tiedup.remake.prison.PrisonerManager; -import com.tiedup.remake.prison.service.PrisonerService; +import com.tiedup.remake.prison.service.EscapeMonitorService; import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.MessageDispatcher; @@ -520,7 +520,7 @@ public class EntityLaborGuard extends EntityDamsel { labor.setGuardId(null); // Trigger escape - PrisonerService.get().escape(level, prisonerUUID, reason); + EscapeMonitorService.get().escape(level, prisonerUUID, reason); // Notify prisoner ServerPlayer prisoner = level diff --git a/src/main/java/com/tiedup/remake/entities/ai/guard/GuardMonitorGoal.java b/src/main/java/com/tiedup/remake/entities/ai/guard/GuardMonitorGoal.java index 3895c3e..f4699a6 100644 --- a/src/main/java/com/tiedup/remake/entities/ai/guard/GuardMonitorGoal.java +++ b/src/main/java/com/tiedup/remake/entities/ai/guard/GuardMonitorGoal.java @@ -8,7 +8,7 @@ import com.tiedup.remake.labor.LaborTask; import com.tiedup.remake.prison.LaborRecord; import com.tiedup.remake.prison.PrisonerManager; import com.tiedup.remake.prison.PrisonerRecord; -import com.tiedup.remake.prison.service.PrisonerService; +import com.tiedup.remake.prison.service.EscapeMonitorService; import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.util.MessageDispatcher; @@ -562,7 +562,7 @@ public class GuardMonitorGoal extends Goal { LaborRecord labor = manager.getLaborRecord(prisoner.getUUID()); labor.setGuardId(null); - PrisonerService.get().escape(level, prisoner.getUUID(), reason); + EscapeMonitorService.get().escape(level, prisoner.getUUID(), reason); // Discard the guard entity itself guard.discard(); diff --git a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java index 741d4f9..a8b7026 100644 --- a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java +++ b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperBringToCellGoal.java @@ -8,7 +8,7 @@ import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory; import com.tiedup.remake.entities.AbstractTiedUpNpc; import com.tiedup.remake.entities.EntityKidnapper; -import com.tiedup.remake.entities.EntityKidnapper.CaptivePriority; +import com.tiedup.remake.entities.kidnapper.CaptivePriority; import com.tiedup.remake.entities.ai.StuckDetector; import com.tiedup.remake.entities.ai.WaypointNavigator; import com.tiedup.remake.v2.bondage.CollarHelper; diff --git a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperDecideNextActionGoal.java b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperDecideNextActionGoal.java index f94e981..a92bb3e 100644 --- a/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperDecideNextActionGoal.java +++ b/src/main/java/com/tiedup/remake/entities/ai/kidnapper/KidnapperDecideNextActionGoal.java @@ -8,7 +8,7 @@ import com.tiedup.remake.core.SystemMessageManager.MessageCategory; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory; import com.tiedup.remake.entities.EntityKidnapper; -import com.tiedup.remake.prison.service.PrisonerService; +import com.tiedup.remake.prison.service.EscapeMonitorService; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.v2.BodyRegionV2; import java.util.ArrayList; @@ -574,7 +574,7 @@ public class KidnapperDecideNextActionGoal extends Goal { captive.free(false); // false = don't drop leash (we take it) // 3b. Clean up PrisonerManager state (CAPTURED -> FREE) - PrisonerService.get().escape( + EscapeMonitorService.get().escape( (ServerLevel) player.level(), player.getUUID(), "theft_release" diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/CaptivePriority.java b/src/main/java/com/tiedup/remake/entities/kidnapper/CaptivePriority.java new file mode 100644 index 0000000..2c502a9 --- /dev/null +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/CaptivePriority.java @@ -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; + } +} diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/IAggressionHost.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/IAggressionHost.java index a733b63..fb48d9e 100644 --- a/src/main/java/com/tiedup/remake/entities/kidnapper/components/IAggressionHost.java +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/IAggressionHost.java @@ -1,5 +1,7 @@ package com.tiedup.remake.entities.kidnapper.components; +import java.util.UUID; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; @@ -26,4 +28,18 @@ public interface IAggressionHost { com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category, int radius ); + + /** + * Get the associated camp structure UUID. + */ + @Nullable + UUID getAssociatedStructure(); + + /** + * Punish a prisoner who attacks this kidnapper. + * + * @param player The player who attacked + * @return true if punishment was applied + */ + boolean punishAttackingPrisoner(ServerPlayer player); } diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/IDialogueHost.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/IDialogueHost.java new file mode 100644 index 0000000..e42a1c1 --- /dev/null +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/IDialogueHost.java @@ -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(); +} diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/ILootHost.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/ILootHost.java new file mode 100644 index 0000000..592c4a7 --- /dev/null +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/ILootHost.java @@ -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(); +} diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAggressionSystem.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAggressionSystem.java index 77c335c..82278f4 100644 --- a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAggressionSystem.java +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperAggressionSystem.java @@ -6,6 +6,9 @@ import com.tiedup.remake.state.IBondageState; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.LivingEntity; import org.jetbrains.annotations.Nullable; @@ -232,6 +235,69 @@ public class KidnapperAggressionSystem { this.robbedImmunity.clear(); } + // DAMAGE PROCESSING + + /** Damage reduction multiplier against monsters (50% damage taken) */ + private static final float MONSTER_DAMAGE_REDUCTION = 0.5f; + + /** + * Process incoming damage: track attacker, punish prisoners, expire protection, + * apply monster damage reduction for camp kidnappers. + * + * @param source The damage source + * @param amount The raw damage amount + * @return The modified damage amount (0 or less means cancel the hurt) + */ + public float processIncomingDamage(DamageSource source, float amount) { + float finalAmount = amount; + + // Track the attacker for fight back system + if (source.getEntity() instanceof LivingEntity attacker) { + this.setLastAttacker(attacker); + + // Punish prisoners who dare to attack us + if ( + host.level() != null && + !host.level().isClientSide && + attacker instanceof ServerPlayer player + ) { + host.punishAttackingPrisoner(player); + + // Expire PROTECTED status if player attacks a kidnapper + // Attacking a kidnapper voids your safe-exit window + if (host.level() instanceof ServerLevel serverLevel) { + com.tiedup.remake.prison.PrisonerManager pm = + com.tiedup.remake.prison.PrisonerManager.get( + serverLevel + ); + if ( + pm.getState(player.getUUID()) == + com.tiedup.remake.prison.PrisonerState.PROTECTED + ) { + pm.expireProtection( + player.getUUID(), + serverLevel.getGameTime() + ); + TiedUpMod.LOGGER.debug( + "[KidnapperAggressionSystem] Expired PROTECTED status for {} (attacked kidnapper)", + player.getName().getString() + ); + } + } + } + + // Camp kidnappers take reduced damage from monsters (trained fighters) + if ( + host.getAssociatedStructure() != null && + attacker instanceof net.minecraft.world.entity.monster.Monster + ) { + finalAmount = amount * MONSTER_DAMAGE_REDUCTION; + } + } + + return finalAmount; + } + // HELPER METHODS /** diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java index e9dc8c0..8332442 100644 --- a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperCaptiveManager.java @@ -508,7 +508,7 @@ public class KidnapperCaptiveManager { if (record != null && record.isImprisoned()) { // Clear captivity state - prisoner escaped // Use centralized escape service for complete cleanup - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( serverLevel, captiveUUID, "kidnapper tied up" @@ -545,7 +545,7 @@ public class KidnapperCaptiveManager { if (record != null && record.isImprisoned()) { // Clear captivity state - captor died // Use centralized escape service for complete cleanup - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( serverLevel, captiveUUID, "kidnapper died" @@ -670,7 +670,7 @@ public class KidnapperCaptiveManager { if (record != null && record.isImprisoned()) { // Clear captivity state - prisoner freed // Use centralized escape service for complete cleanup - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( serverLevel, captiveUUID, "abandoned by kidnapper" diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperDialogue.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperDialogue.java new file mode 100644 index 0000000..f1cf1c5 --- /dev/null +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperDialogue.java @@ -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 + * + *

Low complexity - Pure state queries with no side effects.

+ */ +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--; + } + } +} diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperLootManager.java b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperLootManager.java new file mode 100644 index 0000000..ec99b8a --- /dev/null +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/components/KidnapperLootManager.java @@ -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%) + * + *

Low complexity - List management with NBT persistence.

+ */ +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 stolenItems = new ArrayList<>(); + + /** Collar keys generated when collaring captives. Dropped at 20% on death. */ + private final List 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); + } + } + } + } +} diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/AggressionHost.java b/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/AggressionHost.java index 9b8a429..882f4eb 100644 --- a/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/AggressionHost.java +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/AggressionHost.java @@ -3,6 +3,8 @@ package com.tiedup.remake.entities.kidnapper.hosts; import com.tiedup.remake.dialogue.EntityDialogueManager; import com.tiedup.remake.entities.EntityKidnapper; import com.tiedup.remake.entities.kidnapper.components.IAggressionHost; +import java.util.UUID; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.Level; import org.jetbrains.annotations.Nullable; @@ -35,4 +37,15 @@ public class AggressionHost implements IAggressionHost { ) { entity.talkToPlayersInRadius(category, radius); } + + @Override + @Nullable + public UUID getAssociatedStructure() { + return entity.getAssociatedStructure(); + } + + @Override + public boolean punishAttackingPrisoner(ServerPlayer player) { + return entity.punishAttackingPrisoner(player); + } } diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/DialogueHost.java b/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/DialogueHost.java new file mode 100644 index 0000000..0658578 --- /dev/null +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/DialogueHost.java @@ -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; + } +} diff --git a/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/LootHost.java b/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/LootHost.java new file mode 100644 index 0000000..43f1521 --- /dev/null +++ b/src/main/java/com/tiedup/remake/entities/kidnapper/hosts/LootHost.java @@ -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(); + } +} diff --git a/src/main/java/com/tiedup/remake/events/camp/CampManagementHandler.java b/src/main/java/com/tiedup/remake/events/camp/CampManagementHandler.java index b2560b2..0d2f8cd 100644 --- a/src/main/java/com/tiedup/remake/events/camp/CampManagementHandler.java +++ b/src/main/java/com/tiedup/remake/events/camp/CampManagementHandler.java @@ -8,7 +8,7 @@ import com.tiedup.remake.entities.EntitySlaveTrader; import com.tiedup.remake.entities.ModEntities; // Prison system v2 import com.tiedup.remake.prison.PrisonerManager; -import com.tiedup.remake.prison.service.PrisonerService; +import com.tiedup.remake.prison.service.EscapeMonitorService; import java.util.List; import java.util.UUID; import net.minecraft.ChatFormatting; @@ -75,7 +75,7 @@ public class CampManagementHandler { } // Prison system v2 - tick escape service (handles escape detection) - PrisonerService.get().tick(level.getServer(), currentTime); + EscapeMonitorService.get().tick(level.getServer(), currentTime); // Prison system v2 - tick protection expiry PrisonerManager.get(level).tickProtectionExpiry(currentTime); diff --git a/src/main/java/com/tiedup/remake/events/lifecycle/PlayerStateEventHandler.java b/src/main/java/com/tiedup/remake/events/lifecycle/PlayerStateEventHandler.java index 66451ca..c7d0d2d 100644 --- a/src/main/java/com/tiedup/remake/events/lifecycle/PlayerStateEventHandler.java +++ b/src/main/java/com/tiedup/remake/events/lifecycle/PlayerStateEventHandler.java @@ -288,7 +288,7 @@ public class PlayerStateEventHandler { // Transition to FREE state via escape (clears all data) long currentTime = player.serverLevel().getGameTime(); // Use centralized escape service for complete cleanup - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( player.serverLevel(), player.getUUID(), "death" diff --git a/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java b/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java index fcef76d..f195784 100644 --- a/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java +++ b/src/main/java/com/tiedup/remake/network/trader/PacketBuyCaptive.java @@ -280,7 +280,7 @@ public class PacketBuyCaptive { } // Release via PrisonerService (state transition + cell cleanup + restraint removal) - com.tiedup.remake.prison.service.PrisonerService.get().release( + com.tiedup.remake.prison.service.EscapeMonitorService.get().release( level, captiveId, 6000L // 5 minutes grace period diff --git a/src/main/java/com/tiedup/remake/prison/service/EscapeMonitorService.java b/src/main/java/com/tiedup/remake/prison/service/EscapeMonitorService.java new file mode 100644 index 0000000..304354d --- /dev/null +++ b/src/main/java/com/tiedup/remake/prison/service/EscapeMonitorService.java @@ -0,0 +1,616 @@ +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) + 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( + "[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 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 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 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 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) {} +} diff --git a/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java b/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java index 6226736..e56c467 100644 --- a/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java +++ b/src/main/java/com/tiedup/remake/prison/service/PrisonerService.java @@ -6,40 +6,28 @@ import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.entities.AbstractTiedUpNpc; import com.tiedup.remake.entities.EntityDamsel; import com.tiedup.remake.personality.PersonalityState; -import com.tiedup.remake.prison.LaborRecord; import com.tiedup.remake.prison.PrisonerManager; import com.tiedup.remake.prison.PrisonerRecord; import com.tiedup.remake.prison.PrisonerState; -import com.tiedup.remake.prison.service.BondageService; -import com.tiedup.remake.state.CollarRegistry; import com.tiedup.remake.state.IBondageState; import com.tiedup.remake.state.ICaptor; -import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.util.KidnappedHelper; -import com.tiedup.remake.v2.BodyRegionV2; import java.util.*; -import net.minecraft.ChatFormatting; -import net.minecraft.core.BlockPos; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.network.chat.Component; -import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; -import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; import org.jetbrains.annotations.Nullable; /** - * Centralized prisoner lifecycle service. + * Prisoner lifecycle service. * - * Manages ALL prisoner state transitions atomically: + * Manages prisoner state transitions atomically: * - PrisonerManager (SavedData state) * - CellRegistryV2 (cell assignment) * - Leash/captor refs (IBondageState / ICaptor) - * - Escape detection (tick) * - * Replaces EscapeService and unifies scattered capture/imprison/extract/return/transfer logic. + * Handles: capture, imprison, extractFromCell, returnToCell, transferCaptive. + * Escape detection and release are handled by {@link EscapeMonitorService}. * * AI goals manage: Navigation, Teleport, Equipment, Dialogue. * PrisonerService manages: State transitions + leash coordination. @@ -54,29 +42,6 @@ public class PrisonerService { private PrisonerService() {} - // ==================== CONSTANTS (from EscapeService) ==================== - - /** Maximum distance from cell for IMPRISONED prisoners (blocks) */ - public static final double CELL_ESCAPE_DISTANCE = 20.0; - - /** Maximum distance from camp for WORKING prisoners (blocks) */ - public static final double WORK_ESCAPE_DISTANCE = 100.0; - - /** Maximum time offline before escape (30 minutes in ticks) */ - public static final long OFFLINE_TIMEOUT_TICKS = 30 * 60 * 20L; - - /** Maximum time in WORKING state before forced return (10 minutes in ticks) */ - public static final long WORK_TIMEOUT_TICKS = 10 * 60 * 20L; - - /** Maximum time in RETURNING phase before escape (5 minutes in ticks) */ - public static final long RETURN_TIMEOUT_TICKS = 5 * 60 * 20L; - - /** Maximum time in PENDING_RETURN phase before forced return (2.5 minutes in ticks) */ - public static final long PENDING_RETURN_TIMEOUT_TICKS = 3000; - - /** Check interval in ticks (every 5 seconds) */ - public static final int CHECK_INTERVAL_TICKS = 100; - // ==================== CAPTURE ==================== /** @@ -506,552 +471,4 @@ public class PrisonerService { return true; } - // ==================== ESCAPE (from EscapeService) ==================== - - /** - * CENTRAL ESCAPE METHOD - All escapes must go through here. - * - * Handles complete cleanup: - * - State transition (PrisonerManager) - * - Cell registry cleanup - * - Guard despawn - * - Restraints removal (if online) - * - Inventory NOT restored (stays in camp chest as punishment) - * - * @param level The server level - * @param playerId Player UUID - * @param reason Reason for escape (for logging) - * @return true if escape was processed successfully - */ - public boolean escape(ServerLevel level, UUID playerId, String reason) { - long currentTime = level.getGameTime(); - PrisonerManager manager = PrisonerManager.get(level); - CellRegistryV2 cellRegistry = CellRegistryV2.get(level); - - TiedUpMod.LOGGER.info( - "[PrisonerService] Processing escape for {} - reason: {}", - playerId.toString().substring(0, 8), - reason - ); - - // Step 1: Save guard ID BEFORE state transition (manager.escape() removes the LaborRecord) - LaborRecord laborBeforeEscape = manager.getLaborRecord(playerId); - UUID guardId = laborBeforeEscape.getGuardId(); - - // Step 2: Transition prisoner state to FREE - boolean stateChanged = manager.escape(playerId, currentTime, reason); - if (!stateChanged) { - TiedUpMod.LOGGER.warn( - "[PrisonerService] Failed to change state for {} - invalid transition", - playerId.toString().substring(0, 8) - ); - return false; - } - - // Step 3: Cleanup CellRegistryV2 - remove from all cells - int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId); - if (cellsCleared > 0) { - TiedUpMod.LOGGER.debug( - "[PrisonerService] Cleared {} from {} cells", - playerId.toString().substring(0, 8), - cellsCleared - ); - } - - // Step 4: Cleanup guard using saved reference (LaborRecord was removed by manager.escape()) - if (guardId != null) { - net.minecraft.world.entity.Entity guardEntity = level.getEntity( - guardId - ); - if (guardEntity != null) { - guardEntity.discard(); - TiedUpMod.LOGGER.debug( - "[PrisonerService] Despawned guard {} during escape", - guardId.toString().substring(0, 8) - ); - } - } - - // Step 5: Free from restraints (if player is online) - // NOTE: Inventory is NOT restored on escape - items remain in camp chest as punishment - ServerPlayer player = level - .getServer() - .getPlayerList() - .getPlayer(playerId); - if (player != null) { - IBondageState cap = KidnappedHelper.getKidnappedState(player); - if (cap != null) { - cap.free(false); - TiedUpMod.LOGGER.info( - "[PrisonerService] Freed {} from restraints (inventory remains in camp chest)", - player.getName().getString() - ); - } - } else { - TiedUpMod.LOGGER.debug( - "[PrisonerService] Player {} offline - restraints cleanup deferred", - playerId.toString().substring(0, 8) - ); - } - - TiedUpMod.LOGGER.info( - "[PrisonerService] Escape complete for {} - items stay in camp chest", - playerId.toString().substring(0, 8) - ); - - return true; - } - - // ==================== RELEASE (from EscapeService) ==================== - - /** - * CENTRAL RELEASE METHOD - All legitimate releases must go through here. - * - * Similar to escape() but transitions to PROTECTED state with grace period. - * Handles complete cleanup: - * - State transition to PROTECTED (PrisonerManager) - * - Cell registry cleanup - * - Inventory restoration - * - Restraints removal (if online) - * - * @param level The server level - * @param playerId Player UUID - * @param gracePeriodTicks Grace period before returning to FREE (0 = instant FREE) - * @return true if release was processed successfully - */ - public boolean release( - ServerLevel level, - UUID playerId, - long gracePeriodTicks - ) { - long currentTime = level.getGameTime(); - PrisonerManager manager = PrisonerManager.get(level); - CellRegistryV2 cellRegistry = CellRegistryV2.get(level); - com.tiedup.remake.cells.ConfiscatedInventoryRegistry inventoryRegistry = - com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level); - - TiedUpMod.LOGGER.info( - "[PrisonerService] Processing release for {} - grace period: {} ticks", - playerId.toString().substring(0, 8), - gracePeriodTicks - ); - - // Step 1: Transition prisoner state to PROTECTED (or FREE if gracePeriod = 0) - boolean stateChanged = manager.release( - playerId, - currentTime, - gracePeriodTicks - ); - if (!stateChanged) { - TiedUpMod.LOGGER.warn( - "[PrisonerService] Failed to change state for {} - invalid transition", - playerId.toString().substring(0, 8) - ); - return false; - } - - // Step 2: Cleanup CellRegistryV2 - remove from all cells - int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId); - if (cellsCleared > 0) { - TiedUpMod.LOGGER.debug( - "[PrisonerService] Cleared {} from {} cells", - playerId.toString().substring(0, 8), - cellsCleared - ); - } - - // Step 3: Restore confiscated inventory (if player is online) - ServerPlayer player = level - .getServer() - .getPlayerList() - .getPlayer(playerId); - if (player != null) { - // Restore inventory - if (inventoryRegistry.hasConfiscatedInventory(playerId)) { - boolean restored = inventoryRegistry.restoreInventory(player); - if (restored) { - TiedUpMod.LOGGER.info( - "[PrisonerService] Restored confiscated inventory for {}", - player.getName().getString() - ); - } - } - - // Free from restraints - IBondageState cap = KidnappedHelper.getKidnappedState(player); - if (cap != null) { - cap.free(false); - TiedUpMod.LOGGER.debug( - "[PrisonerService] Freed {} from restraints", - player.getName().getString() - ); - } - } else { - TiedUpMod.LOGGER.debug( - "[PrisonerService] Player {} offline - inventory cleanup deferred", - playerId.toString().substring(0, 8) - ); - } - - TiedUpMod.LOGGER.info( - "[PrisonerService] Release complete for {} - full cleanup done", - playerId.toString().substring(0, 8) - ); - - return true; - } - - // ==================== TICK (escape detection, from EscapeService) ==================== - - /** - * Tick escape detection. - * Called from CampManagementHandler. - * - * @param server The server - * @param currentTime Current game time - */ - public void tick(MinecraftServer server, long currentTime) { - if (currentTime % CHECK_INTERVAL_TICKS != 0) { - return; - } - - ServerLevel level = server.overworld(); - PrisonerManager manager = PrisonerManager.get(level); - CellRegistryV2 cells = CellRegistryV2.get(level); - CollarRegistry collars = CollarRegistry.get(level); - - List 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 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 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 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) {} } diff --git a/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java b/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java index 180d822..78ecf9b 100644 --- a/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java +++ b/src/main/java/com/tiedup/remake/state/components/PlayerLifecycle.java @@ -122,7 +122,7 @@ public class PlayerLifecycle { state == com.tiedup.remake.prison.PrisonerState.WORKING ) { // Use centralized escape service for complete cleanup - com.tiedup.remake.prison.service.PrisonerService.get().escape( + com.tiedup.remake.prison.service.EscapeMonitorService.get().escape( serverLevel, playerId, "player_death"