Clean repo for open source release

Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,226 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.state.IBondageState;
import java.util.List;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.GoalSelector;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Host interface for KidnapperAIManager callbacks.
* Provides access to entity properties and components needed for AI goal registration.
*/
public interface IAIHost {
// ========================================
// NAVIGATION & BASIC
// ========================================
/**
* Get the entity's navigation system.
*/
PathNavigation getNavigation();
/**
* Get the goal selector for registering AI goals.
*/
GoalSelector getGoalSelector();
/**
* Get the entity's level/world.
*/
Level getLevel();
// ========================================
// STATE MANAGEMENT
// ========================================
/**
* Get the current behavioral state.
*/
KidnapperState getCurrentState();
/**
* Set the current behavioral state.
*/
void setCurrentState(KidnapperState state);
// ========================================
// TARGET MANAGEMENT
// ========================================
/**
* Get the current hunt target.
*/
@Nullable
LivingEntity getTarget();
/**
* Set the current hunt target.
*/
void setTarget(@Nullable LivingEntity target);
/**
* Check if an entity is a suitable target for capture.
*/
boolean isSuitableTarget(LivingEntity entity);
/**
* Check if target is still valid during an active chase.
*/
boolean isTargetStillValidForChase(LivingEntity entity);
/**
* Check if kidnapper is close enough to capture target.
*/
boolean isCloseToTarget();
/**
* Find closest suitable target within radius.
*/
@Nullable
LivingEntity getClosestSuitableTarget(int radius);
// ========================================
// CAPTIVE MANAGEMENT
// ========================================
/**
* Get the current captive.
*/
@Nullable
IBondageState getCaptive();
/**
* Check if kidnapper has captives.
*/
boolean hasCaptives();
// ========================================
// AGGRESSION SYSTEM
// ========================================
/**
* Get the last entity that attacked this kidnapper.
*/
@Nullable
LivingEntity getLastAttacker();
/**
* Get the escaped target if still within memory time.
*/
@Nullable
LivingEntity getEscapedTarget();
// ========================================
// ALERT SYSTEM
// ========================================
/**
* Get the alert target (escapee being searched for).
*/
@Nullable
LivingEntity getAlertTarget();
/**
* Set the alert target.
*/
void setAlertTarget(@Nullable LivingEntity target);
// ========================================
// CAMP SYSTEM
// ========================================
/**
* Get the associated structure UUID for this kidnapper.
*/
@Nullable
UUID getAssociatedStructure();
/**
* Check if this kidnapper is a "hunter" that patrols far from camp.
*/
boolean isHunter();
// ========================================
// CELL SYSTEM
// ========================================
/**
* Get all nearby cells that have prisoners in them.
*/
List<CellDataV2> getNearbyCellsWithPrisoners();
/**
* Get the PATROL markers near this kidnapper.
*/
List<BlockPos> getNearbyPatrolMarkers(int radius);
// ========================================
// SALE & JOB SYSTEM
// ========================================
/**
* Check if currently selling captive.
*/
boolean isSellingCaptive();
/**
* Check if waiting for worker to complete job.
*/
boolean isWaitingForJobToBeCompleted();
// ========================================
// COLLAR CONFIG
// ========================================
/**
* Check if kidnapping mode is fully ready (enabled + prison set).
*/
boolean isKidnappingModeReady();
/**
* Get cell ID from collar.
*/
@Nullable
UUID getCellIdFromCollar();
// ========================================
// CAPTURE EQUIPMENT
// ========================================
/**
* Equip themed bind and gag items before capture.
*/
void setUpHeldItems();
/**
* Get bind item to use for capture.
*/
ItemStack getBindItem();
/**
* Get gag item to use for capture.
*/
ItemStack getGagItem();
// ========================================
// STATE FLAGS
// ========================================
/**
* Check if in "get out" state (flee behavior).
*/
boolean isGetOutState();
/**
* Check if currently dogwalking a prisoner.
*/
boolean isDogwalking();
}

View File

@@ -0,0 +1,29 @@
package com.tiedup.remake.entities.kidnapper.components;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.level.Level;
/**
* Host interface for KidnapperAggressionSystem callbacks.
* Provides access to entity information needed for aggression tracking.
*/
public interface IAggressionHost {
/**
* Get the world/level for tick timing.
*/
@Nullable
Level level();
/**
* Get the entity's display name for dialogue.
*/
String getNpcName();
/**
* Trigger alert dialogue when captive escapes.
*/
void talkToPlayersInRadius(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius
);
}

View File

@@ -0,0 +1,46 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
/**
* Host interface for KidnapperAlertManager callbacks.
* Provides access to entity properties needed for the alert system.
*/
public interface IAlertHost {
/**
* Get the entity's level/world.
*/
Level level();
/**
* Get the entity's bounding box for area searches.
*/
AABB getBoundingBox();
/**
* Get the current AI state.
*/
KidnapperState getCurrentState();
/**
* Set the current AI state.
*/
void setCurrentState(KidnapperState state);
/**
* Check if this kidnapper has captives.
*/
boolean hasCaptives();
/**
* Check if this kidnapper is tied up.
*/
boolean isTiedUp();
/**
* Get the kidnapper's name for logging.
*/
String getNpcName();
}

View File

@@ -0,0 +1,60 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.skins.Gender;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
/**
* Host interface for KidnapperAppearance callbacks.
* Provides access to entity data and configuration needed for appearance management.
*/
public interface IAppearanceHost {
/**
* Get the entity's UUID for deterministic skin selection.
*/
UUID getUUID();
/**
* Get the entity's random source.
*/
RandomSource getRandom();
/**
* Get the world/level.
*/
@Nullable
Level level();
/**
* Set whether the kidnapper uses slim arms model.
*/
void setSlimArms(boolean slimArms);
/**
* Set the kidnapper's gender.
*/
void setGender(Gender gender);
/**
* Set the kidnapper's display name.
*/
void setNpcName(String name);
/**
* Get the kidnapper's display name.
*/
String getNpcName();
/**
* Set synced entity data string value.
*/
void setEntityDataString(EntityDataAccessor<String> accessor, String value);
/**
* Get synced entity data string value.
*/
String getEntityDataString(EntityDataAccessor<String> accessor);
}

View File

@@ -0,0 +1,19 @@
package com.tiedup.remake.entities.kidnapper.components;
import net.minecraft.util.RandomSource;
/**
* Host interface for KidnapperCampManager callbacks.
* Provides access to random source and name for camp assignment logic.
*/
public interface ICampHost {
/**
* Get random source for hunter role assignment.
*/
RandomSource getRandom();
/**
* Get the kidnapper's name for logging.
*/
String getNpcName();
}

View File

@@ -0,0 +1,112 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
/**
* Host interface for KidnapperCaptiveManager callbacks.
* Provides access to entity properties needed for captive management.
*/
public interface ICaptiveHost {
/**
* Get the entity's level/world.
*/
Level level();
/**
* Get the entity's UUID.
*/
UUID getUUID();
/**
* Get the entity's position.
*/
Vec3 position();
/**
* Get the entity's block position.
*/
BlockPos blockPosition();
/**
* Get the kidnapper's name for logging.
*/
String getNpcName();
/**
* Set entity data value.
*/
<T> void setEntityData(EntityDataAccessor<T> accessor, T value);
/**
* Check if this kidnapper is tied up.
*/
boolean isTiedUp();
/**
* Get the current AI state.
*/
KidnapperState getCurrentState();
/**
* Set the current AI state.
*/
void setCurrentState(KidnapperState state);
/**
* Set the "get out" / flee state.
*/
void setGetOutState(boolean state);
/**
* Trigger dialogue with nearby players.
*/
void talkToPlayersInRadius(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius
);
/**
* Get the aggression system for escape tracking.
*/
KidnapperAggressionSystem getAggressionSystem();
/**
* Get the alert manager for broadcasting alerts.
*/
KidnapperAlertManager getAlertManager();
/**
* Broadcast alert about escaped captive.
*/
void broadcastAlert(LivingEntity escapee);
/**
* Check if has line of sight to entity.
*/
boolean hasLineOfSight(LivingEntity entity);
/**
* Get distance to entity.
*/
float distanceTo(LivingEntity entity);
/**
* Find cell containing prisoner.
*/
@Nullable
com.tiedup.remake.cells.CellDataV2 findCellContainingPrisoner(
UUID prisonerId
);
/**
* Get entity as LivingEntity.
*/
LivingEntity asLivingEntity();
}

View File

@@ -0,0 +1,53 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.level.Level;
/**
* Host interface for KidnapperCellManager callbacks.
* Provides access to entity properties needed for cell management.
*/
public interface ICellHost {
/**
* Get the entity's level/world.
*/
Level level();
/**
* Get the entity's current block position.
*/
BlockPos blockPosition();
/**
* Get the associated structure UUID for camp-linked kidnappers.
*/
@Nullable
UUID getAssociatedStructure();
/**
* Get the entity's navigation component.
*/
PathNavigation getNavigation();
/**
* Set the current AI state.
*/
void setCurrentState(KidnapperState state);
/**
* Trigger dialogue with nearby players.
*/
void talkToPlayersInRadius(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius
);
/**
* Get the kidnapper's name for logging.
*/
String getNpcName();
}

View File

@@ -0,0 +1,83 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.KidnapperJobManager;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.syncher.EntityDataAccessor;
/**
* Host interface for KidnapperDataSerializer callbacks.
* Provides access to entity data and components for NBT persistence.
*/
public interface IDataSerializerHost {
// ========================================
// ENTITY ACCESS
// ========================================
/**
* Get the entity instance.
*/
EntityKidnapper getEntity();
// ========================================
// ENTITY DATA ACCESS
// ========================================
/**
* Get entity data value.
*/
<T> T getEntityData(EntityDataAccessor<T> accessor);
/**
* Set entity data value.
*/
<T> void setEntityData(EntityDataAccessor<T> accessor, T value);
// ========================================
// STATE FLAGS
// ========================================
/**
* Check if in "get out" state.
*/
boolean isGetOutState();
/**
* Set "get out" state.
*/
void setGetOutState(boolean state);
// ========================================
// COMPONENT ACCESS
// ========================================
/**
* Get appearance manager.
*/
KidnapperAppearance getAppearance();
/**
* Get aggression system.
*/
KidnapperAggressionSystem getAggressionSystem();
/**
* Get captive manager.
*/
KidnapperCaptiveManager getCaptiveManager();
/**
* Get state manager.
*/
KidnapperStateManager getStateManager();
/**
* Get camp manager.
*/
KidnapperCampManager getCampManager();
/**
* Get job manager.
*/
KidnapperJobManager getJobManager();
}

View File

@@ -0,0 +1,47 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.state.IRestrainable;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.level.Level;
/**
* Host interface for KidnapperSaleManager callbacks.
* Provides access to entity properties needed for sale management.
*/
public interface ISaleHost {
/**
* Get the entity's level/world.
*/
Level level();
/**
* Check if this kidnapper has captives.
*/
boolean hasCaptives();
/**
* Get the current captive.
*/
@Nullable
IRestrainable getCaptive();
/**
* Get the kidnapper's name for logging.
*/
String getNpcName();
/**
* Set the "get out" / flee state after completing a sale.
*/
void setGetOutState(boolean state);
/**
* Get the captive transfer flag.
*/
boolean getAllowCaptiveTransferFlag();
/**
* Set the captive transfer flag.
*/
void setAllowCaptiveTransferFlag(boolean flag);
}

View File

@@ -0,0 +1,14 @@
package com.tiedup.remake.entities.kidnapper.components;
/**
* Host interface for KidnapperStateManager callbacks.
* Provides access to entity information needed for state management logging.
*/
public interface IStateHost {
/**
* Get the name of this kidnapper for logging purposes.
*
* @return The kidnapper's name
*/
String getNpcName();
}

View File

@@ -0,0 +1,93 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.entities.KidnapperCollarConfig;
import com.tiedup.remake.entities.KidnapperJobManager;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.entity.ai.sensing.Sensing;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
/**
* Host interface for KidnapperTargetSelector callbacks.
* Provides access to entity properties needed for target selection and validation.
*/
public interface ITargetHost {
/**
* Get the entity's level/world.
*/
Level level();
/**
* Get the entity's sensing component for line-of-sight checks.
*/
Sensing getSensing();
/**
* Get the entity's bounding box for area searches.
*/
AABB getBoundingBox();
/**
* Calculate distance to another entity.
*/
float distanceTo(LivingEntity entity);
/**
* Get the entity's UUID.
*/
UUID getUUID();
/**
* Check if this kidnapper is tied up.
*/
boolean isTiedUp();
/**
* Check if this kidnapper has captives.
*/
boolean hasCaptives();
/**
* Check if waiting for a job to be completed.
*/
boolean isWaitingForJobToBeCompleted();
/**
* Check if in "get out" / fleeing state.
*/
boolean isGetOutState();
/**
* Check if kidnapping mode is enabled.
*/
boolean isKidnappingModeEnabled();
/**
* Get the associated structure UUID for camp-linked kidnappers.
*/
@Nullable
UUID getAssociatedStructure();
/**
* Get the entity's navigation component.
*/
PathNavigation getNavigation();
/**
* Get the aggression system for robbery immunity checks.
*/
KidnapperAggressionSystem getAggressionSystem();
/**
* Get the job manager for worker UUID checks.
*/
KidnapperJobManager getJobManager();
/**
* Get the collar config for kidnapping mode validation.
*/
KidnapperCollarConfig getCollarConfig();
}

View File

