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. * *

Damsel-specific features (remain here):

* */ 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 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 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 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 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; } }