Files
TiedUp-/src/main/java/com/tiedup/remake/entities/EntityDamsel.java
NotEvil f4aa5ffdc5 split PrisonerService + decompose EntityKidnapper
PrisonerService 1057L -> 474L lifecycle + 616L EscapeMonitorService
EntityKidnapper 2035L -> 1727L via LootManager, Dialogue, CaptivePriority extraction
2026-04-16 14:08:52 +02:00

834 lines
24 KiB
Java

package com.tiedup.remake.entities;
import com.tiedup.remake.core.SettingsAccessor;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.damsel.components.*;
import com.tiedup.remake.entities.skins.DamselSkinManager;
import com.tiedup.remake.entities.skins.Gender;
import com.tiedup.remake.v2.bondage.CollarHelper;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.*;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
/**
* EntityDamsel - Capturable female NPC with full bondage support.
*
* All shared NPC functionality (bondage, equipment, animation, pose, name, gender)
* has been extracted to AbstractTiedUpNpc.
*
* <p><b>Damsel-specific features (remain here):</b></p>
* <ul>
* <li>DamselAppearance (variant/skin system)</li>
* <li>DamselPersonalitySystem (personality, mood, training, commands)</li>
* <li>DamselAIController (damsel-specific AI goals)</li>
* <li>DamselDialogueHandler (dialogue cooldowns)</li>
* <li>DamselRewardTracker (savior/reward system)</li>
* <li>MenuProvider (inventory GUI)</li>
* <li>Personality-related IDialogueSpeaker methods</li>
* </ul>
*/
public class EntityDamsel
extends AbstractTiedUpNpc
implements net.minecraft.world.MenuProvider
{
// DAMSEL-SPECIFIC DATA SYNC
/**
* Personality type name (e.g., "TIMID", "FIERCE").
* Synced to client for UI display.
*/
public static final EntityDataAccessor<String> DATA_PERSONALITY_TYPE =
SynchedEntityData.defineId(
EntityDamsel.class,
EntityDataSerializers.STRING
);
/**
* Current active command (e.g., "FOLLOW", "STAY", "NONE").
* Synced to client for UI display.
*/
public static final EntityDataAccessor<String> DATA_ACTIVE_COMMAND =
SynchedEntityData.defineId(
EntityDamsel.class,
EntityDataSerializers.STRING
);
// DAMSEL-SPECIFIC COMPONENTS
/**
* Manages visual appearance (variant, skin, gender, name, slim arms).
*/
private final DamselAppearance appearance;
/**
* Manages all personality-related systems.
*/
private DamselPersonalitySystem personalitySystem;
/**
* Manages all AI-related systems.
*/
private DamselAIController aiController;
/**
* Manages all dialogue-related systems.
*/
private DamselDialogueHandler dialogueHandler;
/**
* Orchestrates NBT serialization for all components.
*/
private DamselDataSerializer serializer;
/** Tracks savior and reward state */
private final DamselRewardTracker rewardTracker = new DamselRewardTracker(
this
);
// CONSTRUCTOR
@Override
protected IBondageHost createBondageHost() {
return new com.tiedup.remake.entities.damsel.hosts.BondageHost(this);
}
public EntityDamsel(EntityType<? extends EntityDamsel> type, Level level) {
super(type, level);
this.appearance = new DamselAppearance(this);
this.personalitySystem = new DamselPersonalitySystem(
this,
new com.tiedup.remake.entities.damsel.hosts.PersonalityTickContextHost(
this
)
);
this.aiController = new DamselAIController(
this,
new com.tiedup.remake.entities.damsel.hosts.AIHost(this)
);
// (already done in AbstractTiedUpNpc constructor)
this.dialogueHandler = new DamselDialogueHandler(
new com.tiedup.remake.entities.damsel.hosts.DialogueHost(this)
);
this.serializer = new DamselDataSerializer(this);
// CRITICAL: Register AI goals now that aiController is initialized
// (registerGoals() was called by Mob constructor but skipped due to null check)
this.registerGoals();
}
// ENTITY INITIALIZATION
@Override
protected void defineSynchedData() {
super.defineSynchedData();
// Damsel-specific personality data
this.entityData.define(DATA_PERSONALITY_TYPE, "UNKNOWN");
this.entityData.define(DATA_ACTIVE_COMMAND, "NONE");
}
@Override
public void onSyncedDataUpdated(EntityDataAccessor<?> key) {
super.onSyncedDataUpdated(key);
if (DATA_VARIANT_ID.equals(key)) {
this.appearance.invalidateVariantCache();
}
}
@Override
public void onAddedToWorld() {
super.onAddedToWorld();
if (
!this.level().isClientSide &&
this.appearance.getVariantId().isEmpty()
) {
Gender preferredGender = SettingsAccessor.getPreferredSpawnGender(
this.level().getGameRules()
);
DamselVariant variant = DamselSkinManager.CORE.getVariantForEntity(
this.getUUID(),
preferredGender
);
this.setVariant(variant);
TiedUpMod.LOGGER.debug(
"[EntityDamsel] Spawned with variant: {}",
variant.id()
);
}
// Server-side only: Initialize personality if not already set (new spawn)
if (
!this.level().isClientSide &&
this.personalitySystem.getPersonalityState() == null
) {
this.personalitySystem.initializePersonality();
}
}
/**
* Register all AI goals for this entity.
*
* CRITICAL: This method is called by Mob constructor BEFORE our constructor finishes.
* We must null-check aiController and defer goal registration until after initialization.
*/
@Override
protected void registerGoals() {
if (aiController == null) {
// Called during super() construction, before aiController is initialized
// Goals will be registered manually after component initialization
return;
}
aiController.registerGoals(this.goalSelector, this.targetSelector);
}
/**
* Not used directly -- registerGoals() delegates to aiController.
* Required by AbstractTiedUpNpc contract.
*/
@Override
protected void registerNpcGoals() {
// Handled by registerGoals() override above
}
/**
* Create attribute modifiers for EntityDamsel.
* Called during entity type registration.
*/
public static AttributeSupplier.Builder createAttributes() {
return Mob.createMobAttributes()
.add(Attributes.MAX_HEALTH, 20.0)
.add(Attributes.MOVEMENT_SPEED, 0.25)
.add(Attributes.KNOCKBACK_RESISTANCE, 0.7)
.add(Attributes.FOLLOW_RANGE, 60.0);
}
// TICK (damsel-specific extensions)
/**
* Damsel-specific tick logic.
* Called by AbstractTiedUpNpc.tick() after animation and bondage restoration.
*/
@Override
protected void tickSubclass() {
aiController.tickCallForHelp();
aiController.tickLeashTraction();
personalitySystem.tickPersonality();
personalitySystem.tickIdleDialogue();
personalitySystem.tickApproachDetection();
personalitySystem.tickEnvironmentDialogue();
}
// VARIANT SYSTEM (Delegated to DamselAppearance)
@Nullable
public DamselVariant getVariant() {
return this.appearance.getVariant();
}
public void setVariant(DamselVariant variant) {
this.appearance.setVariant(variant);
}
public String getVariantId() {
return this.appearance.getVariantId();
}
// SKIN TEXTURE (ISkinnedEntity implementation)
@Override
public ResourceLocation getSkinTexture() {
return this.appearance.getSkinTexture();
}
// NAME SYSTEM OVERRIDE
// (DamselAppearance also sets custom name visible, delegate to it)
@Override
public String getNpcName() {
return this.appearance.getNpcName();
}
@Override
public void setNpcName(String name) {
this.appearance.setNpcName(name);
}
// GENDER/SLIMARMS OVERRIDE
// (DamselAppearance manages these for damsels)
@Override
public boolean hasSlimArms() {
return this.appearance.hasSlimArms();
}
@Override
public void setGender(Gender gender) {
this.appearance.setGender(gender);
}
@Override
public Gender getGender() {
return this.appearance.getGender();
}
// COMPONENT ACCESSORS
public DamselAppearance getAppearance() {
return appearance;
}
public DamselPersonalitySystem getPersonalitySystem() {
return personalitySystem;
}
public DamselRewardTracker getRewardTracker() {
return rewardTracker;
}
// NBT PERSISTENCE (damsel-specific)
@Override
public void addAdditionalSaveData(CompoundTag tag) {
super.addAdditionalSaveData(tag);
serializer.save(tag);
}
@Override
public void readAdditionalSaveData(CompoundTag tag) {
super.readAdditionalSaveData(tag);
serializer.load(tag);
}
// BONDAGE SERVICE OVERRIDES
/**
* Override hurt to intercept player attacks for bondage service.
*/
@Override
public boolean hurt(DamageSource source, float amount) {
if (
!this.level().isClientSide &&
getBondageManager().handleDamageWithService(source, amount)
) {
return false;
}
return super.hurt(source, amount);
}
/**
* Override display name to show violet color when bondage service is active.
*/
@Override
public Component getDisplayName() {
if (getBondageManager().isBondageServiceEnabled()) {
return Component.literal(this.getNpcName()).withStyle(
ChatFormatting.LIGHT_PURPLE
);
}
return super.getDisplayName();
}
// DIALOGUE SYSTEM
public void talkTo(Player player, String message) {
com.tiedup.remake.dialogue.EntityDialogueManager.talkTo(
this,
player,
message
);
}
public void talkTo(
Player player,
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category
) {
com.tiedup.remake.dialogue.EntityDialogueManager.talkTo(
this,
player,
category
);
}
public void actionTo(Player player, String action) {
com.tiedup.remake.dialogue.EntityDialogueManager.actionTo(
this,
player,
action
);
}
public void actionTo(
Player player,
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category
) {
com.tiedup.remake.dialogue.EntityDialogueManager.actionTo(
this,
player,
category
);
}
public void talkToPlayersInRadius(String message, int radius) {
com.tiedup.remake.dialogue.EntityDialogueManager.talkToNearby(
this,
message,
radius
);
}
public void talkToPlayersInRadius(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius
) {
com.tiedup.remake.dialogue.EntityDialogueManager.talkToNearby(
this,
category,
radius
);
}
public boolean talkToPlayersInRadiusWithCooldown(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius
) {
return dialogueHandler.talkToPlayersInRadiusWithCooldown(
category,
radius
);
}
public void actionToPlayersInRadius(String action, int radius) {
com.tiedup.remake.dialogue.EntityDialogueManager.actionToNearby(
this,
action,
radius
);
}
public void actionToPlayersInRadius(
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
int radius
) {
com.tiedup.remake.dialogue.EntityDialogueManager.actionToNearby(
this,
category,
radius
);
}
// SAVIOR & REWARD SYSTEM
public void setSavior(@Nullable Player player) {
rewardTracker.setSavior(player);
}
public boolean isSavior(Entity entity) {
return rewardTracker.isSavior(entity);
}
@Nullable
public UUID getSaviorUUID() {
return rewardTracker.getSaviorUUID();
}
public void setTiedBy(@Nullable Player player) {
rewardTracker.setTiedBy(player);
}
@Nullable
public UUID getTiedByUUID() {
return rewardTracker.getTiedByUUID();
}
public boolean hasGivenReward() {
return rewardTracker.hasGivenReward();
}
public void resetRewardState() {
rewardTracker.reset();
}
public void rewardSavior(Player savior) {
rewardTracker.rewardSavior(savior);
}
// PERSONALITY SYSTEM ACCESS
@Nullable
public com.tiedup.remake.personality.PersonalityState getPersonalityState() {
if (
this.personalitySystem.getPersonalityState() == null &&
!this.level().isClientSide
) {
personalitySystem.initializePersonality();
}
return this.personalitySystem.getPersonalityState();
}
public com.tiedup.remake.personality.PersonalityType getPersonalityType() {
return personalitySystem.getPersonalityType();
}
public void setPersonalityType(
com.tiedup.remake.personality.PersonalityType newType
) {
personalitySystem.setPersonalityType(newType);
}
public com.tiedup.remake.personality.NpcCommand getActiveCommand() {
return personalitySystem.getActiveCommand();
}
// ANTI-FLEE SYSTEM
public long getLastWhipTime() {
return personalitySystem.getLastWhipTime();
}
public void setLastWhipTime(long time) {
personalitySystem.setLastWhipTime(time);
}
// COMMAND SYSTEM
public boolean giveCommand(
Player commander,
com.tiedup.remake.personality.NpcCommand command,
@Nullable net.minecraft.core.BlockPos targetPos
) {
if (!this.hasCollar()) return false;
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (!CollarHelper.isCollar(collar)) return false;
if (!CollarHelper.isOwner(collar, commander.getUUID())) {
if (!this.isGagged()) {
com.tiedup.remake.dialogue.EntityDialogueManager.talkByDialogueId(
this,
commander,
"command.reject.not_master"
);
}
return false;
}
if (this.personalitySystem.getPersonalityState() != null) {
if (
!this.personalitySystem.getPersonalityState().willObeyCommand(
commander,
command
)
) {
return false;
}
this.personalitySystem.getPersonalityState().setActiveCommand(
command,
commander.getUUID(),
targetPos
);
personalitySystem.syncPersonalityData();
return true;
}
return false;
}
public boolean giveCommandWithTwoTargets(
Player commander,
com.tiedup.remake.personality.NpcCommand command,
@Nullable net.minecraft.core.BlockPos targetPos,
@Nullable net.minecraft.core.BlockPos targetPos2
) {
boolean success = giveCommand(commander, command, targetPos);
if (
success &&
targetPos2 != null &&
this.personalitySystem.getPersonalityState() != null
) {
this.personalitySystem.getPersonalityState().setCommandTarget2(
targetPos2
);
personalitySystem.syncPersonalityData();
}
return success;
}
public void cancelCommand() {
if (this.personalitySystem.getPersonalityState() != null) {
this.personalitySystem.getPersonalityState().clearCommand();
personalitySystem.syncPersonalityData();
}
}
// NPC INVENTORY ACCESS
public net.minecraft.core.NonNullList<ItemStack> getNpcInventory() {
return this.getInventoryManager().getNpcInventory();
}
public int getNpcInventorySize() {
return this.getInventoryManager().getNpcInventorySize();
}
public void setNpcInventorySize(int newSize) {
this.getInventoryManager().setNpcInventorySize(newSize);
}
public boolean hasEdibleInInventory() {
return this.getInventoryManager().hasEdibleInInventory();
}
public boolean tryEatFromInventory() {
return this.getInventoryManager().tryEatFromInventory(
this.personalitySystem.getPersonalityState()
);
}
public boolean tryEatFromChest(net.minecraft.core.BlockPos chestPos) {
return this.getInventoryManager().tryEatFromChest(
chestPos,
this.personalitySystem.getPersonalityState()
);
}
// DIRECT FEEDING SYSTEM
public boolean feedByPlayer(Player player, ItemStack foodStack) {
return this.getInventoryManager().feedByPlayer(
player,
foodStack,
this.personalitySystem.getPersonalityState()
);
}
// INTERACTION
@Override
protected net.minecraft.world.InteractionResult mobInteract(
Player player,
net.minecraft.world.InteractionHand hand
) {
ItemStack heldItem = player.getItemInHand(hand);
// Check if holding edible item
if (heldItem.getItem().isEdible()) {
if (!this.hasCollar()) {
if (
player instanceof net.minecraft.server.level.ServerPlayer sp
) {
sp.displayClientMessage(
Component.translatable(
"entity.tiedup.damsel.needs_collar_to_feed"
).withStyle(ChatFormatting.RED),
true
);
}
return net.minecraft.world.InteractionResult.FAIL;
}
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (CollarHelper.isCollar(collar)) {
if (!CollarHelper.isOwner(collar, player)) {
if (
player instanceof
net.minecraft.server.level.ServerPlayer sp
) {
sp.displayClientMessage(
Component.translatable(
"entity.tiedup.damsel.not_collar_owner"
).withStyle(ChatFormatting.RED),
true
);
}
return net.minecraft.world.InteractionResult.FAIL;
}
if (this.feedByPlayer(player, heldItem)) {
return net.minecraft.world.InteractionResult.SUCCESS;
}
if (
player instanceof net.minecraft.server.level.ServerPlayer sp
) {
sp.displayClientMessage(
Component.translatable(
"entity.tiedup.damsel.cant_eat_now"
).withStyle(ChatFormatting.RED),
true
);
}
return net.minecraft.world.InteractionResult.FAIL;
}
}
// Shift + empty hand on collared NPC = open conversation
if (
!this.level().isClientSide() &&
player.isShiftKeyDown() &&
heldItem.isEmpty() &&
this.hasCollar()
) {
ItemStack collar = this.getEquipment(BodyRegionV2.NECK);
if (CollarHelper.isCollar(collar)) {
if (
CollarHelper.isOwner(collar, player) &&
player instanceof
net.minecraft.server.level.ServerPlayer serverPlayer
) {
if (
com.tiedup.remake.dialogue.conversation.ConversationManager.openConversation(
this,
serverPlayer
)
) {
return net.minecraft.world.InteractionResult.SUCCESS;
}
}
}
}
// Leash detach: if player right-clicks their leashed damsel, detach leash
if (
!this.level().isClientSide() &&
this.isLeashed() &&
this.getLeashHolder() == player
) {
this.dropLeash(true, !player.getAbilities().instabuild);
ICaptor currentCaptor = getBondageManager().getCaptor();
if (currentCaptor != null) {
currentCaptor.removeCaptive(this, false);
getBondageManager().clearCaptor();
}
return net.minecraft.world.InteractionResult.SUCCESS;
}
return super.mobInteract(player, hand);
}
// MENU PROVIDER
@Override
@javax.annotation.Nullable
public net.minecraft.world.inventory.AbstractContainerMenu createMenu(
int containerId,
net.minecraft.world.entity.player.Inventory playerInventory,
Player player
) {
return this.getInventoryManager().createMenu(
containerId,
playerInventory,
player
);
}
// LIFECYCLE - DEATH
@Override
public void die(DamageSource damageSource) {
if (
!this.level().isClientSide &&
this.level() instanceof
net.minecraft.server.level.ServerLevel serverLevel
) {
UUID uuid = this.getUUID();
com.tiedup.remake.cells.CellRegistryV2 cellRegistry =
com.tiedup.remake.cells.CellRegistryV2.get(serverLevel);
cellRegistry.releasePrisonerFromAllCells(uuid);
com.tiedup.remake.prison.PrisonerManager manager =
com.tiedup.remake.prison.PrisonerManager.get(serverLevel);
com.tiedup.remake.prison.PrisonerState state = manager.getState(
uuid
);
if (
state == com.tiedup.remake.prison.PrisonerState.IMPRISONED ||
state == com.tiedup.remake.prison.PrisonerState.WORKING
) {
com.tiedup.remake.prison.service.EscapeMonitorService.get().escape(
serverLevel,
uuid,
"player_death"
);
}
TiedUpMod.LOGGER.debug(
"[EntityDamsel] {} died, cleaned up registries",
getNpcName()
);
}
super.die(damageSource);
}
// IDIALOGUESPEAKER - Personality-specific methods
@Override
public com.tiedup.remake.dialogue.SpeakerType getSpeakerType() {
return com.tiedup.remake.dialogue.SpeakerType.DAMSEL;
}
@Override
@Nullable
public com.tiedup.remake.personality.PersonalityType getSpeakerPersonality() {
if (
personalitySystem != null &&
personalitySystem.getPersonalityState() != null
) {
return personalitySystem.getPersonalityState().getPersonality();
}
return null;
}
@Override
public int getSpeakerMood() {
if (
personalitySystem != null &&
personalitySystem.getPersonalityState() != null
) {
return (int) personalitySystem.getPersonalityState().getMood();
}
return 50;
}
@Override
@Nullable
public String getTargetRelation(Player player) {
if (hasCollar()) {
ItemStack collar = getEquipment(BodyRegionV2.NECK);
if (CollarHelper.isCollar(collar)) {
if (CollarHelper.isOwner(collar, player)) {
return "master";
}
}
}
return null;
}
}