@@ -0,0 +1,167 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.*;
import net.minecraft.world.entity.ai.goal.*;
import net.minecraft.world.entity.player.Player;
/**
* KidnapperAIManager - Manages AI goal registration.
* Phase 3.2: AI management component (~79 lines).
*
* Handles:
* - Registering all 20 AI goals with priorities
* - Does NOT call super() - kidnappers have completely different AI than damsels
*
* <p><b>Low complexity</b> - Straightforward goal registration.</p>
*/
public class KidnapperAIManager {
// ========================================
// FIELDS
// ========================================
/** Entity reference (needed for goal constructors) */
private final EntityKidnapper entity;
/** Host callbacks */
private final IAIHost host;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperAIManager(EntityKidnapper entity, IAIHost host) {
this.entity = entity;
this.host = host;
}
// ========================================
// AI GOAL REGISTRATION
// ========================================
/**
* Register all AI goals for the kidnapper.
* CRITICAL: Does NOT call super() - kidnappers have completely different AI than damsels.
*/
public void registerGoals() {
// Don't call super - kidnapper has completely different AI than damsel
// Priority 0: Always swim
host.getGoalSelector().addGoal(0, new FloatGoal(this.entity));
// Priority 1: Fight back if attacked while holding captive (MEDIUM FIX: higher priority - self defense)
host
.getGoalSelector()
.addGoal(1, new KidnapperFightBackGoal(this.entity));
// Priority 1: Self-defense when attacked without captives (prevents PROTECTED exploit)
host
.getGoalSelector()
.addGoal(1, new KidnapperSelfDefenseGoal(this.entity));
// Priority 2: Hunt monsters (camp kidnappers only - protect captives)
host
.getGoalSelector()
.addGoal(2, new KidnapperHuntMonstersGoal(this.entity));
// Priority 3: ALERT - Search for escaped prisoner (high priority) (MEDIUM FIX: bumped from 2 to 3)
host.getGoalSelector().addGoal(3, new KidnapperAlertGoal(this.entity));
// Priority 3: Find and capture targets
host
.getGoalSelector()
.addGoal(3, new KidnapperFindTargetGoal(this.entity, 25));
// Priority 3: PUNISH - Punish recaptured prisoner (after Alert, before Capture)
host.getGoalSelector().addGoal(3, new KidnapperPunishGoal(this.entity));
// Priority 4: Capture target
host
.getGoalSelector()
.addGoal(4, new KidnapperCaptureGoal(this.entity));
// Priority 4: Recapture escaped captives (same priority as capture, but checked after)
host
.getGoalSelector()
.addGoal(4, new KidnapperRecaptureGoal(this.entity));
// Priority 5: Bring slave to cell (unified goal handles both camp and player cells)
host
.getGoalSelector()
.addGoal(5, new KidnapperBringToCellGoal(this.entity));
// Priority 6: Decide what to do with slave (sell or job)
host
.getGoalSelector()
.addGoal(6, new KidnapperDecideNextActionGoal(this.entity));
// Priority 6: Dispersal - Force movement away when too many near camp center
host
.getGoalSelector()
.addGoal(6, new KidnapperDispersalGoal(this.entity));
// Priority 7: GUARD - Watch over occupied cells
host.getGoalSelector().addGoal(7, new KidnapperGuardGoal(this.entity));
// Priority 7: Thief behavior for wild kidnappers (steal items and flee)
// BUG FIX (Phase 2): DISABLED - Integrated into DecideNextActionGoal
// Problem: ThiefGoal (P7) never executed because DecideNextActionGoal (P6)
// finished first and changed state (sell/job), blocking ThiefGoal activation.
// Solution: Integrated theft as a 3rd option (30% chance) in DecideNextActionGoal.
// The standalone ThiefGoal is now DEAD CODE and has been disabled.
// host.getGoalSelector().addGoal(7, new KidnapperThiefGoal(this.entity));
// Priority 7: Walk prisoner (DISABLED - chunk unload loses goal state,
// causing prisoners to be sold/stolen instead of returned to cell)
// host.getGoalSelector().addGoal(7, new KidnapperWalkPrisonerGoal(this.entity));
// Priority 8: Patrol when no slave (close to camp)
host.getGoalSelector().addGoal(8, new KidnapperPatrolGoal(this.entity));
// Priority 8: Hunt far from camp (only for hunters)
host.getGoalSelector().addGoal(8, new KidnapperHuntGoal(this.entity));
// Priority 9-10: Flee behavior (getOutState)
host
.getGoalSelector()
.addGoal(9, new KidnapperFleeWithCaptiveGoal(this.entity));
host
.getGoalSelector()
.addGoal(10, new KidnapperFleeSafeGoal(this.entity));
// Priority 11-12: Sale and Job systems
host
.getGoalSelector()
.addGoal(11, new KidnapperWaitForBuyerGoal(this.entity));
host
.getGoalSelector()
.addGoal(12, new KidnapperWaitForJobGoal(this.entity));
// Priority 13-15: Basic movement (lower priority)
host
.getGoalSelector()
.addGoal(13, new WaterAvoidingRandomStrollGoal(this.entity, 1.0D));
// Use custom LookAtPlayerGoal that ignores players in cells
host
.getGoalSelector()
.addGoal(
14,
new KidnapperLookAtPlayerGoal(this.entity, Player.class, 8.0F)
);
host
.getGoalSelector()
.addGoal(15, new RandomLookAroundGoal(this.entity));
// Priority 16: Open doors
host
.getGoalSelector()
.addGoal(16, new OpenDoorGoal(this.entity, false));
TiedUpMod.LOGGER.debug(
"[EntityKidnapper] Registered {} AI goals (ThiefGoal disabled)",
20
);
}
}

View File

@@ -0,0 +1,308 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.state.IBondageState;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.entity.LivingEntity;
/**
* KidnapperAggressionSystem - Manages aggression tracking and immunity.
* Phase 1.3: Aggression management component (151 lines).
*
* Handles three aggression subsystems:
* 1. **Escaped Target Memory** - Remember escapees for pursuit (30 seconds)
* 2. **Fight Back System** - Track last attacker for retaliation (5 seconds)
* 3. **Robbery Immunity** - Prevent repeated targeting after robbery (2 minutes)
*
* <p><b>Low complexity</b> - Simple state tracking with time-based expiration.</p>
*/
public class KidnapperAggressionSystem {
// ========================================
// CONSTANTS
// ========================================
/** How long to remember an escaped target (30 seconds = 600 ticks) */
private static final long ESCAPED_TARGET_MEMORY = 600;
/** Duration of robbed immunity (2 minutes = 2400 ticks) */
private static final long ROBBED_IMMUNITY_DURATION = 2400L;
/** How long to remember last attacker for fight-back (5 seconds = 100 ticks) */
private static final int FIGHT_BACK_MEMORY = 100;
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final IAggressionHost host;
// Escaped Target Tracking
/** Entity that escaped from this kidnapper */
@Nullable
private LivingEntity escapedTarget = null;
/** Tick count when captive escaped */
private long escapedTargetTime = 0;
// Fight Back System
/** Last entity that attacked this kidnapper */
@Nullable
private LivingEntity lastAttacker = null;
/** Tick count when last attacked */
private int lastAttackTime = 0;
// Robbery Immunity System
/**
* Map of player UUIDs to game time when immunity expires.
* Players robbed by this kidnapper cannot be targeted again for 2 minutes.
*/
private final Map<UUID, Long> robbedImmunity = new HashMap<>();
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperAggressionSystem(IAggressionHost host) {
this.host = host;
}
// ========================================
// ESCAPED TARGET SYSTEM
// ========================================
/**
* Called when a captive escapes from this kidnapper.
* Records the escaped entity for potential recapture.
*
* @param escaped The entity that escaped (IBondageState or LivingEntity)
*/
public void onCaptiveEscaped(@Nullable Object escaped) {
LivingEntity escapee = null;
if (escaped instanceof IBondageState kidnapped) {
if (kidnapped instanceof LivingEntity living) {
escapee = living;
}
} else if (escaped instanceof LivingEntity living) {
escapee = living;
}
if (escapee != null) {
this.escapedTarget = escapee;
this.escapedTargetTime = getCurrentTick();
TiedUpMod.LOGGER.debug(
"[KidnapperAggressionSystem] {} remembering escapee: {}",
host.getNpcName(),
escapee.getName().getString()
);
// Trigger alert dialogue
host.talkToPlayersInRadius(
EntityDialogueManager.DialogueCategory.CAPTURE_ESCAPE,
20
);
}
}
/**
* Get the escaped target if still remembered.
* Returns null if memory expired or target is dead/removed.
*
* @return The escaped entity, or null if forgotten/dead
*/
@Nullable
public LivingEntity getEscapedTarget() {
if (this.escapedTarget == null) {
return null;
}
// Check if memory expired (30 seconds)
long currentTick = getCurrentTick();
if (currentTick - this.escapedTargetTime > ESCAPED_TARGET_MEMORY) {
this.escapedTarget = null;
this.escapedTargetTime = 0;
return null;
}
// Check if target is still valid
if (!this.escapedTarget.isAlive() || this.escapedTarget.isRemoved()) {
this.escapedTarget = null;
this.escapedTargetTime = 0;
return null;
}
return this.escapedTarget;
}
/**
* Clear the escaped target memory.
*/
public void clearEscapedTarget() {
this.escapedTarget = null;
this.escapedTargetTime = 0;
}
// ========================================
// FIGHT BACK SYSTEM
// ========================================
/**
* Get the last entity that attacked this kidnapper.
* Returns null if attack was more than 5 seconds ago.
*
* @return The attacker if recent, null otherwise
*/
@Nullable
public LivingEntity getLastAttacker() {
if (this.lastAttacker == null) {
return null;
}
// Check if attack was recent (5 seconds = 100 ticks)
int currentTick = (int) getCurrentTick();
if (currentTick - this.lastAttackTime > FIGHT_BACK_MEMORY) {
return null;
}
return this.lastAttacker;
}
/**
* Set the last attacker for fight-back mechanics.
*
* @param attacker The entity that attacked
*/
public void setLastAttacker(LivingEntity attacker) {
this.lastAttacker = attacker;
this.lastAttackTime = (int) getCurrentTick();
}
// ========================================
// ROBBERY IMMUNITY SYSTEM
// ========================================
/**
* Grant robbery immunity to a player for 2 minutes.
* Players with immunity cannot be targeted by this kidnapper.
*
* @param playerUUID The player's UUID
*/
public void grantRobbedImmunity(UUID playerUUID) {
long expirationTime = getCurrentTick() + ROBBED_IMMUNITY_DURATION;
this.robbedImmunity.put(playerUUID, expirationTime);
TiedUpMod.LOGGER.debug(
"[KidnapperAggressionSystem] {} granted robbery immunity to player {}",
host.getNpcName(),
playerUUID
);
}
/**
* Check if a player has active robbery immunity.
* Lazily removes expired immunity during check.
*
* @param playerUUID The player's UUID
* @return true if player has active immunity
*/
public boolean hasRobbedImmunity(UUID playerUUID) {
Long expirationTime = this.robbedImmunity.get(playerUUID);
if (expirationTime == null) {
return false;
}
// Check if immunity expired (lazy cleanup)
long currentTick = getCurrentTick();
if (currentTick >= expirationTime) {
this.robbedImmunity.remove(playerUUID);
return false;
}
return true;
}
/**
* Clear robbery immunity for a specific player.
*
* @param playerUUID The player's UUID
*/
public void clearRobbedImmunity(UUID playerUUID) {
this.robbedImmunity.remove(playerUUID);
}
/**
* Clear all robbery immunities (e.g., on death/removal).
*/
public void clearAllRobbedImmunities() {
this.robbedImmunity.clear();
}
// ========================================
// HELPER METHODS
// ========================================
/**
* Get current game tick from world.
*/
private long getCurrentTick() {
if (host.level() != null) {
return host.level().getGameTime();
}
return 0;
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save aggression data to NBT.
* Note: Only saves robbery immunity map, not transient tracking fields.
*/
public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
// Save robbery immunity map
if (!this.robbedImmunity.isEmpty()) {
net.minecraft.nbt.CompoundTag immunityTag =
new net.minecraft.nbt.CompoundTag();
for (Map.Entry<UUID, Long> entry : this.robbedImmunity.entrySet()) {
immunityTag.putLong(
entry.getKey().toString(),
entry.getValue()
);
}
tag.put("RobbedImmunity", immunityTag);
}
}
/**
* Load aggression data from NBT.
*/
public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
// Load robbery immunity map
if (tag.contains("RobbedImmunity")) {
net.minecraft.nbt.CompoundTag immunityTag = tag.getCompound(
"RobbedImmunity"
);
for (String key : immunityTag.getAllKeys()) {
try {
UUID playerUUID = UUID.fromString(key);
long expirationTime = immunityTag.getLong(key);
this.robbedImmunity.put(playerUUID, expirationTime);
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn(
"[KidnapperAggressionSystem] Invalid UUID in robbery immunity: {}",
key
);
}
}
}
}
}

View File

@@ -0,0 +1,211 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
/**
* KidnapperAlertManager - Manages alert system between kidnappers.
* Phase 2.1: Alert management component (~75 lines).
*
* Handles:
* - Broadcasting alerts about escaped captives to nearby kidnappers
* - Receiving alert broadcasts from other kidnappers
* - Alert cooldown to prevent spam (5 second cooldown = 100 ticks)
* - Alert target tracking for ALERT state
*
* <p><b>Low complexity</b> - Simple broadcast/receive pattern with cooldown.</p>
*/
public class KidnapperAlertManager {
// ========================================
// CONSTANTS
// ========================================
/** Cooldown duration before receiving another alert (5 seconds = 100 ticks) */
private static final int ALERT_COOLDOWN_DURATION = 100;
/** Broadcast radius for alerts (50 blocks horizontal, 20 blocks vertical) */
private static final double BROADCAST_RADIUS_HORIZONTAL = 50.0;
private static final double BROADCAST_RADIUS_VERTICAL = 20.0;
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final IAlertHost host;
/** Entity reference (needed for getEntitiesOfClass filtering) */
private final EntityKidnapper entity;
/** Target entity being searched for during ALERT state */
@Nullable
private LivingEntity alertTarget;
/** Cooldown before receiving another alert broadcast (in ticks) */
private int alertCooldown = 0;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperAlertManager(EntityKidnapper entity, IAlertHost host) {
this.entity = entity;
this.host = host;
}
// ========================================
// ALERT TARGET TRACKING
// ========================================
/**
* Get the current alert target.
*
* @return The target entity being searched for, or null if none
*/
@Nullable
public LivingEntity getAlertTarget() {
return this.alertTarget;
}
/**
* Set the alert target.
*
* @param target The target to search for
*/
public void setAlertTarget(@Nullable LivingEntity target) {
this.alertTarget = target;
}
// ========================================
// ALERT BROADCASTING
// ========================================
/**
* Broadcast an alert about an escaped captive to nearby kidnappers.
* Searches for kidnappers within 50 blocks horizontally and 20 blocks vertically.
* Only kidnappers in states that can receive alerts will be notified.
*
* @param escapee The entity that escaped
*/
public void broadcastAlert(LivingEntity escapee) {
if (!(host.level() instanceof ServerLevel serverLevel)) {
return;
}
if (escapee == null) {
return;
}
// Find nearby kidnappers that can receive alerts
List<EntityKidnapper> nearby = serverLevel.getEntitiesOfClass(
EntityKidnapper.class,
host
.getBoundingBox()
.inflate(
BROADCAST_RADIUS_HORIZONTAL,
BROADCAST_RADIUS_VERTICAL,
BROADCAST_RADIUS_HORIZONTAL
),
k -> k != this.entity && k.getCurrentState().canReceiveAlerts()
);
TiedUpMod.LOGGER.info(
"[KidnapperAlertManager] {} broadcasting alert about {} to {} kidnappers",
host.getNpcName(),
escapee.getName().getString(),
nearby.size()
);
// Send alert to each nearby kidnapper
for (EntityKidnapper other : nearby) {
other.receiveAlertBroadcast(escapee, this.entity);
}
}
// ========================================
// ALERT RECEIVING
// ========================================
/**
* Receive an alert broadcast from another kidnapper.
* Ignores alerts if:
* - On cooldown (prevents spam)
* - Already has captives
* - Is tied up
*
* If accepted, sets alert target and switches to ALERT state.
*
* @param escapee The entity that escaped
* @param source The kidnapper that sent the alert
*/
public void receiveAlertBroadcast(
LivingEntity escapee,
EntityKidnapper source
) {
// On cooldown, ignore alert
if (this.alertCooldown > 0) {
return;
}
// Don't respond if we already have a captive or are tied up
if (host.hasCaptives() || host.isTiedUp()) {
return;
}
// Set alert target and state
this.setAlertTarget(escapee);
host.setCurrentState(KidnapperState.ALERT);
this.alertCooldown = ALERT_COOLDOWN_DURATION; // 5 second cooldown
TiedUpMod.LOGGER.info(
"[KidnapperAlertManager] {} received alert from {} about {}",
host.getNpcName(),
source.getNpcName(),
escapee.getName().getString()
);
}
// ========================================
// TICK PROCESSING
// ========================================
/**
* Called each tick to update alert system state.
* Decrements the alert cooldown timer.
*/
public void tick() {
// Decrement alert cooldown
if (this.alertCooldown > 0) {
this.alertCooldown--;
}
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save alert data to NBT.
* Note: alertTarget is transient and not saved (recalculated on load).
* Note: alertCooldown is transient and not saved (resets on load).
*/
public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
// Alert system uses only transient runtime state
// No persistent data needs to be saved
}
/**
* Load alert data from NBT.
* Note: alertTarget is transient and not loaded.
* Note: alertCooldown is transient and not loaded.
*/
public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
// Alert system uses only transient runtime state
// No persistent data needs to be loaded
}
}

