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

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