split PrisonerService + decompose EntityKidnapper

PrisonerService 1057L -> 474L lifecycle + 616L EscapeMonitorService
EntityKidnapper 2035L -> 1727L via LootManager, Dialogue, CaptivePriority extraction
This commit is contained in:
NotEvil
2026-04-16 14:08:52 +02:00
parent ea14fc2cec
commit f4aa5ffdc5
25 changed files with 1421 additions and 968 deletions

View File

@@ -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"

View File

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

View File

@@ -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

View File

@@ -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();

View File

@@ -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;

View File

@@ -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"

View File

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

View File

@@ -1,5 +1,7 @@
package com.tiedup.remake.entities.kidnapper.components;
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);
}

View File

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

View File

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

View File

@@ -6,6 +6,9 @@ import com.tiedup.remake.state.IBondageState;
import java.util.HashMap;
import java.util.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
/**

View File

@@ -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"

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.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);
}
}

View File

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

View File

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