View File

@@ -0,0 +1,396 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.KidnapperItemSelector;
import com.tiedup.remake.entities.KidnapperTheme;
import com.tiedup.remake.entities.KidnapperVariant;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.entities.skins.KidnapperSkinManager;
import com.tiedup.remake.items.base.ItemColor;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/**
* KidnapperAppearance - Manages visual appearance and item selection.
* Phase 1.2: Appearance management component (~220 lines).
*
* Handles:
* - Kidnapper skin variants (texture selection)
* - Item themes (bind/gag sets)
* - Theme colors
* - Item selection results
* - Initialization and persistence
*
* <p><b>Medium complexity</b> - Lazy loading, synced data integration.</p>
*/
public class KidnapperAppearance {
// ========================================
// FIELDS
// ========================================
/** Parent entity for accessing static data accessors */
private final EntityKidnapper entity;
/** Host callbacks */
private final IAppearanceHost host;
/** DATA accessors passed in (cannot be moved due to Minecraft static requirements) */
private final EntityDataAccessor<String> dataVariantId;
private final EntityDataAccessor<String> dataTheme;
private final EntityDataAccessor<String> dataThemeColor;
/** Current kidnapper skin variant (volatile for thread-safe render access) */
@Nullable
private volatile KidnapperVariant kidnapperVariant;
/** Current item theme */
@Nullable
private KidnapperTheme currentTheme;
/** Current theme color */
@Nullable
private ItemColor themeColor;
/** Selected items for this kidnapper */
@Nullable
private KidnapperItemSelector.SelectionResult itemSelection;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperAppearance(
EntityKidnapper entity,
IAppearanceHost host,
EntityDataAccessor<String> dataVariantId,
EntityDataAccessor<String> dataTheme,
EntityDataAccessor<String> dataThemeColor
) {
this.entity = entity;
this.host = host;
this.dataVariantId = dataVariantId;
this.dataTheme = dataTheme;
this.dataThemeColor = dataThemeColor;
}
// ========================================
// VARIANT SYSTEM
// ========================================
/**
* Set kidnapper variant and name.
*/
public void setKidnapperVariant(KidnapperVariant variant) {
if (variant == null) {
TiedUpMod.LOGGER.warn(
"[KidnapperAppearance] Attempted to set null kidnapper variant"
);
return;
}
this.kidnapperVariant = variant;
host.setEntityDataString(dataVariantId, variant.id());
host.setSlimArms(variant.hasSlimArms());
host.setGender(variant.gender());
applyVariantName(variant);
}
/**
* Get current kidnapper variant.
*/
@Nullable
public KidnapperVariant getKidnapperVariant() {
// Try to load from synced variant ID first
if (
this.kidnapperVariant == null && !getKidnapperVariantId().isEmpty()
) {
this.kidnapperVariant = lookupVariantById(getKidnapperVariantId());
}
// Fallback: compute from UUID if not yet synced (client-side race condition fix)
if (this.kidnapperVariant == null) {
this.kidnapperVariant = computeVariantForEntity(host.getUUID());
}
return this.kidnapperVariant;
}
/**
* Lookup a variant by ID from the skin manager.
* Delegates to entity's virtual method to allow subclass overrides.
*/
protected KidnapperVariant lookupVariantById(String variantId) {
return entity.lookupVariantById(variantId);
}
/**
* Compute which variant to use based on UUID.
* Delegates to entity's virtual method to allow subclass overrides.
*/
protected KidnapperVariant computeVariantForEntity(UUID entityUUID) {
return entity.computeVariantForEntity(entityUUID);
}
/**
* Get the texture folder for this kidnapper type.
* Delegates to entity's virtual method to allow subclass overrides.
*/
protected String getVariantTextureFolder() {
return entity.getVariantTextureFolder();
}
/**
* Get the default variant ID for fallback.
* Delegates to entity's virtual method to allow subclass overrides.
*/
protected String getDefaultVariantId() {
return entity.getDefaultVariantId();
}
/**
* Apply the name from variant to this entity.
* Delegates to entity's virtual method to allow subclass overrides.
*/
protected void applyVariantName(KidnapperVariant variant) {
entity.applyVariantName(variant);
}
/**
* Get the NBT key for saving the variant.
* Delegates to entity's virtual method to allow subclass overrides.
*/
protected String getVariantNBTKey() {
return entity.getVariantNBTKey();
}
/**
* Get kidnapper variant ID (synced data).
*/
public String getKidnapperVariantId() {
return host.getEntityDataString(dataVariantId);
}
/**
* Get the skin texture resource location.
* Reads variant ID directly from synced entityData to work on LAN clients
* where SkinManager is not populated.
*/
public ResourceLocation getSkinTexture() {
String variantId = getKidnapperVariantId();
if (!variantId.isEmpty()) {
return ResourceLocation.fromNamespaceAndPath(
"tiedup",
getVariantTextureFolder() + variantId + ".png"
);
}
// Fallback to default
return ResourceLocation.fromNamespaceAndPath(
"tiedup",
getVariantTextureFolder() + getDefaultVariantId() + ".png"
);
}
/**
* Invalidate variant cache (called when synced data updates from server).
*/
public void invalidateVariantCache() {
this.kidnapperVariant = null;
}
// ========================================
// INITIALIZATION
// ========================================
/**
* Initialize variant and theme when entity is first added to world.
* Should only be called on server side.
*/
public void initializeAppearance() {
initializeVariantOnSpawn();
initializeTheme();
}
/**
* Initialize variant on spawn (server-side only).
*/
protected void initializeVariantOnSpawn() {
if (host.level() == null || host.level().isClientSide) {
return;
}
// Only set variant if not already set
if (this.kidnapperVariant == null) {
KidnapperVariant variant = computeVariantForEntity(host.getUUID());
setKidnapperVariant(variant);
}
}
/**
* Initialize theme (server-side only).
*/
protected void initializeTheme() {
if (host.level() == null || host.level().isClientSide) {
return;
}
// Only initialize if not already set
if (this.currentTheme == null) {
this.itemSelection = selectItemsForKidnapper();
this.currentTheme = this.itemSelection.theme;
this.themeColor = this.itemSelection.color;
// Sync to client
host.setEntityDataString(dataTheme, this.currentTheme.name());
if (this.themeColor != null) {
host.setEntityDataString(
dataThemeColor,
this.themeColor.getName()
);
}
}
}
/**
* Select themed items for this kidnapper.
* Delegates to entity's virtual method to allow subclass overrides.
*/
protected KidnapperItemSelector.SelectionResult selectItemsForKidnapper() {
return entity.selectItemsForKidnapper();
}
// ========================================
// THEME & ITEM SELECTION
// ========================================
/**
* Get current theme (lazy load from synced data if needed).
*/
public KidnapperTheme getTheme() {
if (this.currentTheme == null) {
String themeName = host.getEntityDataString(dataTheme);
if (!themeName.isEmpty()) {
try {
this.currentTheme = KidnapperTheme.valueOf(themeName);
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.debug(
"[KidnapperAppearance] Invalid theme from synced data: {}",
themeName
);
}
}
}
return this.currentTheme;
}
/**
* Get current theme color (lazy load from synced data if needed).
*/
@Nullable
public ItemColor getThemeColor() {
if (this.themeColor == null) {
String colorName = host.getEntityDataString(dataThemeColor);
if (!colorName.isEmpty()) {
this.themeColor = ItemColor.fromName(colorName);
}
}
return this.themeColor;
}
/**
* Get item selection result.
*/
@Nullable
public KidnapperItemSelector.SelectionResult getItemSelection() {
return this.itemSelection;
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save appearance data to NBT.
*/
public void saveToNBT(CompoundTag tag) {
// Save kidnapper variant using NBT key
if (this.kidnapperVariant != null) {
tag.putString(getVariantNBTKey(), this.kidnapperVariant.id());
}
// Save theme and color
if (this.currentTheme != null) {
tag.putString("Theme", this.currentTheme.name());
}
if (this.themeColor != null) {
tag.putString("ThemeColor", this.themeColor.getName());
}
}
/**
* Load appearance data from NBT.
*/
public void loadFromNBT(CompoundTag tag) {
// Restore kidnapper variant using NBT key and lookup
String nbtKey = getVariantNBTKey();
if (tag.contains(nbtKey)) {
String variantId = tag.getString(nbtKey);
KidnapperVariant variant = lookupVariantById(variantId);
if (variant != null) {
this.kidnapperVariant = variant;
host.setEntityDataString(dataVariantId, variantId);
}
}
// Restore theme and color
if (tag.contains("Theme")) {
try {
this.currentTheme = KidnapperTheme.valueOf(
tag.getString("Theme")
);
host.setEntityDataString(dataTheme, this.currentTheme.name());
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.debug(
"[KidnapperAppearance] Invalid theme from NBT: {}",
tag.getString("Theme")
);
}
}
if (tag.contains("ThemeColor")) {
this.themeColor = ItemColor.fromName(tag.getString("ThemeColor"));
if (this.themeColor != null) {
host.setEntityDataString(
dataThemeColor,
this.themeColor.getName()
);
}
}
// Recreate item selection if theme was loaded
if (this.currentTheme != null) {
this.itemSelection = new KidnapperItemSelector.SelectionResult(
this.currentTheme,
this.themeColor,
KidnapperItemSelector.createBind(
this.currentTheme.getBind(),
this.themeColor
),
KidnapperItemSelector.createGag(
this.currentTheme.getPrimaryGag(),
this.themeColor
),
KidnapperItemSelector.createMittens(),
KidnapperItemSelector.createEarplugs(),
this.currentTheme.hasBlindfolds()
? KidnapperItemSelector.createBlindfold(
this.currentTheme.getPrimaryBlindfold(),
this.themeColor
)
: ItemStack.EMPTY
);
}
}
}

View File

@@ -0,0 +1,127 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
/**
* KidnapperCampManager - Manages camp/structure association.
* Phase 1.4: Camp management component (67 lines).
*
* Handles:
* - Structure UUID tracking (which camp this kidnapper belongs to)
* - Hunter role assignment (patrols far vs stays close to camp)
*
* <p><b>Low complexity</b> - Simple state tracking with smart initialization.</p>
*/
public class KidnapperCampManager {
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final ICampHost host;
/** UUID of the structure this kidnapper belongs to (for marker queries) */
@Nullable
private UUID associatedStructure;
/**
* Whether this kidnapper is a "hunter" that patrols far from camp.
* Hunters patrol 80-150 blocks from camp looking for prey.
* Non-hunters stay closer (GUARD/PATROL within 40 blocks).
* Decided randomly when associated with a camp (50/50 chance).
*/
private boolean isHunter = false;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperCampManager(ICampHost host) {
this.host = host;
}
// ========================================
// STRUCTURE ASSOCIATION
// ========================================
/**
* Get the associated structure UUID for this kidnapper.
*
* @return The structure UUID, or null if not associated
*/
@Nullable
public UUID getAssociatedStructure() {
return this.associatedStructure;
}
/**
* Set the associated structure UUID.
* When first associated with a camp, randomly decides if this kidnapper
* becomes a "hunter" (50% chance) that patrols far from camp.
*
* @param structureId The structure this kidnapper belongs to
*/
public void setAssociatedStructure(@Nullable UUID structureId) {
// If being newly associated with a camp, randomly assign hunter role
if (structureId != null && this.associatedStructure == null) {
this.isHunter = host.getRandom().nextBoolean(); // 50% chance
TiedUpMod.LOGGER.debug(
"[KidnapperCampManager] {} assigned to camp, isHunter: {}",
host.getNpcName(),
this.isHunter
);
}
this.associatedStructure = structureId;
}
// ========================================
// HUNTER ROLE
// ========================================
/**
* Check if this kidnapper is a "hunter" that patrols far from camp.
*
* @return true if this kidnapper hunts in the wilderness
*/
public boolean isHunter() {
return this.isHunter;
}
/**
* Set whether this kidnapper is a hunter.
*
* @param hunter true to make this kidnapper a hunter
*/
public void setHunter(boolean hunter) {
this.isHunter = hunter;
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save camp data to NBT.
*/
public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
if (this.associatedStructure != null) {
tag.putUUID("AssociatedStructure", this.associatedStructure);
}
tag.putBoolean("IsHunter", this.isHunter);
}
/**
* Load camp data from NBT.
*/
public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
if (tag.contains("AssociatedStructure")) {
this.associatedStructure = tag.getUUID("AssociatedStructure");
}
if (tag.contains("IsHunter")) {
this.isHunter = tag.getBoolean("IsHunter");
}
}
}

View File

@@ -0,0 +1,988 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.core.ModConfig;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
import com.tiedup.remake.state.IRestrainable;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.util.RestraintApplicator;
import java.util.Random;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.Mob;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3;
/**
* KidnapperCaptiveManager - Manages captive lifecycle and ICaptor implementation.
* Phase 3.1: Captive management component (~400 lines) - CRITICAL.
*
* Handles:
* - ICaptor interface implementation (10 methods)
* - Captive tracking (currentCaptive, pendingCaptiveUUID)
* - UUID lazy resolution after chunk reload
* - Lifecycle hooks (putBindOn, die, remove, tick)
* - Abandon/keep captive logic (solo mode fallback)
* - Struggle and attack punishment
* - Leash physics (visual positioning)
* - NBT serialization (backward compatible SlaveUUID → CaptiveUUID)
*
* <p><b>VERY HIGH complexity</b> - Critical captive state management with UUID resolution,
* leash physics, and punishment flows. Bugs here cause captive loss or state corruption.</p>
*/
public class KidnapperCaptiveManager {
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final ICaptiveHost host;
/** Entity reference (needed for leash attachment and UUID lookups) */
private final EntityKidnapper entity;
/** Entity data accessor for has-captive sync */
private final EntityDataAccessor<Boolean> dataHasCaptive;
/** Current captive being held */
@Nullable
private IRestrainable currentCaptive;
/** UUID of captive to restore after chunk/server reload */
@Nullable
private UUID pendingCaptiveUUID;
/** Retry counter for pending captive restoration (ticks) */
private int pendingCaptiveRetryTicks = 0;
/** Max ticks to retry before giving up (30 seconds = 600 ticks) */
private static final int MAX_PENDING_CAPTIVE_RETRY_TICKS = 600;
/** Flag to allow captive transfer (prevents re-capture during transfer) */
private boolean allowCaptiveTransferFlag = false;
/** Target for struggle punishment (shock/tighten) */
@Nullable
private LivingEntity strugglePunishmentTarget;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperCaptiveManager(
EntityKidnapper entity,
ICaptiveHost host,
EntityDataAccessor<Boolean> dataHasCaptive
) {
this.entity = entity;
this.host = host;
this.dataHasCaptive = dataHasCaptive;
}
// ========================================
// I_KIDNAPPER IMPLEMENTATION
// ========================================
/**
* Add a captive to this kidnapper.
* For NPCs: Attach vanilla leash.
* For Players: Leash already attached by PlayerBindState mixin.
*/
public void addCaptive(IRestrainable captive) {
if (captive == null) return;
// Guard: don't overwrite an existing captive - prevents state corruption
if (this.currentCaptive != null && this.currentCaptive != captive) {
TiedUpMod.LOGGER.warn(
"[KidnapperCaptiveManager] {} tried to capture {} but already has captive {} - rejecting",
host.getNpcName(),
captive.getKidnappedName(),
this.currentCaptive.getKidnappedName()
);
return;
}
// For Players: Leash is already attached by PlayerBindState.getCapturedBy()
// For NPCs: Attach vanilla leash directly
LivingEntity captiveEntity = captive.asLivingEntity();
if (captiveEntity instanceof Mob mob) {
// NPC captive - use vanilla leash
mob.setLeashedTo(this.entity, true);
}
// Player captives have LeashProxyEntity attached via mixin - no action needed here
this.currentCaptive = captive;
host.setEntityData(dataHasCaptive, true);
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} captured {}",
host.getNpcName(),
captive.getKidnappedName()
);
}
/**
* Remove a captive from this kidnapper.
* For NPCs: Detach vanilla leash.
* For Players: Leash detached by PlayerBindState mixin.
*
* @param captive The captive to remove
* @param transportState true if captive is being transported (no leash drop item)
*/
public void removeCaptive(IRestrainable captive, boolean transportState) {
if (captive == null || captive != this.currentCaptive) return;
// For Players: Leash detached by PlayerBindState.free()
// For NPCs: Detach vanilla leash without dropping item
LivingEntity captiveEntity = captive.asLivingEntity();
if (captiveEntity instanceof Mob mob && mob.isLeashed()) {
mob.dropLeash(false, false); // Don't drop leash item when imprisoning
}
// Player captives have LeashProxyEntity removed via mixin free() - no action needed here
String captiveName = this.currentCaptive.getKidnappedName();
this.currentCaptive = null;
host.setEntityData(dataHasCaptive, false);
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} released captive {}",
host.getNpcName(),
captiveName
);
}
/**
* Check if this kidnapper can capture a target.
* Validates collar ownership if target has collar.
* Also checks PrisonerManager state to prevent capturing already-imprisoned players.
*/
public boolean canCapture(IRestrainable target) {
if (target == null) return false;
// Check if target is already in any non-FREE state
// Only FREE players can be targeted by kidnappers
if (host.level() instanceof ServerLevel serverLevel) {
PrisonerManager manager = PrisonerManager.get(serverLevel);
PrisonerState state = manager.getState(
target.asLivingEntity().getUUID()
);
if (!state.isTargetable()) {
TiedUpMod.LOGGER.debug(
"[KidnapperCaptiveManager] {} can't capture {} - not targetable (state={})",
host.getNpcName(),
target.getKidnappedName(),
state
);
return false;
}
}
// If target has collar, verify we own it
if (target.hasCollar()) {
ItemStack collar = target.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
// Check if THIS kidnapper is owner
if (!collarItem.getOwners(collar).contains(host.getUUID())) {
TiedUpMod.LOGGER.debug(
"[KidnapperCaptiveManager] {} can't capture {} - collar owned by someone else",
host.getNpcName(),
target.getKidnappedName()
);
return false; // Not our collar
}
}
}
return true;
}
/**
* Check if captive can be released.
*/
public boolean canRelease(IRestrainable captive) {
return captive != null && captive == this.currentCaptive;
}
/**
* Check if captive transfer is allowed.
* Used to prevent re-capture during transfer between kidnappers.
*/
public boolean allowCaptiveTransfer() {
// Can transfer if explicitly flagged OR if we're tied up (forced release)
return this.allowCaptiveTransferFlag || host.isTiedUp();
}
/**
* Get the captive transfer flag.
*/
public boolean getAllowCaptiveTransferFlag() {
return this.allowCaptiveTransferFlag;
}
/**
* Set the captive transfer flag.
*/
public void setAllowCaptiveTransferFlag(boolean flag) {
this.allowCaptiveTransferFlag = flag;
}
/**
* Check if multiple captives are allowed.
*/
public boolean allowMultipleCaptives() {
return false; // Kidnappers only hold one captive at a time
}
/**
* Called when a captive (player) logs out.
* Releases the captive gracefully.
*/
public void onCaptiveLogout(IRestrainable captive) {
if (captive == this.currentCaptive) {
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} captive {} logged out - releasing",
host.getNpcName(),
captive.getKidnappedName()
);
removeCaptive(captive, false);
}
}
/**
* Called when a captive is released/freed.
* Tracks escape, broadcasts alert, and transitions to IDLE.
*/
public void onCaptiveReleased(IRestrainable releasedCaptive) {
if (releasedCaptive != this.currentCaptive) return;
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} captive {} was released",
host.getNpcName(),
releasedCaptive.getKidnappedName()
);
LivingEntity escapedEntity = releasedCaptive.asLivingEntity();
// Track escapee for potential recapture (30 seconds)
host.getAggressionSystem().onCaptiveEscaped(escapedEntity);
// Broadcast alert to nearby kidnappers
host.broadcastAlert(escapedEntity);
// Clear captive reference
this.currentCaptive = null;
host.setEntityData(dataHasCaptive, false);
// Return to IDLE state
host.setCurrentState(KidnapperState.IDLE);
}
/**
* Called when a captive struggles.
* Currently no special handling needed.
*/
public void onCaptiveStruggle(IRestrainable captive) {
// No-op - struggle punishment handled by onStruggleDetected() separately
}
/**
* Check if this kidnapper has captives.
*/
public boolean hasCaptives() {
return this.currentCaptive != null;
}
/**
* Get the entity implementing ICaptor.
*/
public LivingEntity getEntity() {
return this.entity;
}
// ========================================
// CAPTIVE ACCESS
// ========================================
/**
* Get the current captive.
*/
@Nullable
public IRestrainable getCaptive() {
return this.currentCaptive;
}
/**
* Transfer all captives to another kidnapper.
* Updates PrisonerManager with new captor.
*/
public void transferAllCaptivesTo(ICaptor target) {
if (!this.hasCaptives() || target == null) return;
// Update captivity state before transfer
if (
host.level() instanceof ServerLevel serverLevel &&
target.getEntity() != null
) {
PrisonerManager manager = PrisonerManager.get(serverLevel);
UUID captiveId = this.currentCaptive.asLivingEntity().getUUID();
PrisonerRecord record = manager.getRecord(captiveId);
// Update captor ID
record.setCaptorId(target.getEntity().getUUID());
}
// Transfer captive
this.allowCaptiveTransferFlag = true;
this.currentCaptive.transferCaptivityTo(target);
this.allowCaptiveTransferFlag = false;
}
// ========================================
// UUID RESTORATION (CRITICAL)
// ========================================
/**
* Restore captive reference from UUID after chunk/server reload.
* Called from customServerAiStep() when pendingCaptiveUUID is set but currentCaptive is null.
* This is CRITICAL for maintaining captive state across save/load cycles.
*
* FIX: Now retries for up to 30 seconds before giving up, to handle:
* - Players temporarily offline
* - Chunks not yet loaded
* - Entity loading delays
*/
public void restoreCaptiveFromUUID() {
if (this.pendingCaptiveUUID == null) return;
if (!(host.level() instanceof ServerLevel serverLevel)) return;
// Find entity by UUID
net.minecraft.world.entity.Entity foundEntity = serverLevel.getEntity(
this.pendingCaptiveUUID
);
if (foundEntity == null) {
// Entity not found - increment retry counter
this.pendingCaptiveRetryTicks++;
if (
this.pendingCaptiveRetryTicks >= MAX_PENDING_CAPTIVE_RETRY_TICKS
) {
// Timeout reached - give up
TiedUpMod.LOGGER.warn(
"[KidnapperCaptiveManager] {} giving up on captive UUID {} after {} ticks",
host.getNpcName(),
this.pendingCaptiveUUID,
this.pendingCaptiveRetryTicks
);
this.pendingCaptiveUUID = null;
this.pendingCaptiveRetryTicks = 0;
} else if (this.pendingCaptiveRetryTicks % 100 == 0) {
// Log every 5 seconds at debug level
TiedUpMod.LOGGER.debug(
"[KidnapperCaptiveManager] {} waiting for captive UUID {} ({}/{})",
host.getNpcName(),
this.pendingCaptiveUUID,
this.pendingCaptiveRetryTicks,
MAX_PENDING_CAPTIVE_RETRY_TICKS
);
}
return;
}
if (!(foundEntity instanceof LivingEntity livingEntity)) {
TiedUpMod.LOGGER.warn(
"[KidnapperCaptiveManager] {} found captive entity {} but it's not LivingEntity",
host.getNpcName(),
foundEntity.getName().getString()
);
this.pendingCaptiveUUID = null;
this.pendingCaptiveRetryTicks = 0;
return;
}
// Get kidnapped state
IRestrainable kidnappedState = KidnappedHelper.getKidnappedState(
livingEntity
);
if (kidnappedState == null) {
TiedUpMod.LOGGER.warn(
"[KidnapperCaptiveManager] {} found captive {} but it doesn't have IRestrainable state",
host.getNpcName(),
livingEntity.getName().getString()
);
this.pendingCaptiveUUID = null;
this.pendingCaptiveRetryTicks = 0;
return;
}
// Verify captive is still marked as captured by this kidnapper
ICaptor captor = kidnappedState.getCaptor();
UUID captorUUID = (captor != null && captor.getEntity() != null)
? captor.getEntity().getUUID()
: null;
if (
!kidnappedState.isCaptive() ||
captorUUID == null ||
!captorUUID.equals(host.getUUID())
) {
TiedUpMod.LOGGER.warn(
"[KidnapperCaptiveManager] {} found captive {} but it's not captured by us (captive={}, captor={})",
host.getNpcName(),
livingEntity.getName().getString(),
kidnappedState.isCaptive(),
captorUUID
);
this.pendingCaptiveUUID = null;
this.pendingCaptiveRetryTicks = 0;
return;
}
// Restore reference - success!
this.currentCaptive = kidnappedState;
host.setEntityData(dataHasCaptive, true);
this.pendingCaptiveUUID = null;
this.pendingCaptiveRetryTicks = 0;
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} restored captive {} from UUID",
host.getNpcName(),
kidnappedState.getKidnappedName()
);
}
/**
* Tick processing for UUID restoration and captive validation.
* Call this from EntityKidnapper.customServerAiStep().
*/
public void tick() {
// FIX: Check if current captive is still alive (died, disconnected, etc.)
// Without this, kidnapper keeps reference to dead captive, blocking all goals
if (this.currentCaptive != null) {
LivingEntity captiveEntity = this.currentCaptive.asLivingEntity();
if (captiveEntity == null || !captiveEntity.isAlive()) {
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} captive died or was removed - clearing reference",
host.getNpcName()
);
// Clear reference without dropping leash (entity is already gone)
this.currentCaptive = null;
host.setEntityData(dataHasCaptive, false);
host.setCurrentState(KidnapperState.IDLE);
}
}
// Restore captive from UUID after chunk/server reload
if (this.pendingCaptiveUUID != null && this.currentCaptive == null) {
restoreCaptiveFromUUID();
}
}
// ========================================
// LIFECYCLE HOOKS
// ========================================
/**
* Called when kidnapper gets bound/tied up.
* Releases captive and updates PrisonerManager.
*/
public void onPutBindOn() {
if (!this.hasCaptives()) return;
// Capture local reference - escape() may trigger onCaptiveReleased() which nulls the field
IRestrainable captive = this.currentCaptive;
if (captive == null) return;
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} got tied up - releasing captive {}",
host.getNpcName(),
captive.getKidnappedName()
);
// Release prisoner from captivity state
if (host.level() instanceof ServerLevel serverLevel) {
UUID captiveUUID = captive.asLivingEntity().getUUID();
PrisonerManager manager = PrisonerManager.get(serverLevel);
PrisonerRecord record = manager.getPrisoner(captiveUUID);
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(
serverLevel,
captiveUUID,
"kidnapper tied up"
);
}
}
// Free the captive
captive.free();
}
/**
* Called when kidnapper dies.
* Releases captive and updates PrisonerManager.
*/
public void onDie() {
if (!this.hasCaptives()) return;
// Capture local reference - escape() may trigger onCaptiveReleased() which nulls the field
IRestrainable captive = this.currentCaptive;
if (captive == null) return;
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} died - releasing captive {}",
host.getNpcName(),
captive.getKidnappedName()
);
// Release prisoner from captivity state
if (host.level() instanceof ServerLevel serverLevel) {
UUID captiveUUID = captive.asLivingEntity().getUUID();
PrisonerManager manager = PrisonerManager.get(serverLevel);
PrisonerRecord record = manager.getPrisoner(captiveUUID);
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(
serverLevel,
captiveUUID,
"kidnapper died"
);
}
}
// Free the captive
captive.free();
}
/**
* Called when kidnapper is removed (despawn/chunk unload).
* Conditional leash drop based on removal reason.
*/
public void onRemove(
net.minecraft.world.entity.Entity.RemovalReason reason
) {
if (!this.hasCaptives()) return;
// Graceful chunk unload - don't drop leash item
boolean transportState = (reason ==
net.minecraft.world.entity.Entity.RemovalReason.UNLOADED_TO_CHUNK);
TiedUpMod.LOGGER.debug(
"[KidnapperCaptiveManager] {} removed (reason={}), transport={}",
host.getNpcName(),
reason,
transportState
);
removeCaptive(this.currentCaptive, transportState);
}
// ========================================
// SOLO MODE FALLBACK
// ========================================
/**
* Keep captive (solo mode fallback when no buyers available).
* DecideNextActionGoal will handle job assignment or cell transport.
*/
public void keepCaptive() {
if (!this.hasCaptives()) {
TiedUpMod.LOGGER.warn(
"[KidnapperCaptiveManager] {} tried to keep captive but has none",
host.getNpcName()
);
return;
}
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} keeping captive {} for themselves",
host.getNpcName(),
this.currentCaptive.getKidnappedName()
);
// DecideNextActionGoal will take over and assign a job or bring to cell
// No need to do anything else here - just don't set getOutState
}
/**
* Abandon captive (solo mode fallback when no buyers and no cells).
* Blindfolds, teleports to safe position, optionally removes restraints.
*/
public void abandonCaptive() {
if (!this.hasCaptives()) {
TiedUpMod.LOGGER.warn(
"[KidnapperCaptiveManager] {} tried to abandon captive but has none",
host.getNpcName()
);
return;
}
LivingEntity captiveEntity = this.currentCaptive.asLivingEntity();
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} abandoning captive {}",
host.getNpcName(),
this.currentCaptive.getKidnappedName()
);
// Apply blindfold if enabled in config
if (
com.tiedup.remake.core.ModConfig.SERVER.abandonKeepsBlindfold.get()
) {
net.minecraft.world.item.ItemStack blindfold = getBlindfoldItem();
if (
blindfold != null &&
!blindfold.isEmpty() &&
!this.currentCaptive.isBlindfolded()
) {
this.currentCaptive.equip(BodyRegionV2.EYES, blindfold.copy());
}
}
// Find safe position and teleport
BlockPos safePos = findRandomSafePosition();
if (safePos != null) {
captiveEntity.teleportTo(
safePos.getX() + 0.5,
safePos.getY(),
safePos.getZ() + 0.5
);
}
// Remove restraints if NOT configured to keep them
boolean keepBinds =
com.tiedup.remake.core.ModConfig.SERVER.abandonKeepsBinds.get();
if (!keepBinds) {
// Full release including binds
this.currentCaptive.untie(true);
this.currentCaptive.free(true);
} else {
// Just release from leash, keep binds
this.currentCaptive.free(false);
}
// Release from captivity state
if (host.level() instanceof ServerLevel serverLevel) {
UUID captiveUUID = captiveEntity.getUUID();
PrisonerManager manager = PrisonerManager.get(serverLevel);
PrisonerRecord record = manager.getPrisoner(captiveUUID);
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(
serverLevel,
captiveUUID,
"abandoned by kidnapper"
);
}
}
// Enter flee state
host.setGetOutState(true);
}
/**
* Get blindfold item to use for abandon.
*/
@Nullable
private net.minecraft.world.item.ItemStack getBlindfoldItem() {
return this.entity.getBlindfoldItem();
}
/**
* Find a random safe position near kidnapper for abandoning captive.
*/
@Nullable
private BlockPos findRandomSafePosition() {
Random random = new Random();
for (int i = 0; i < 10; i++) {
int offsetX = random.nextInt(21) - 10;
int offsetZ = random.nextInt(21) - 10;
BlockPos testPos = host.blockPosition().offset(offsetX, 0, offsetZ);
if (host.level().getBlockState(testPos.below()).isSolid()) {
return testPos;
}
}
return host.blockPosition();
}
// ========================================
// PUNISHMENT SYSTEM
// ========================================
/** Maximum distance to HEAR struggle (through bars, no line of sight needed) */
private static final double STRUGGLE_HEARING_RANGE = 6.0;
/** Maximum distance to SEE struggle (requires line of sight) */
private static final double STRUGGLE_VISION_RANGE = 15.0;
/**
* Called when struggle noise is detected from a prisoner.
* Uses HYBRID detection: HEARING (close range) + VISION (far range).
*
* Detection modes:
* - Within 6 blocks: Can HEAR through bars/fences (no line of sight needed)
* - Within 15 blocks: Can SEE if line of sight is clear
*
* Validates prisoner belongs to us, then applies shock + tighten punishment.
*/
public void onStruggleDetected(LivingEntity source, BlockPos strugglePos) {
// Ignore if not in GUARD state or already has punishment target
if (
host.getCurrentState() != KidnapperState.GUARD ||
this.strugglePunishmentTarget != null
) {
return;
}
// HYBRID DETECTION: Hearing (close) + Vision (far)
double distance = host.distanceTo(source);
// HEARING: Close range - can hear through bars/fences (no LOS needed)
boolean canHear = distance <= STRUGGLE_HEARING_RANGE;
// VISION: Longer range - requires clear line of sight
boolean canSee =
distance <= STRUGGLE_VISION_RANGE && host.hasLineOfSight(source);
if (!canHear && !canSee) {
return; // Can't detect the struggle
}
// Verify this prisoner belongs to a cell we're guarding
IRestrainable sourceState = KidnappedHelper.getKidnappedState(source);
if (sourceState == null || !sourceState.isCaptive()) {
return;
}
// Check if prisoner is in a cell
CellDataV2 cell = host.findCellContainingPrisoner(source.getUUID());
if (cell == null) {
return;
}
String detectionMethod = canHear ? "heard" : "saw";
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} {} struggle from prisoner {} (distance: {})",
host.getNpcName(),
detectionMethod,
source.getName().getString(),
distance
);
// Apply shock punishment
sourceState.shockKidnapped(" Stop struggling!", 2.0f);
// Tighten binds
tightenBinds(source);
// Set punishment target
this.strugglePunishmentTarget = source;
}
/**
* Punish a prisoner for attacking.
* Used when prisoner attacks while in cell/camp.
* Stronger punishment: shock (4.0f) + tighten.
*/
public boolean punishAttackingPrisoner(Player player) {
if (player == null) return false;
IRestrainable playerState = KidnappedHelper.getKidnappedState(player);
if (playerState == null) return false;
// Check if captive (leashed) OR imprisoned/working in PrisonerManager
if (!playerState.isCaptive()) {
if (
!(host.level() instanceof
net.minecraft.server.level.ServerLevel serverLevel)
) {
return false;
}
PrisonerManager pm = PrisonerManager.get(serverLevel);
PrisonerState pmState = pm.getState(player.getUUID());
if (
pmState != PrisonerState.IMPRISONED &&
pmState != PrisonerState.WORKING
) {
return false;
}
}
boolean shouldPunish = false;
// Check if prisoner is in one of our cells
CellDataV2 cell = host.findCellContainingPrisoner(player.getUUID());
if (cell != null) {
shouldPunish = true;
}
// Also check camp ownership for camp kidnappers
if (!shouldPunish && host.level() instanceof ServerLevel serverLevel) {
UUID campId = entity.getAssociatedStructure();
if (campId != null) {
// Check if this prisoner is in a cell owned by our camp
com.tiedup.remake.cells.CellRegistryV2 cellRegistry =
com.tiedup.remake.cells.CellRegistryV2.get(serverLevel);
java.util.List<CellDataV2> campCells =
cellRegistry.getCellsByCamp(campId);
for (CellDataV2 campCell : campCells) {
if (campCell.hasPrisoner(player.getUUID())) {
shouldPunish = true;
break;
}
}
}
}
if (shouldPunish) {
TiedUpMod.LOGGER.info(
"[KidnapperCaptiveManager] {} punishing attacking prisoner {}",
host.getNpcName(),
player.getName().getString()
);
// Stronger shock for attacking
playerState.shockKidnapped(" How DARE you!", 4.0f);
// Tighten binds (also re-applies if player struggled free)
tightenBinds(player);
// Re-apply gag and blindfold if missing
RestraintApplicator.applyGagIfMissing(player, entity.getGagItem());
RestraintApplicator.applyBlindfoldIfMissing(
player,
entity.getBlindfoldItem()
);
// Trigger dialogue
host.talkToPlayersInRadius(
EntityDialogueManager.DialogueCategory.PUNISH,
20
);
return true;
}
return false;
}
/**
* Tighten binds on a target.
*/
private void tightenBinds(LivingEntity target) {
IRestrainable targetState = KidnappedHelper.getKidnappedState(target);
if (targetState != null) {
RestraintApplicator.tightenBind(targetState, target);
}
}
/**
* Set struggle punishment target.
*/
public void setStrugglePunishmentTarget(LivingEntity target) {
this.strugglePunishmentTarget = target;
}
/**
* Get struggle punishment target.
*/
@Nullable
public LivingEntity getStrugglePunishmentTarget() {
return this.strugglePunishmentTarget;
}
/**
* Clear struggle punishment target.
*/
public void clearStrugglePunishmentTarget() {
this.strugglePunishmentTarget = null;
}
// ========================================
// LEASH PHYSICS (VISUAL)
// ========================================
/**
* Get the position where the rope/leash is held.
* Overrides default chest position to use right hand.
* Custom visual physics - positions leash at right hand with body rotation.
*/
public Vec3 getRopeHoldPosition(float partialTicks) {
// Interpolate position for smooth rendering
double x = entity.xo + (entity.getX() - entity.xo) * partialTicks;
double y = entity.yo + (entity.getY() - entity.yo) * partialTicks;
double z = entity.zo + (entity.getZ() - entity.zo) * partialTicks;
// Get body yaw rotation
float bodyYaw =
entity.yBodyRotO +
(entity.yBodyRot - entity.yBodyRotO) * partialTicks;
double yawRad = Math.toRadians(bodyYaw);
// Calculate offset in kidnapper's local space (right hand position)
double lateralOffset = 0.4; // Right side
double heightOffset = 1.3; // Shoulder height
double forwardOffset = 0.2; // Slightly forward
// Rotate offset by body yaw
double offsetX =
-Math.sin(yawRad) * forwardOffset +
Math.cos(yawRad) * lateralOffset;
double offsetZ =
Math.cos(yawRad) * forwardOffset + Math.sin(yawRad) * lateralOffset;
return new Vec3(x + offsetX, y + heightOffset, z + offsetZ);
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save captive manager data to NBT.
* Saves captive UUID for lazy restoration.
* Backward compatible: saves as "CaptiveUUID" (old: "SlaveUUID").
*/
public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
// Save captive UUID if we have one
if (this.currentCaptive != null) {
tag.putUUID(
"CaptiveUUID",
this.currentCaptive.getKidnappedUniqueId()
);
}
}
/**
* Load captive manager data from NBT.
* Loads captive UUID for lazy restoration.
* Backward compatible: reads both "CaptiveUUID" and old "SlaveUUID".
*/
public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
// Load captive UUID (will be resolved later in tick)
// Support both old "SlaveUUID" and new "CaptiveUUID" for backward compatibility
if (tag.contains("CaptiveUUID")) {
this.pendingCaptiveUUID = tag.getUUID("CaptiveUUID");
} else if (tag.contains("SlaveUUID")) {
this.pendingCaptiveUUID = tag.getUUID("SlaveUUID");
}
}
}

View File

@@ -0,0 +1,269 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.blocks.entity.MarkerBlockEntity;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.cells.MarkerType;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.entity.BlockEntity;
/**
* KidnapperCellManager - Manages cell-related queries and interactions.
* Phase 2.3: Cell management component (~125 lines).
*
* Handles:
* - Finding cells containing specific prisoners
* - Querying nearby cells with occupants
* - Cell boundary detection (isInsideCell)
* - Patrol marker discovery
* - Cell breach response
*
* <p><b>Medium complexity</b> - Cell registry integration and boundary calculations.</p>
*/
public class KidnapperCellManager {
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final ICellHost host;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperCellManager(ICellHost host) {
this.host = host;
}
// ========================================
// CELL QUERIES
// ========================================
/**
* Find the cell containing a specific prisoner.
*
* @param prisonerId UUID of the prisoner
* @return The cell data, or null if not found
*/
@Nullable
public CellDataV2 findCellContainingPrisoner(UUID prisonerId) {
if (!(host.level() instanceof ServerLevel serverLevel)) return null;
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
return registry.findCellByPrisoner(prisonerId);
}
/**
* Get all nearby cells (within 32 blocks) that have prisoners.
*
* @return List of occupied cells near this kidnapper
*/
public List<CellDataV2> getNearbyCellsWithPrisoners() {
List<CellDataV2> result = new ArrayList<>();
if (!(host.level() instanceof ServerLevel serverLevel)) {
return result;
}
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
List<CellDataV2> nearbyCells = registry.findCellsNear(
host.blockPosition(),
32
);
for (CellDataV2 cell : nearbyCells) {
if (cell.isOccupied()) {
result.add(cell);
}
}
return result;
}
// ========================================
// CELL BOUNDARY DETECTION
// ========================================
/**
* Check if a position is inside a cell's boundaries.
* Uses wall markers to calculate bounds, or defaults to 5-block radius from spawn point.
*
* @param pos Position to check
* @param cell Cell data
* @return true if position is inside the cell
*/
public boolean isInsideCell(BlockPos pos, CellDataV2 cell) {
java.util.Set<BlockPos> walls = cell.getWallBlocks();
if (walls.isEmpty()) {
// No wall markers - use default radius from core position
BlockPos spawn = cell.getCorePos();
double distSq = pos.distSqr(spawn);
return distSq <= 25; // 5 block radius default
}
// Calculate bounding box from wall markers
int minX = Integer.MAX_VALUE,
minY = Integer.MAX_VALUE,
minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE,
maxY = Integer.MIN_VALUE,
maxZ = Integer.MIN_VALUE;
for (BlockPos wall : walls) {
minX = Math.min(minX, wall.getX());
minY = Math.min(minY, wall.getY());
minZ = Math.min(minZ, wall.getZ());
maxX = Math.max(maxX, wall.getX());
maxY = Math.max(maxY, wall.getY());
maxZ = Math.max(maxZ, wall.getZ());
}
// Check if position is within bounds (with 1-block tolerance on sides, 3-block tolerance on top)
return (
pos.getX() >= minX - 1 &&
pos.getX() <= maxX + 1 &&
pos.getY() >= minY - 1 &&
pos.getY() <= maxY + 3 &&
pos.getZ() >= minZ - 1 &&
pos.getZ() <= maxZ + 1
);
}
// ========================================
// PATROL MARKERS
// ========================================
/**
* Get all patrol markers from nearby cells.
*
* @param radius Search radius in blocks
* @return List of patrol marker positions
*/
public List<BlockPos> getNearbyPatrolMarkers(int radius) {
List<BlockPos> result = new ArrayList<>();
if (!(host.level() instanceof ServerLevel serverLevel)) return result;
BlockPos center = host.blockPosition();
int chunkRadius = (radius + 15) >> 4;
int centerCX = center.getX() >> 4;
int centerCZ = center.getZ() >> 4;
for (
int cx = centerCX - chunkRadius;
cx <= centerCX + chunkRadius;
cx++
) {
for (
int cz = centerCZ - chunkRadius;
cz <= centerCZ + chunkRadius;
cz++
) {
net.minecraft.world.level.chunk.LevelChunk chunk = serverLevel
.getChunkSource()
.getChunkNow(cx, cz);
if (chunk == null) continue;
for (BlockEntity be : chunk.getBlockEntities().values()) {
if (!(be instanceof MarkerBlockEntity marker)) continue;
if (marker.getMarkerType() != MarkerType.PATROL) continue;
BlockPos pos = be.getBlockPos();
if (center.distSqr(pos) <= (double) radius * radius) {
result.add(pos);
}
}
}
}
return result;
}
// ========================================
// CELL BREACH RESPONSE
// ========================================
/**
* Called when a cell wall is breached (broken).
* Kidnappers react to breaches in occupied cells or their own camp cells.
*
* @param breachPos Position where wall was broken
* @param cellId UUID of the cell that was breached
*/
public void onCellBreach(BlockPos breachPos, UUID cellId) {
if (cellId == null) return;
boolean shouldReact = false;
CellDataV2 cell = null;
// Check if this is an occupied cell
if (host.level() instanceof ServerLevel serverLevel) {
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
cell = registry.getCell(cellId);
if (cell != null && cell.isOccupied()) {
shouldReact = true;
}
}
// Also react if it's our associated structure
if (
host.getAssociatedStructure() != null &&
host.getAssociatedStructure().equals(cellId)
) {
shouldReact = true;
}
if (shouldReact) {
TiedUpMod.LOGGER.info(
"[KidnapperCellManager] {} detected wall breach at {}, investigating",
host.getNpcName(),
breachPos.toShortString()
);
// Navigate to breach location
host
.getNavigation()
.moveTo(
breachPos.getX() + 0.5,
breachPos.getY(),
breachPos.getZ() + 0.5,
1.2
);
// Enter alert state
host.setCurrentState(KidnapperState.ALERT);
// Trigger dialogue
host.talkToPlayersInRadius(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.CAPTURE_ESCAPE,
20
);
}
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save cell manager data to NBT.
* Note: Cell manager uses only transient runtime state.
*/
public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
// Cell manager uses only transient runtime state
// No persistent data needs to be saved
}
/**
* Load cell manager data from NBT.
* Note: Cell manager uses only transient runtime state.
*/
public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
// Cell manager uses only transient runtime state
// No persistent data needs to be loaded
}
}

View File

@@ -0,0 +1,123 @@
package com.tiedup.remake.entities.kidnapper.components;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.syncher.EntityDataAccessor;
/**
* KidnapperDataSerializer - Manages NBT serialization and deserialization.
* Phase 4.1: Data serialization component (~65 lines).
*
* Handles:
* - Orchestrating component save/load (appearance, aggression, captive, state, camp, job)
* - Direct entity data (KidnappingMode, GetOutState)
* - Backward compatibility (AIState → KidnapperState)
*
* <p><b>Low complexity</b> - Already component-based, minimal orchestration needed.</p>
*/
public class KidnapperDataSerializer {
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final IDataSerializerHost host;
/** Entity data accessor for kidnapping mode */
private final EntityDataAccessor<Boolean> dataKidnappingMode;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperDataSerializer(
IDataSerializerHost host,
EntityDataAccessor<Boolean> dataKidnappingMode
) {
this.host = host;
this.dataKidnappingMode = dataKidnappingMode;
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save all data to NBT.
* Orchestrates component saves and direct entity data.
*
* @param tag The NBT tag to save to
*/
public void saveToNBT(CompoundTag tag) {
// Save appearance data
host.getAppearance().saveToNBT(tag);
// Save aggression system data
host.getAggressionSystem().saveToNBT(tag);
// Save captive manager data
host.getCaptiveManager().saveToNBT(tag);
// Save direct entity data
tag.putBoolean(
"KidnappingMode",
host.getEntityData(dataKidnappingMode)
);
tag.putBoolean("GetOutState", host.isGetOutState());
// Save AI state
tag.putString(
host.getStateManager().getNBTKey(),
host.getStateManager().serializeState()
);
// Save camp manager state
host.getCampManager().saveToNBT(tag);
// Save job manager state
host.getJobManager().save(tag);
}
/**
* Load all data from NBT.
* Orchestrates component loads and direct entity data with backward compatibility.
*
* @param tag The NBT tag to load from
*/
public void loadFromNBT(CompoundTag tag) {
// Restore appearance data
host.getAppearance().loadFromNBT(tag);
// Restore aggression system data
host.getAggressionSystem().loadFromNBT(tag);
// Restore captive manager data
host.getCaptiveManager().loadFromNBT(tag);
// Restore direct entity data with null checks
if (tag.contains("KidnappingMode")) {
host.setEntityData(
dataKidnappingMode,
tag.getBoolean("KidnappingMode")
);
}
if (tag.contains("GetOutState")) {
host.setGetOutState(tag.getBoolean("GetOutState"));
}
// Load AI state with backward compatibility
String stateKey = host.getStateManager().getNBTKey();
if (tag.contains(stateKey)) {
host.getStateManager().deserializeState(tag.getString(stateKey));
} else if (tag.contains("AIState")) {
// Backward compatibility: old saves used "AIState" instead of "KidnapperState"
host.getStateManager().deserializeState(tag.getString("AIState"));
}
// Load camp manager state
host.getCampManager().loadFromNBT(tag);
// Load job manager state
host.getJobManager().load(tag);
}
}

View File

@@ -0,0 +1,230 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.EntityDamselShiny;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.util.tasks.ItemTask;
import com.tiedup.remake.util.tasks.SaleLoader;
import java.util.UUID;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
/**
* KidnapperSaleManager - Manages NPC sale transactions.
* Phase 2.4: Sale management component (~115 lines).
*
* Handles:
* - Initiating sales with price calculation (player/shiny multipliers)
* - Checking sale status
* - Canceling sales
* - Completing sales with captive transfer
* - PrisonerManager integration
*
* <p><b>Low complexity</b> - Straightforward state transitions and captive transfer.</p>
*/
public class KidnapperSaleManager {
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final ISaleHost host;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperSaleManager(ISaleHost host) {
this.host = host;
}
// ========================================
// SALE STATUS
// ========================================
/**
* Check if currently selling a captive.
*
* @return true if has captive and captive is for sale
*/
public boolean isSellingCaptive() {
return host.hasCaptives() && host.getCaptive().isForSell();
}
// ========================================
// START SALE
// ========================================
/**
* Start selling the current captive with auto-calculated price.
* Price is based on SaleLoader random price with multipliers:
* - Players: 2x multiplier
* - Shiny Damsels: 3x multiplier
* - Regular NPCs: 1x multiplier
*
* @return true if sale started successfully
*/
public boolean startSale() {
if (!host.hasCaptives()) {
TiedUpMod.LOGGER.warn(
"[KidnapperSaleManager] {} can't start sale - no captive",
host.getNpcName()
);
return false;
}
if (host.getCaptive().isForSell()) {
TiedUpMod.LOGGER.warn(
"[KidnapperSaleManager] {} captive already for sale",
host.getNpcName()
);
return false;
}
// Get base price
ItemTask basePrice = SaleLoader.getRandomSale();
// Calculate price multiplier based on captive type
int multiplier = 1;
LivingEntity captiveEntity = host.getCaptive().asLivingEntity();
if (captiveEntity instanceof Player) {
multiplier = 2; // Players cost 2x more
} else if (captiveEntity instanceof EntityDamselShiny) {
multiplier = 3; // Shiny damsels cost 3x more
}
// Apply multiplier
ItemTask finalPrice =
multiplier > 1
? new ItemTask(
basePrice.getItemId(),
basePrice.getAmount() * multiplier
)
: basePrice;
host.getCaptive().putForSale(finalPrice);
TiedUpMod.LOGGER.info(
"[KidnapperSaleManager] {} started selling {} for {} ({}x multiplier)",
host.getNpcName(),
host.getCaptive().getKidnappedName(),
finalPrice.toDisplayString(),
multiplier
);
return true;
}
/**
* Start selling the current captive with a specific price.
*
* @param price The price to sell for
* @return true if sale started successfully
*/
public boolean startSale(ItemTask price) {
if (!host.hasCaptives() || price == null) {
return false;
}
if (host.getCaptive().isForSell()) {
return false;
}
host.getCaptive().putForSale(price);
return true;
}
// ========================================
// CANCEL SALE
// ========================================
/**
* Cancel the current sale.
* Removes the for-sale flag from the captive.
*/
public void cancelSale() {
if (host.hasCaptives() && host.getCaptive().isForSell()) {
host.getCaptive().cancelSale();
}
}
// ========================================
// COMPLETE SALE
// ========================================
/**
* Complete a sale by transferring the captive to the buyer.
* Also updates PrisonerManager, removes sale flag, and triggers flee state.
*
* @param buyer The kidnapper purchasing the captive
* @return true if sale completed successfully
*/
public boolean completeSale(ICaptor buyer) {
if (!host.hasCaptives() || buyer == null) {
return false;
}
if (!host.getCaptive().isForSell()) {
return false;
}
// Cancel sale flag
host.getCaptive().cancelSale();
// Update PrisonerManager with new captor (the buyer)
if (
host.level() instanceof ServerLevel serverLevel &&
buyer.getEntity() != null
) {
PrisonerManager manager = PrisonerManager.get(serverLevel);
UUID captiveId = host.getCaptive().asLivingEntity().getUUID();
PrisonerRecord record = manager.getRecord(captiveId);
record.setCaptorId(buyer.getEntity().getUUID());
}
// Transfer captive to buyer
host.setAllowCaptiveTransferFlag(true);
host.getCaptive().transferCaptivityTo(buyer);
host.setAllowCaptiveTransferFlag(false);
TiedUpMod.LOGGER.info(
"[KidnapperSaleManager] {} sold captive to {}",
host.getNpcName(),
buyer.getEntity().getName().getString()
);
// Enter flee state after sale
host.setGetOutState(true);
return true;
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save sale manager data to NBT.
* Note: Sale state is stored in captive (IRestrainable), not in this manager.
*/
public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
// Sale manager uses only transient runtime state
// Sale flag is stored in captive entity
// No persistent data needs to be saved
}
/**
* Load sale manager data from NBT.
* Note: Sale state is loaded from captive (IRestrainable).
*/
public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
// Sale manager uses only transient runtime state
// Sale flag is loaded from captive entity
// No persistent data needs to be loaded
}
}

View File

@@ -0,0 +1,103 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
/**
* KidnapperStateManager - Manages the behavioral state of a kidnapper.
* Phase 1.1: Simple state management component (26 lines).
*
* Handles the current KidnapperState enum (IDLE, HUNT, CAPTURE, etc.)
* with debug logging for state transitions.
*
* <p><b>No dependencies</b> - Foundation component.</p>
*/
public class KidnapperStateManager {
// ========================================
// FIELDS
// ========================================
/** Host callbacks for logging */
private final IStateHost host;
/** Current behavioral state */
private KidnapperState currentState = KidnapperState.IDLE;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperStateManager(IStateHost host) {
this.host = host;
}
// ========================================
// STATE MANAGEMENT
// ========================================
/**
* Get the current behavioral state.
*
* @return The current state
*/
public KidnapperState getCurrentState() {
return this.currentState;
}
/**
* Set the current behavioral state with debug logging.
*
* @param state The new state
*/
public void setCurrentState(KidnapperState state) {
if (this.currentState != state) {
TiedUpMod.LOGGER.debug(
"[KidnapperStateManager] {} state change: {} -> {}",
host.getNpcName(),
this.currentState,
state
);
this.currentState = state;
}
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Get the NBT key for this component.
*
* @return The NBT key
*/
public String getNBTKey() {
return "CurrentState";
}
/**
* Serialize state to NBT.
*
* @return The state name as a string
*/
public String serializeState() {
return currentState.name();
}
/**
* Deserialize state from NBT.
*
* @param stateName The state name string
*/
public void deserializeState(String stateName) {
try {
this.currentState = KidnapperState.valueOf(stateName);
} catch (IllegalArgumentException e) {
TiedUpMod.LOGGER.warn(
"[KidnapperStateManager] Invalid state name: {}, defaulting to IDLE",
stateName
);
this.currentState = KidnapperState.IDLE;
}
}
}

View File

@@ -0,0 +1,543 @@
package com.tiedup.remake.entities.kidnapper.components;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.compat.mca.MCACompat;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.EntityKidnapperElite;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import java.util.List;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.AABB;
/**
* KidnapperTargetSelector - Manages target selection and validation logic.
* Phase 2.2: Target selection component (~286 lines) - CRITICAL.
*
* Handles:
* - isSuitableTarget() - Complex targeting logic with 8 categories of checks
* - isTargetStillValidForChase() - Mid-chase validation (no line-of-sight)
* - isTargetBeingPursuedByOther() - Competition detection
* - getClosestSuitableTarget() - Proximity-based targeting
* - Target tracking (currentTarget field)
*
* <p><b>VERY HIGH complexity</b> - Critical logic controlling kidnapper behavior.
* Bugs here cause kidnappers to be passive (ignore everyone) or too aggressive
* (target protected players). Preserve all conditions exactly.</p>
*/
public class KidnapperTargetSelector {
// ========================================
// FIELDS
// ========================================
/** Host callbacks */
private final ITargetHost host;
/** Entity reference (needed for type checks and competition detection) */
private final EntityKidnapper entity;
/** Current target being pursued */
@Nullable
private LivingEntity currentTarget;
// ========================================
// CONSTRUCTOR
// ========================================
public KidnapperTargetSelector(EntityKidnapper entity, ITargetHost host) {
this.entity = entity;
this.host = host;
}
// ========================================
// TARGET TRACKING
// ========================================
/**
* Get the current target being pursued.
*
* @return The target entity, or null if none
*/
@Nullable
public LivingEntity getTarget() {
return this.currentTarget;
}
/**
* Set the current target.
*
* @param target The target to pursue
*/
public void setTarget(@Nullable LivingEntity target) {
this.currentTarget = target;
}
// ========================================
// TARGET SUITABILITY (CRITICAL METHOD)
// ========================================
/**
* Check if an entity is a suitable target for capture.
* This is the CRITICAL method controlling kidnapper behavior.
*
* Validation categories:
* 1. Basic validity (null, alive, self, type)
* 2. Kidnapper state (has captive, tied up, waiting for job, fleeing)
* 3. Job worker protection
* 4. Player-specific (creative/spectator, robbed immunity, grace period, ransom/labor, token)
* 5. Kidnapped state (already captive, collar ownership)
* 6. Kidnapping mode (whitelist/blacklist)
* 7. Competition check (other kidnappers pursuing)
* 8. Line of sight (expensive, checked last)
*
* @param entity The entity to evaluate
* @return true if entity is a valid target, false otherwise
*/
public boolean isSuitableTarget(LivingEntity entity) {
if (entity == null) return false;
if (!entity.isAlive()) return false;
if (entity.isInvisible()) return false;
// Note: hasLineOfSight check moved to end (expensive raycast)
// Can't target self
if (entity == this.entity) return false;
// Can't target other kidnappers
if (entity instanceof EntityKidnapper) return false;
// Can't target labor guards (they're allies, not capture targets)
if (
entity instanceof com.tiedup.remake.entities.EntityLaborGuard
) return false;
// Can't target if we already have a captive
if (host.hasCaptives()) return false;
// Can't target if we're tied up
if (host.isTiedUp()) return false;
// Can't target if we're waiting for a job to be completed
if (host.isWaitingForJobToBeCompleted()) return false;
// Can't target if we're fleeing (get out state)
if (host.isGetOutState()) return false;
// Can't target the current job worker
UUID workerUUID = host.getJobManager().getJobWorkerUUID();
if (workerUUID != null && entity.getUUID().equals(workerUUID)) {
return false;
}
// FIX: Can't target job workers of OTHER kidnappers (wild jobs protection)
// Prevents kidnapper B from capturing a player doing a job for kidnapper A
if (host.level() instanceof ServerLevel serverLevel) {
AABB searchBox = new AABB(entity.blockPosition()).inflate(64);
List<EntityKidnapper> nearbyKidnappers =
serverLevel.getEntitiesOfClass(
EntityKidnapper.class,
searchBox
);
for (EntityKidnapper otherKidnapper : nearbyKidnappers) {
if (otherKidnapper == this.entity) continue;
if (
otherKidnapper.getJobManager() != null &&
entity
.getUUID()
.equals(
otherKidnapper.getJobManager().getJobWorkerUUID()
)
) {
return false; // Protected - active job worker for another kidnapper
}
}
}
// Player-specific checks
if (entity instanceof Player player) {
if (player.isCreative() || player.isSpectator()) return false;
// Robbed immunity check - this kidnapper recently robbed this player
if (
host.getAggressionSystem().hasRobbedImmunity(player.getUUID())
) {
return false;
}
// UNIFIED CAPTIVITY CHECK - check if player is detained in a camp/cell
// This prevents kidnappers from targeting players who are:
// 1. Being transported (CAPTURED, BEING_TRANSPORTED)
// 2. Imprisoned (in cell, working, returning, resting)
// 3. Under post-release protection (grace period)
if (host.level() instanceof ServerLevel serverLevel) {
PrisonerManager manager = PrisonerManager.get(serverLevel);
long currentTime = serverLevel.getGameTime();
// Can't target if captive (transported/imprisoned) OR protected (grace period)
if (!manager.isTargetable(player.getUUID(), currentTime)) {
return false; // Player is captive or protected
}
}
// Token check - only protects from kidnappers LINKED to a camp
// Non-linked kidnappers (wild spawns) ignore the token
if (
hasTokenInInventory(player) &&
host.getAssociatedStructure() != null
) {
return false; // Token protects from camp-linked kidnappers
}
}
// Get entity's kidnapped state
IBondageState state = KidnappedHelper.getKidnappedState(entity);
if (state == null) return false;
// Can't target if entity is already someone's captive (unless on pole)
if (state.isCaptive() && !state.isTiedToPole()) {
return false;
}
// Can't target if entity has a collar (is a slave) - UNLESS we own that collar
// OR the collared player escaped from our camp (same camp kidnappers can recapture)
// Self-collared entities (exploit) are treated as uncolllared — kidnappers ignore self-collars
if (state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
java.util.List<UUID> owners = collarItem.getOwners(collar);
// Filter out self-collar (owner == wearer = exploit)
java.util.List<UUID> realOwners = owners
.stream()
.filter(id -> !id.equals(entity.getUUID()))
.toList();
if (
!realOwners.isEmpty() &&
!realOwners.contains(host.getUUID())
) {
// Not our collar directly - check if escaped prisoner from our camp
boolean sameCampEscapee = false;
if (
entity instanceof Player collarPlayer &&
host.level() instanceof ServerLevel sl
) {
PrisonerRecord record = PrisonerManager.get(
sl
).getRecord(collarPlayer.getUUID());
UUID prisonerCampId = record.getCampId();
UUID ourCampId = host.getAssociatedStructure();
if (
prisonerCampId != null &&
ourCampId != null &&
prisonerCampId.equals(ourCampId)
) {
sameCampEscapee = true; // Escaped from our camp - recapture
}
}
if (!sameCampEscapee) {
return false; // Not our collar and not from our camp
}
}
// Our collar, self-collar, or same-camp escapee - continue targeting
} else {
return false; // Unknown collar type, don't target
}
}
// If kidnapping mode is enabled, check blacklist/whitelist (players only)
if (entity instanceof Player player && host.isKidnappingModeEnabled()) {
if (!host.getCollarConfig().isValidKidnappingTarget(player)) {
return false;
}
}
// Check if another kidnapper is already pursuing this target (elite kidnappers bypass this)
if (isTargetBeingPursuedByOther(entity)) {
return false;
}
// Expensive raycast check - do this last after all cheap checks pass
if (!host.getSensing().hasLineOfSight(entity)) return false;
return true;
}
// ========================================
// CHASE VALIDATION
// ========================================
/**
* Check if target is still valid during active chase.
* More lenient than isSuitableTarget - no line-of-sight requirement.
* Allows chasing through forests and around obstacles.
*
* @param entity The target being chased
* @return true if chase should continue, false otherwise
*/
public boolean isTargetStillValidForChase(LivingEntity entity) {
if (entity == null) return false;
if (!entity.isAlive()) return false;
if (entity.isInvisible()) return false;
// Can't continue if we got tied up
if (host.isTiedUp()) return false;
// Can't continue if we're fleeing
if (host.isGetOutState()) return false;
// Player-specific checks
if (entity instanceof Player player) {
if (player.isCreative() || player.isSpectator()) return false;
// UNIFIED CAPTIVITY CHECK - same as isSuitableTarget
if (host.level() instanceof ServerLevel serverLevel) {
PrisonerManager manager = PrisonerManager.get(serverLevel);
long currentTime = serverLevel.getGameTime();
// Can't chase if captive (transported/imprisoned) OR protected (grace period)
if (!manager.isTargetable(player.getUUID(), currentTime)) {
return false; // Player is captive or protected
}
}
}
// Check if captured by someone else
IBondageState state = KidnappedHelper.getKidnappedState(entity);
if (state != null && state.isCaptive() && !state.isTiedToPole()) {
// Captured by another - stop chase
return false;
}
// Check if collared by a PLAYER (player may have collared during chase)
// Other kidnappers' collars are fair game — kidnapper can steal from kidnapper
if (state != null && state.hasCollar()) {
ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
if (collar.getItem() instanceof ItemCollar collarItem) {
java.util.List<UUID> owners = collarItem.getOwners(collar);
if (!owners.isEmpty() && !owners.contains(host.getUUID())) {
// Check if any owner is a DIFFERENT player (not self-collared, not a kidnapper)
if (host.level() instanceof ServerLevel sl) {
for (UUID ownerId : owners) {
// Skip self-collared (exploit prevention: player puts collar on themselves)
if (ownerId.equals(entity.getUUID())) continue;
if (
sl
.getServer()
.getPlayerList()
.getPlayer(ownerId) !=
null
) {
return false; // Owned by another player — stop chase
}
}
}
// All owners are NPCs/kidnappers — continue chase
}
}
}
// NO LINE-OF-SIGHT CHECK - allow chasing through forests
return true;
}
// ========================================
// COMPETITION DETECTION
// ========================================
/**
* Check if another kidnapper is already effectively pursuing this target.
* Elite kidnappers bypass this check (always return false).
*
* Considers a target "pursued" only if:
* - Pursuer is in active pursuit state (HUNT/CHASE)
* - Pursuer is within 40 blocks
* - Pursuer has active navigation OR is very close (< 5 blocks)
*
* @param target The target entity
* @return true if another kidnapper is effectively pursuing, false otherwise
*/
public boolean isTargetBeingPursuedByOther(LivingEntity target) {
if (target == null) return false;
// Elite kidnappers can always pursue, regardless of other kidnappers
if (this.entity instanceof EntityKidnapperElite) {
return false;
}
if (!(host.level() instanceof ServerLevel serverLevel)) {
return false;
}
// Check nearby kidnappers (within 50 blocks)
List<EntityKidnapper> nearbyKidnappers = serverLevel.getEntitiesOfClass(
EntityKidnapper.class,
target.getBoundingBox().inflate(50, 20, 50),
k -> k != this.entity && k.isAlive()
);
for (EntityKidnapper other : nearbyKidnappers) {
LivingEntity otherTarget = other.getTarget();
if (otherTarget != null && otherTarget.equals(target)) {
// Check if the other kidnapper is actually pursuing effectively
KidnapperState otherState = other.getCurrentState();
// Only consider "pursued" if kidnapper is in active pursuit state
if (!otherState.isPursuit()) {
continue; // IDLE/GUARD/PATROL with stale target - ignore
}
// Check distance - if pursuer is too far (> 40 blocks), they're not effective
double distSq = other.distanceToSqr(target);
if (distSq > 1600) {
// > 40 blocks squared
continue;
}
// Check if pursuer has valid navigation path (can reach target)
if (!other.getNavigation().isInProgress() && distSq > 25) {
// Not moving and far away - probably stuck
continue;
}
// This kidnapper is effectively pursuing the target
return true;
}
}
return false;
}
// ========================================
// PROXIMITY TARGETING
// ========================================
/**
* Find the closest suitable target within a given radius.
* Search order: Players → Damsels → MCA villagers (if loaded)
*
* @param radius Search radius in blocks
* @return The closest suitable target, or null if none found
*/
@Nullable
public LivingEntity getClosestSuitableTarget(int radius) {
AABB searchBox = host.getBoundingBox().inflate(radius);
// Search for players first (highest priority)
List<Player> players = host
.level()
.getEntitiesOfClass(Player.class, searchBox);
for (Player player : players) {
if (this.isSuitableTarget(player)) {
return player;
}
}
// Then search for damsels
List<EntityDamsel> damsels = host
.level()
.getEntitiesOfClass(EntityDamsel.class, searchBox);
for (EntityDamsel damsel : damsels) {
// EntityDamsel no longer includes kidnappers (separate hierarchies)
if (this.isSuitableTarget(damsel)) {
return damsel;
}
}
// MCA Compatibility: Search for MCA villagers
if (MCACompat.isMCALoaded()) {
List<LivingEntity> entities = host
.level()
.getEntitiesOfClass(
LivingEntity.class,
searchBox,
MCACompat::isMCAVillager // Filter only MCA villagers
);
for (LivingEntity mcaVillager : entities) {
if (this.isSuitableTarget(mcaVillager)) {
return mcaVillager;
}
}
}
return null;
}
// ========================================
// DISTANCE HELPERS
// ========================================
/**
* Check if close to current target (within 2 blocks).
*
* @return true if close to target, false otherwise
*/
public boolean isCloseToTarget() {
if (this.currentTarget == null) return false;
return host.distanceTo(this.currentTarget) <= 2.0f;
}
/**
* Check if too far from current target.
*
* @param radius Maximum acceptable distance
* @return true if beyond radius, false otherwise
*/
public boolean isTooFarFromTarget(int radius) {
if (this.currentTarget == null) return false;
return host.distanceTo(this.currentTarget) > radius;
}
// ========================================
// HELPER METHODS
// ========================================
/**
* Check if a player has a protection token in their inventory.
* Tokens protect from camp-linked kidnappers only.
*
* @param player The player to check
* @return true if player has token, false otherwise
*/
public static boolean hasTokenInInventory(Player player) {
for (int i = 0; i < player.getInventory().getContainerSize(); i++) {
ItemStack stack = player.getInventory().getItem(i);
if (!stack.isEmpty() && stack.getItem() == ModItems.TOKEN.get()) {
return true;
}
}
return false;
}
// ========================================
// NBT SERIALIZATION
// ========================================
/**
* Save target selector data to NBT.
* Note: currentTarget is transient and not saved (recalculated on load).
*/
public void saveToNBT(net.minecraft.nbt.CompoundTag tag) {
// Target selector uses only transient runtime state
// No persistent data needs to be saved
}
/**
* Load target selector data from NBT.
* Note: currentTarget is transient and not loaded.
*/
public void loadFromNBT(net.minecraft.nbt.CompoundTag tag) {
// Target selector uses only transient runtime state
// No persistent data needs to be loaded
}
}

View File

@@ -0,0 +1,236 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.entities.kidnapper.components.IAIHost;
import com.tiedup.remake.state.IBondageState;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.goal.GoalSelector;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Host implementation for KidnapperAIManager callbacks.
* Provides access to entity properties and components needed for AI goal registration.
*/
public class AIHost implements IAIHost {
private final EntityKidnapper entity;
public AIHost(EntityKidnapper entity) {
this.entity = entity;
}
// ========================================
// NAVIGATION & BASIC
// ========================================
@Override
public PathNavigation getNavigation() {
return entity.getNavigation();
}
@Override
public GoalSelector getGoalSelector() {
return entity.goalSelector;
}
@Override
public Level getLevel() {
return entity.level();
}
// ========================================
// STATE MANAGEMENT
// ========================================
@Override
public KidnapperState getCurrentState() {
return entity.getCurrentState();
}
@Override
public void setCurrentState(KidnapperState state) {
entity.setCurrentState(state);
}
// ========================================
// TARGET MANAGEMENT
// ========================================
@Override
@Nullable
public LivingEntity getTarget() {
return entity.getTarget();
}
@Override
public void setTarget(@Nullable LivingEntity target) {
entity.setTarget(target);
}
@Override
public boolean isSuitableTarget(LivingEntity entity) {
return this.entity.isSuitableTarget(entity);
}
@Override
public boolean isTargetStillValidForChase(LivingEntity entity) {
return this.entity.isTargetStillValidForChase(entity);
}
@Override
public boolean isCloseToTarget() {
return entity.isCloseToTarget();
}
@Override
@Nullable
public LivingEntity getClosestSuitableTarget(int radius) {
return entity.getClosestSuitableTarget(radius);
}
// ========================================
// CAPTIVE MANAGEMENT
// ========================================
@Override
@Nullable
public IBondageState getCaptive() {
// C6-V2: EntityKidnapper.getCaptive() returns IRestrainable which IS-A IBondageState
return entity.getCaptive();
}
@Override
public boolean hasCaptives() {
return entity.hasCaptives();
}
// ========================================
// AGGRESSION SYSTEM
// ========================================
@Override
@Nullable
public LivingEntity getLastAttacker() {
return entity.getLastAttacker();
}
@Override
@Nullable
public LivingEntity getEscapedTarget() {
return entity.getEscapedTarget();
}
// ========================================
// ALERT SYSTEM
// ========================================
@Override
@Nullable
public LivingEntity getAlertTarget() {
return entity.getAlertTarget();
}
@Override
public void setAlertTarget(@Nullable LivingEntity target) {
entity.setAlertTarget(target);
}
// ========================================
// CAMP SYSTEM
// ========================================
@Override
@Nullable
public UUID getAssociatedStructure() {
return entity.getAssociatedStructure();
}
@Override
public boolean isHunter() {
return entity.isHunter();
}
// ========================================
// CELL SYSTEM
// ========================================
@Override
public List<CellDataV2> getNearbyCellsWithPrisoners() {
return entity.getNearbyCellsWithPrisoners();
}
@Override
public List<BlockPos> getNearbyPatrolMarkers(int radius) {
return entity.getNearbyPatrolMarkers(radius);
}
// ========================================
// SALE & JOB SYSTEM
// ========================================
@Override
public boolean isSellingCaptive() {
return entity.isSellingCaptive();
}
@Override
public boolean isWaitingForJobToBeCompleted() {
return entity.isWaitingForJobToBeCompleted();
}
// ========================================
// COLLAR CONFIG
// ========================================
@Override
public boolean isKidnappingModeReady() {
return entity.isKidnappingModeReady();
}
@Override
@Nullable
public UUID getCellIdFromCollar() {
return entity.getCellIdFromCollar();
}
// ========================================
// CAPTURE EQUIPMENT
// ========================================
@Override
public void setUpHeldItems() {
entity.setUpHeldItems();
}
@Override
public ItemStack getBindItem() {
return entity.getBindItem();
}
@Override
public ItemStack getGagItem() {
return entity.getGagItem();
}
// ========================================
// STATE FLAGS
// ========================================
@Override
public boolean isGetOutState() {
return entity.isGetOutState();
}
@Override
public boolean isDogwalking() {
return entity.isDogwalking();
}
}

View File

@@ -0,0 +1,39 @@
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 org.jetbrains.annotations.Nullable;
import net.minecraft.world.level.Level;
/**
* Host implementation for AggressionSystem callbacks.
* Phase 1.3: Provides access to entity properties for aggression tracking.
*/
public class AggressionHost implements IAggressionHost {
private final EntityKidnapper entity;
public AggressionHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
@Nullable
public Level level() {
return entity.level();
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
@Override
public void talkToPlayersInRadius(
EntityDialogueManager.DialogueCategory category,
int radius
) {
entity.talkToPlayersInRadius(category, radius);
}
}

View File

@@ -0,0 +1,55 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.entities.kidnapper.components.IAlertHost;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
/**
* Host implementation for KidnapperAlertManager callbacks.
* Provides access to entity properties needed for the alert system.
*/
public class AlertHost implements IAlertHost {
private final EntityKidnapper entity;
public AlertHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public Level level() {
return entity.level();
}
@Override
public AABB getBoundingBox() {
return entity.getBoundingBox();
}
@Override
public KidnapperState getCurrentState() {
return entity.getCurrentState();
}
@Override
public void setCurrentState(KidnapperState state) {
entity.setCurrentState(state);
}
@Override
public boolean hasCaptives() {
return entity.hasCaptives();
}
@Override
public boolean isTiedUp() {
return entity.isTiedUp();
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
}

View File

@@ -0,0 +1,72 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.kidnapper.components.IAppearanceHost;
import com.tiedup.remake.entities.skins.Gender;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.Level;
/**
* Host implementation for AppearanceManager callbacks.
* Phase 1.2: Provides access to entity properties needed for appearance management.
*/
public class AppearanceHost implements IAppearanceHost {
private final EntityKidnapper entity;
public AppearanceHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public UUID getUUID() {
return entity.getUUID();
}
@Override
public RandomSource getRandom() {
return entity.getRandom();
}
@Override
@Nullable
public Level level() {
return entity.level();
}
@Override
public void setSlimArms(boolean slimArms) {
entity.setSlimArms(slimArms);
}
@Override
public void setGender(Gender gender) {
entity.setGender(gender);
}
@Override
public void setNpcName(String name) {
entity.setNpcName(name);
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
@Override
public void setEntityDataString(
EntityDataAccessor<String> accessor,
String value
) {
entity.getEntityData().set(accessor, value);
}
@Override
public String getEntityDataString(EntityDataAccessor<String> accessor) {
return entity.getEntityData().get(accessor);
}
}

View File

@@ -0,0 +1,96 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.damsel.components.DamselInventoryManager;
import com.tiedup.remake.entities.damsel.components.IBondageHost;
import com.tiedup.remake.personality.PersonalityState;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* Host implementation for BondageManager callbacks specific to EntityKidnapper.
*
* Phase 3 (AbstractTiedUpNpc migration): Created so EntityKidnapper can provide
* its own IBondageHost instead of inheriting EntityDamsel's BondageHost.
*
* Key differences from the Damsel BondageHost:
* - getPersonalityState() returns null (kidnappers have no personality system)
* - talkToPlayersInRadius() is a no-op (kidnapper dialogue is separate)
*
* NOTE: This host is created during super() constructor (AbstractTiedUpNpc).
* It must only store the entity reference, NOT access any EntityKidnapper fields
* that haven't been initialized yet.
*/
public class BondageHost implements IBondageHost {
private final EntityKidnapper entity;
public BondageHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public PersonalityState getPersonalityState() {
// Kidnappers don't have a personality system
return null;
}
@Override
public DamselInventoryManager getInventory() {
return entity.getInventoryManager();
}
@Override
public void dropItemStack(ItemStack stack) {
entity.spawnAtLocation(stack);
}
@Override
public void playSound(SoundEvent sound) {
entity.playSound(sound, 1.0f, 1.0f);
}
@Override
public void setHealth(float health) {
entity.setHealth(health);
}
@Override
public void remove(Entity.RemovalReason reason) {
entity.remove(reason);
}
@Override
public Level level() {
return entity.level();
}
@Override
public BlockPos blockPosition() {
return entity.blockPosition();
}
@Override
public UUID getUUID() {
return entity.getUUID();
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
@Override
public void talkToPlayersInRadius(
EntityDialogueManager.DialogueCategory category,
int radius
) {
// Kidnappers use their own dialogue system (KidnapperDialogueTriggerSystem),
// not the damsel dialogue categories. No-op here.
}
}

View File

@@ -0,0 +1,28 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.kidnapper.components.ICampHost;
import net.minecraft.util.RandomSource;
/**
* Host implementation for CampManager callbacks.
* Phase 1.4: Provides random source and name for camp assignment.
*/
public class CampHost implements ICampHost {
private final EntityKidnapper entity;
public CampHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public RandomSource getRandom() {
return entity.getRandom();
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
}

View File

@@ -0,0 +1,123 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.dialogue.EntityDialogueManager;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.entities.kidnapper.components.ICaptiveHost;
import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem;
import com.tiedup.remake.entities.kidnapper.components.KidnapperAlertManager;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
/**
* Host implementation for KidnapperCaptiveManager callbacks.
* Provides access to entity properties needed for captive management.
*/
public class CaptiveHost implements ICaptiveHost {
private final EntityKidnapper entity;
public CaptiveHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public Level level() {
return entity.level();
}
@Override
public UUID getUUID() {
return entity.getUUID();
}
@Override
public Vec3 position() {
return entity.position();
}
@Override
public BlockPos blockPosition() {
return entity.blockPosition();
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
@Override
public <T> void setEntityData(EntityDataAccessor<T> accessor, T value) {
entity.getEntityData().set(accessor, value);
}
@Override
public boolean isTiedUp() {
return entity.isTiedUp();
}
@Override
public KidnapperState getCurrentState() {
return entity.getCurrentState();
}
@Override
public void setCurrentState(KidnapperState state) {
entity.setCurrentState(state);
}
@Override
public void setGetOutState(boolean state) {
entity.setGetOutState(state);
}
@Override
public void talkToPlayersInRadius(
EntityDialogueManager.DialogueCategory category,
int radius
) {
entity.talkToPlayersInRadius(category, radius);
}
@Override
public KidnapperAggressionSystem getAggressionSystem() {
return entity.getAggressionSystem();
}
@Override
public KidnapperAlertManager getAlertManager() {
return entity.getAlertManager();
}
@Override
public void broadcastAlert(LivingEntity escapee) {
entity.broadcastAlert(escapee);
}
@Override
public boolean hasLineOfSight(LivingEntity entity) {
return this.entity.hasLineOfSight(entity);
}
@Override
public float distanceTo(LivingEntity entity) {
return this.entity.distanceTo(entity);
}
@Override
@Nullable
public CellDataV2 findCellContainingPrisoner(UUID prisonerId) {
return entity.findCellContainingPrisoner(prisonerId);
}
@Override
public LivingEntity asLivingEntity() {
return entity;
}
}

View File

@@ -0,0 +1,62 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.ai.kidnapper.KidnapperState;
import com.tiedup.remake.entities.kidnapper.components.ICellHost;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.level.Level;
/**
* Host implementation for KidnapperCellManager callbacks.
* Provides access to entity properties needed for cell management.
*/
public class CellHost implements ICellHost {
private final EntityKidnapper entity;
public CellHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public Level level() {
return entity.level();
}
@Override
public BlockPos blockPosition() {
return entity.blockPosition();
}
@Override
@Nullable
public UUID getAssociatedStructure() {
return entity.getAssociatedStructure();
}
@Override
public PathNavigation getNavigation() {
return entity.getNavigation();
}
@Override
public void setCurrentState(KidnapperState state) {
entity.setCurrentState(state);
}
@Override
public void talkToPlayersInRadius(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius
) {
entity.talkToPlayersInRadius(category, radius);
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
}

View File

@@ -0,0 +1,90 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.KidnapperJobManager;
import com.tiedup.remake.entities.kidnapper.components.*;
import net.minecraft.network.syncher.EntityDataAccessor;
/**
* Host implementation for KidnapperDataSerializer callbacks.
* Provides access to entity data and components for NBT persistence.
*/
public class DataSerializerHost implements IDataSerializerHost {
private final EntityKidnapper entity;
public DataSerializerHost(EntityKidnapper entity) {
this.entity = entity;
}
// ========================================
// ENTITY ACCESS
// ========================================
@Override
public EntityKidnapper getEntity() {
return entity;
}
// ========================================
// ENTITY DATA ACCESS
// ========================================
@Override
public <T> T getEntityData(EntityDataAccessor<T> accessor) {
return entity.getEntityData().get(accessor);
}
@Override
public <T> void setEntityData(EntityDataAccessor<T> accessor, T value) {
entity.getEntityData().set(accessor, value);
}
// ========================================
// STATE FLAGS
// ========================================
@Override
public boolean isGetOutState() {
return entity.isGetOutState();
}
@Override
public void setGetOutState(boolean state) {
entity.setGetOutState(state);
}
// ========================================
// COMPONENT ACCESS
// ========================================
@Override
public KidnapperAppearance getAppearance() {
return entity.getAppearanceComponent();
}
@Override
public KidnapperAggressionSystem getAggressionSystem() {
return entity.getAggressionSystem();
}
@Override
public KidnapperCaptiveManager getCaptiveManager() {
return entity.getCaptiveManagerComponent();
}
@Override
public KidnapperStateManager getStateManager() {
return entity.getStateManagerComponent();
}
@Override
public KidnapperCampManager getCampManager() {
return entity.getCampManagerComponent();
}
@Override
public KidnapperJobManager getJobManager() {
return entity.getJobManager();
}
}

View File

@@ -0,0 +1,56 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.kidnapper.components.ISaleHost;
import com.tiedup.remake.state.IRestrainable;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.level.Level;
/**
* Host implementation for KidnapperSaleManager callbacks.
* Provides access to entity properties needed for sale management.
*/
public class SaleHost implements ISaleHost {
private final EntityKidnapper entity;
public SaleHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public Level level() {
return entity.level();
}
@Override
public boolean hasCaptives() {
return entity.hasCaptives();
}
@Override
@Nullable
public IRestrainable getCaptive() {
return entity.getCaptive();
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
@Override
public void setGetOutState(boolean state) {
entity.setGetOutState(state);
}
@Override
public boolean getAllowCaptiveTransferFlag() {
return entity.getAllowCaptiveTransferFlag();
}
@Override
public void setAllowCaptiveTransferFlag(boolean flag) {
entity.setAllowCaptiveTransferFlag(flag);
}
}

View File

@@ -0,0 +1,22 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.kidnapper.components.IStateHost;
/**
* Host implementation for StateManager callbacks.
* Phase 1.1: Simple host providing name access for logging.
*/
public class StateHost implements IStateHost {
private final EntityKidnapper entity;
public StateHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public String getNpcName() {
return entity.getNpcName();
}
}

View File

@@ -0,0 +1,103 @@
package com.tiedup.remake.entities.kidnapper.hosts;
import com.tiedup.remake.entities.EntityKidnapper;
import com.tiedup.remake.entities.KidnapperCollarConfig;
import com.tiedup.remake.entities.KidnapperJobManager;
import com.tiedup.remake.entities.kidnapper.components.ITargetHost;
import com.tiedup.remake.entities.kidnapper.components.KidnapperAggressionSystem;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.navigation.PathNavigation;
import net.minecraft.world.entity.ai.sensing.Sensing;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
/**
* Host implementation for KidnapperTargetSelector callbacks.
* Provides access to entity properties needed for target selection and validation.
*/
public class TargetHost implements ITargetHost {
private final EntityKidnapper entity;
public TargetHost(EntityKidnapper entity) {
this.entity = entity;
}
@Override
public Level level() {
return entity.level();
}
@Override
public Sensing getSensing() {
return entity.getSensing();
}
@Override
public AABB getBoundingBox() {
return entity.getBoundingBox();
}
@Override
public float distanceTo(LivingEntity other) {
return entity.distanceTo(other);
}
@Override
public UUID getUUID() {
return entity.getUUID();
}
@Override
public boolean isTiedUp() {
return entity.isTiedUp();
}
@Override
public boolean hasCaptives() {
return entity.hasCaptives();
}
@Override
public boolean isWaitingForJobToBeCompleted() {
return entity.isWaitingForJobToBeCompleted();
}
@Override
public boolean isGetOutState() {
return entity.isGetOutState();
}
@Override
public boolean isKidnappingModeEnabled() {
return entity.isKidnappingModeEnabled();
}
@Override
@Nullable
public UUID getAssociatedStructure() {
return entity.getAssociatedStructure();
}
@Override
public PathNavigation getNavigation() {
return entity.getNavigation();
}
@Override
public KidnapperAggressionSystem getAggressionSystem() {
return entity.getAggressionSystem();
}
@Override
public KidnapperJobManager getJobManager() {
return entity.getJobManager();
}
@Override
public KidnapperCollarConfig getCollarConfig() {
return entity.getCollarConfig();
}
}