package com.tiedup.remake.entities; import com.tiedup.remake.core.SettingsAccessor; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.dialogue.SpeakerType; import com.tiedup.remake.entities.ai.master.*; import com.tiedup.remake.entities.master.components.IMasterStateHost; import com.tiedup.remake.entities.master.components.MasterPetManager; import com.tiedup.remake.entities.master.components.MasterStateManager; import com.tiedup.remake.entities.master.components.MasterTaskManager; import com.tiedup.remake.entities.skins.Gender; import com.tiedup.remake.entities.skins.MasterSkinManager; import com.tiedup.remake.minigame.ContinuousStruggleMiniGameState; import com.tiedup.remake.minigame.StruggleSessionManager; import com.tiedup.remake.network.ModNetwork; import com.tiedup.remake.network.master.PacketMasterStateSync; import com.tiedup.remake.state.PlayerBindState; import com.tiedup.remake.v2.BodyRegionV2; import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper; import java.util.UUID; import javax.annotation.Nullable; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Style; import net.minecraft.network.syncher.EntityDataAccessor; import net.minecraft.network.syncher.EntityDataSerializers; import net.minecraft.network.syncher.SynchedEntityData; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.goal.FloatGoal; import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; /** * EntityMaster - Rare NPC that buys solo players from Kidnappers. * * Master NPCs implement the pet play system: * - Buys solo players from Kidnappers (100% chance when no buyers) * - Follows the player (inverted follower mechanic) * - Restricts eating/sleeping to special blocks (Bowl/Pet Bed) * - Monitors player for escape attempts * - Punishes detected struggle attempts * - Has distraction windows where escape is possible * * Key Differences from Kidnapper: * - Does not actively hunt players * - Follows the player instead of other way around * - Much higher combat stats (100+ HP, damage reduction from slave) * - Cannot be easily defeated by the enslaved player */ public class EntityMaster extends EntityKidnapperElite { // CONSTANTS public static final double MASTER_MAX_HEALTH = 150.0D; public static final double MASTER_MOVEMENT_SPEED = 0.30D; public static final double MASTER_KNOCKBACK_RESISTANCE = 0.95D; public static final double MASTER_FOLLOW_RANGE = 60.0D; public static final double MASTER_ATTACK_DAMAGE = 10.0D; public static final double MASTER_ARMOR = 10.0D; /** Damage reduction from slave attacks (75% reduction) */ public static final float SLAVE_DAMAGE_MULTIPLIER = 0.25f; /** Regeneration rate (HP per tick, 0.5 HP/sec = 0.025 HP/tick) */ public static final float REGEN_PER_TICK = 0.025f; /** Name color (dark purple) */ public static final int MASTER_NAME_COLOR = 0x8B008B; /** NBT keys */ private static final String NBT_MASTER_STATE = "MasterState"; private static final String NBT_PET_UUID = "PetPlayerUUID"; private static final String NBT_MASTER_VARIANT = "MasterVariantId"; private static final String NBT_PLACED_BLOCK_POS = "PlacedBlockPos"; private static final String NBT_DOGWALK_MASTER_LEADS = "DogwalkMasterLeads"; // DATA SYNC /** Current state synced to client */ private static final EntityDataAccessor DATA_MASTER_STATE = SynchedEntityData.defineId( EntityMaster.class, EntityDataSerializers.STRING ); /** Whether master is distracted */ private static final EntityDataAccessor DATA_DISTRACTED = SynchedEntityData.defineId( EntityMaster.class, EntityDataSerializers.BOOLEAN ); // COMPONENTS /** State manager for master behavioral states */ private final MasterStateManager stateManager; /** Pet manager for pet player lifecycle (collar, leash, freedom) */ private final MasterPetManager petManager; /** Task manager for pet tasks, engagement cadence, and cold shoulder */ private final MasterTaskManager taskManager; /** The kidnapper that is selling the player (for approach/buy logic) */ @Nullable private EntityKidnapper sellingKidnapper; /** Reference to the place block goal for triggering feeding/resting */ @Nullable private MasterPlaceBlockGoal placeBlockGoal; /** Reference to the dogwalk goal */ @Nullable private MasterDogwalkGoal dogwalkGoal; /** Whether master leads during dogwalk (true = master pulls, false = master follows) */ private boolean dogwalkMasterLeads = false; /** Reference to task assign goal for force assignment */ @Nullable private MasterTaskAssignGoal taskAssignGoal; /** Reference to task watch goal */ @Nullable private MasterTaskWatchGoal taskWatchGoal; /** Forced punishment type for next PUNISH cycle (null = random selection) */ @Nullable private PunishmentType pendingForcedPunishment = null; /** Whether the next punishment is an attack punishment (dual: choke + physical restraint) */ private boolean pendingAttackPunishment = false; // CONSTRUCTOR public EntityMaster(EntityType type, Level level) { super(type, level); // Initialize state manager this.stateManager = new MasterStateManager(new MasterStateHost()); // Initialize pet manager (depends on stateManager) this.petManager = new MasterPetManager(this, this.stateManager); // Initialize task manager this.taskManager = new MasterTaskManager(this); } // ATTRIBUTES /** * Create master attributes. * Very tanky - designed to be nearly impossible for slave to defeat. */ public static AttributeSupplier.Builder createAttributes() { return Mob.createMobAttributes() .add(Attributes.MAX_HEALTH, MASTER_MAX_HEALTH) .add(Attributes.MOVEMENT_SPEED, MASTER_MOVEMENT_SPEED) .add(Attributes.KNOCKBACK_RESISTANCE, MASTER_KNOCKBACK_RESISTANCE) .add(Attributes.FOLLOW_RANGE, MASTER_FOLLOW_RANGE) .add(Attributes.ATTACK_DAMAGE, MASTER_ATTACK_DAMAGE) .add(Attributes.ARMOR, MASTER_ARMOR); } // DATA SYNC @Override protected void defineSynchedData() { super.defineSynchedData(); this.entityData.define(DATA_MASTER_STATE, MasterState.IDLE.name()); this.entityData.define(DATA_DISTRACTED, false); } /** * Sync master state to all tracking clients. */ private void syncState() { this.entityData.set( DATA_MASTER_STATE, stateManager.getCurrentState().name() ); this.entityData.set( DATA_DISTRACTED, stateManager.getCurrentState() == MasterState.DISTRACTED ); // Send network packet for detailed sync if ( !this.level().isClientSide && this.level() instanceof ServerLevel serverLevel ) { ModNetwork.sendToTracking( new PacketMasterStateSync( this.getId(), stateManager.getCurrentState().ordinal(), stateManager.getPetPlayerUUID(), stateManager.getRemainingDistractionTicks() ), this ); } } /** * Get master state from synced data (client-side). */ public MasterState getMasterState() { try { return MasterState.valueOf(this.entityData.get(DATA_MASTER_STATE)); } catch (IllegalArgumentException e) { return MasterState.IDLE; } } /** * Check if master is distracted (client-safe). */ public boolean isDistracted() { return this.entityData.get(DATA_DISTRACTED); } // SELLING KIDNAPPER (for approach/purchase) /** * Set the kidnapper that is selling a player to this Master. * Called when Master spawns to approach a sale. */ public void setSellingKidnapper(@Nullable EntityKidnapper kidnapper) { this.sellingKidnapper = kidnapper; if (kidnapper != null) { // Set state to purchasing - Master will approach this.stateManager.setCurrentState(MasterState.PURCHASING); TiedUpMod.LOGGER.info( "[EntityMaster] {} set to purchase from {}", this.getNpcName(), kidnapper.getNpcName() ); } } /** * Get the kidnapper that is selling a player. */ @Nullable public EntityKidnapper getSellingKidnapper() { return sellingKidnapper; } /** * Check if this Master has a valid selling kidnapper. */ public boolean hasSellingKidnapper() { return sellingKidnapper != null && sellingKidnapper.isAlive(); } /** * Clear the selling kidnapper reference (after purchase complete). */ public void clearSellingKidnapper() { this.sellingKidnapper = null; } // AI GOALS @Override protected void registerGoals() { // Float goal always highest priority this.goalSelector.addGoal(0, new FloatGoal(this)); // Priority 1: Hunt monsters near pet (protect the pet!) this.goalSelector.addGoal(1, new MasterHuntMonstersGoal(this)); // Priority 1: Buy player from kidnapper (initial state) this.goalSelector.addGoal(1, new MasterBuyPlayerGoal(this)); // Priority 2: Punish + Place block (urgent reactions) this.goalSelector.addGoal(2, new MasterPunishGoal(this)); this.placeBlockGoal = new MasterPlaceBlockGoal(this); this.goalSelector.addGoal(2, this.placeBlockGoal); // Priority 3: Dogwalk + Human Chair (exclusive movement modes) this.dogwalkGoal = new MasterDogwalkGoal(this); this.goalSelector.addGoal(3, this.dogwalkGoal); this.goalSelector.addGoal(3, new MasterHumanChairGoal(this)); // Priority 4: Inventory inspection (short burst, higher than observe) this.goalSelector.addGoal(4, new MasterInventoryInspectGoal(this)); // Priority 5: Observe player (watching state) this.goalSelector.addGoal(5, new MasterObservePlayerGoal(this)); // Priority 6: Task assign, task watch, random events (engagement goals) this.taskAssignGoal = new MasterTaskAssignGoal(this); this.goalSelector.addGoal(6, this.taskAssignGoal); this.taskWatchGoal = new MasterTaskWatchGoal(this); this.goalSelector.addGoal(6, this.taskWatchGoal); this.goalSelector.addGoal(6, new MasterRandomEventGoal(this)); // Priority 7: Idle behaviors (micro-actions between engagements) this.goalSelector.addGoal(7, new MasterIdleBehaviorGoal(this)); // Priority 8: Follow player - DEFAULT behavior, lowest master priority // Must be below all engagement goals so they can interrupt following. // FollowPlayer (MOVE+LOOK) at prio 3 was blocking all prio 4-7 goals! this.goalSelector.addGoal(8, new MasterFollowPlayerGoal(this)); // These were non-functional before (Masters have no PersonalityState). // Proper fix: Create MasterCommandGoals that use collar owner instead. // DamselAIController.registerCommandGoals(this.goalSelector, this, 9); // Look goals - lowest priority this.goalSelector.addGoal( 10, new LookAtPlayerGoal(this, Player.class, 8.0F) ); this.goalSelector.addGoal(11, new RandomLookAroundGoal(this)); // Note: No target goals - Master doesn't hunt, only follows their pet } // VARIANT SYSTEM OVERRIDES @Override public KidnapperVariant lookupVariantById(String variantId) { return MasterSkinManager.CORE.getVariant(variantId); } @Override public KidnapperVariant computeVariantForEntity(UUID entityUUID) { Gender preferredGender = SettingsAccessor.getPreferredSpawnGender( this.level() != null ? this.level().getGameRules() : null ); return MasterSkinManager.CORE.getVariantForEntity( entityUUID, preferredGender ); } @Override public String getVariantTextureFolder() { return "textures/entity/master/"; } @Override public String getDefaultVariantId() { return "amy"; } @Override public String getVariantNBTKey() { return NBT_MASTER_VARIANT; } // PLAYER INTERACTION /** * Handle player right-click interaction. * - Pet + Shift + empty hand = open pet request menu * - Pet + empty hand = pet greeting * - Non-pet + Shift + empty hand = open conversation * - Non-pet + empty hand = stranger greeting */ @Override protected net.minecraft.world.InteractionResult mobInteract( Player player, net.minecraft.world.InteractionHand hand ) { // Enslaved: use base NPC behavior (feeding, conversation via EntityDamsel) if (this.isTiedUp()) { return super.mobInteract(player, hand); } ItemStack heldItem = player.getItemInHand(hand); if (this.level().isClientSide()) { return net.minecraft.world.InteractionResult.SUCCESS; } if (!(player instanceof ServerPlayer serverPlayer)) { return net.minecraft.world.InteractionResult.PASS; } boolean isPet = isPetPlayer(player); // Pet interactions if (isPet) { // Cold shoulder - ignore pet interactions if (isGivingColdShoulder()) { return net.minecraft.world.InteractionResult.PASS; } // If holding an item and FETCH_ITEM or DEMAND task is active, try to give it if ( !heldItem.isEmpty() && (taskManager.getCurrentTask() == PetTask.FETCH_ITEM || taskManager.getCurrentTask() == PetTask.DEMAND) ) { if (taskManager.handleFetchItemGive(heldItem.getItem())) { // Take the item from player heldItem.shrink(1); return net.minecraft.world.InteractionResult.SUCCESS; } // Item wasn't the right one - don't consume the interaction } // SPEAK task: pet right-clicks on master to complete if (taskManager.getCurrentTask() == PetTask.SPEAK) { com.tiedup.remake.dialogue.DialogueBridge.talkTo( this, player, "petplay.task_complete" ); taskManager.clearActiveTask(); setMasterState(MasterState.FOLLOWING); return net.minecraft.world.InteractionResult.SUCCESS; } // Empty hand interactions if (heldItem.isEmpty()) { if (player.isShiftKeyDown()) { // Shift + empty hand = open pet request menu com.tiedup.remake.dialogue.conversation.PetRequestManager.openRequestMenu( this, serverPlayer ); return net.minecraft.world.InteractionResult.SUCCESS; } else { // Simple greeting com.tiedup.remake.dialogue.DialogueBridge.talkTo( this, player, "petplay.pet_greeting" ); return net.minecraft.world.InteractionResult.SUCCESS; } } } // Non-pet interactions if (!isPet && heldItem.isEmpty()) { if (player.isShiftKeyDown()) { // Shift + empty hand = open conversation GUI (for non-pets) if ( com.tiedup.remake.dialogue.conversation.ConversationManager.openConversation( this, serverPlayer ) ) { return net.minecraft.world.InteractionResult.SUCCESS; } } else { // Right click = greeting dialogue com.tiedup.remake.dialogue.DialogueBridge.talkTo( this, player, "idle.stranger_greeting" ); return net.minecraft.world.InteractionResult.SUCCESS; } } return super.mobInteract(player, hand); } // DISPLAY @Override public Component getDisplayName() { return Component.literal(this.getNpcName()).withStyle( Style.EMPTY.withColor(MASTER_NAME_COLOR) ); } @Override public SpeakerType getSpeakerType() { return SpeakerType.MASTER; } /** * Get relationship type for dialogue context. * Returns "pet" for the owned player. */ @Override public String getTargetRelation(Player player) { if ( stateManager.getPetPlayerUUID() != null && stateManager.getPetPlayerUUID().equals(player.getUUID()) ) { return "pet"; } return null; } /** * Get mood for dialogue context. * Based on current state and pet behavior. */ @Override public int getSpeakerMood() { int mood = 50; // Neutral MasterState state = stateManager.getCurrentState(); switch (state) { case DISTRACTED -> mood += 20; case PUNISH -> mood -= 30; case OBSERVING -> mood += 10; case FOLLOWING -> mood += 15; default -> { } } return Math.max(0, Math.min(100, mood)); } // TICK @Override public void tick() { super.tick(); if (!this.level().isClientSide) { // Tick distraction system if (stateManager.tickDistraction()) { syncState(); } // Passive regeneration if (this.getHealth() < this.getMaxHealth()) { this.heal(REGEN_PER_TICK); } // Check pet struggle sessions tickStruggleDetection(); // Sync Master data to collar every second (for disconnect persistence) if (this.tickCount % 20 == 0) { ServerPlayer pet = getPetPlayer(); if (pet != null && hasPetCollar(pet)) { syncDataToCollar(pet); } } // FIX: Check for expired temporary event items every 5 seconds if (this.tickCount % 100 == 0) { ServerPlayer pet = getPetPlayer(); if (pet != null) { MasterRandomEventGoal.cleanupExpiredTempItems( pet, this.level().getGameTime() ); } } } } /** * Check if pet is struggling and detect if master is watching. */ private void tickStruggleDetection() { UUID petUUID = stateManager.getPetPlayerUUID(); if (petUUID == null) return; // Get struggle session for pet ContinuousStruggleMiniGameState session = StruggleSessionManager.getInstance().getContinuousStruggleSession( petUUID ); if (session != null && session.getHeldDirection() >= 0) { // Pet is actively struggling ServerPlayer pet = getPetPlayer(); if ( pet != null && stateManager.isWatching() && this.hasLineOfSight(pet) ) { // Detected! Interrupt and punish onStruggleDetected(session); } } } /** * Called when master detects pet struggling. */ private void onStruggleDetected(ContinuousStruggleMiniGameState session) { ServerPlayer pet = getPetPlayer(); if (pet == null) return; TiedUpMod.LOGGER.info( "[EntityMaster] {} detected {} struggling!", this.getNpcName(), pet.getName().getString() ); // Reset collar resistance (repair the lock) resetCollarResistance(pet); // Transition to punish state stateManager.onStruggleAttempt(); syncState(); // Send warning message to pet pet.sendSystemMessage( Component.literal( this.getNpcName() + " caught you trying to escape!" ).withStyle(Style.EMPTY.withColor(MASTER_NAME_COLOR)) ); } /** @see MasterPetManager#resetCollarResistance(ServerPlayer) */ private void resetCollarResistance(ServerPlayer pet) { petManager.resetCollarResistance(pet); } // COMBAT - DAMAGE REDUCTION @Override public boolean hurt(DamageSource source, float amount) { // Check if attacker is the pet boolean attackedByPet = false; ServerPlayer petAttacker = null; if (source.getEntity() instanceof Player player) { if (player.getUUID().equals(stateManager.getPetPlayerUUID())) { attackedByPet = true; petAttacker = (player instanceof ServerPlayer sp) ? sp : null; // Reduce damage from slave attacks amount *= SLAVE_DAMAGE_MULTIPLIER; TiedUpMod.LOGGER.debug( "[EntityMaster] Reduced slave damage: {} -> {}", amount / SLAVE_DAMAGE_MULTIPLIER, amount ); } } // Interrupt interruptible states if attacked if (amount > 0) { stateManager.interruptDistraction(); interruptIfVulnerable(); syncState(); } // PUNISHMENT: If attacked by pet, trigger choke collar and enter PUNISH state if (attackedByPet && petAttacker != null && amount > 0) { triggerPunishmentForAttack(petAttacker); } return super.hurt(source, amount); } /** * Trigger punishment when pet attacks the Master. * Applies dual punishment: choke collar + a physical restraint (bind, blindfold, gag, or leash tug). */ private void triggerPunishmentForAttack(ServerPlayer pet) { // Don't overwrite an active punishment if (stateManager.getCurrentState() == MasterState.PUNISH) { return; } TiedUpMod.LOGGER.info( "[EntityMaster] {} attacked by pet {} - triggering attack punishment", getNpcName(), pet.getName().getString() ); // Dialogue - Master is angry com.tiedup.remake.dialogue.DialogueBridge.talkTo( this, pet, "punishment.attacked" ); // Flag for dual punishment (choke + physical restraint) this.pendingAttackPunishment = true; // Enter PUNISH state setMasterState(MasterState.PUNISH); } /** * Interrupt states where the Master is vulnerable (sitting, walking pet). * Called when the Master takes damage from a non-pet source. * Forces the Master to get up / stop walking and return to FOLLOWING. */ private void interruptIfVulnerable() { MasterState current = stateManager.getCurrentState(); if ( current == MasterState.HUMAN_CHAIR || current == MasterState.DOGWALK ) { TiedUpMod.LOGGER.info( "[EntityMaster] {} interrupted {} due to taking damage", getNpcName(), current ); // State change triggers goal stop() which handles cleanup setMasterState(MasterState.FOLLOWING); } } /** * Called when the pet player takes damage. * If the Master is sitting on them (HUMAN_CHAIR), get up immediately. */ public void onPetHurt(DamageSource source, float amount) { if (amount <= 0) return; MasterState current = stateManager.getCurrentState(); if (current == MasterState.HUMAN_CHAIR) { TiedUpMod.LOGGER.info( "[EntityMaster] {} getting up - pet was hurt", getNpcName() ); ServerPlayer pet = getPetPlayer(); if (pet != null) { com.tiedup.remake.dialogue.DialogueBridge.talkTo( this, pet, "petplay.human_chair_end" ); } setMasterState(MasterState.FOLLOWING); } } @Override public void die(DamageSource source) { // Free the pet when master dies ServerPlayer pet = getPetPlayer(); if (pet != null) { freePet(pet); } super.die(source); } // PET MANAGEMENT (delegates to MasterPetManager) /** Get the pet manager component. */ public MasterPetManager getPetManager() { return petManager; } /** @see MasterPetManager#getPetPlayer() */ @Nullable public ServerPlayer getPetPlayer() { return petManager.getPetPlayer(); } /** @see MasterPetManager#setPetPlayer(ServerPlayer) */ public void setPetPlayer(ServerPlayer player) { petManager.setPetPlayer(player); syncState(); } /** @see MasterPetManager#hasPet() */ public boolean hasPet() { return petManager.hasPet(); } /** @see MasterPetManager#isPetPlayer(Player) */ public boolean isPetPlayer(Player player) { return petManager.isPetPlayer(player); } // GOAL ACCESSORS /** * Get the place block goal for triggering feeding/resting. */ @Nullable public MasterPlaceBlockGoal getPlaceBlockGoal() { return placeBlockGoal; } /** * Get the dogwalk goal. */ @Nullable public MasterDogwalkGoal getDogwalkGoal() { return dogwalkGoal; } // DOGWALK MODE /** * Set dogwalk mode. * * @param masterLeads If true, master walks and pulls pet. If false, master follows pet. */ public void setDogwalkMode(boolean masterLeads) { this.dogwalkMasterLeads = masterLeads; if (dogwalkGoal != null) { dogwalkGoal.setMasterLeads(masterLeads); } } /** * Check if master leads during dogwalk. */ public boolean isDogwalkMasterLeads() { return dogwalkMasterLeads; } // TASK MANAGEMENT (delegates to MasterTaskManager) /** Get the task manager component. */ public MasterTaskManager getTaskManager() { return taskManager; } /** @see MasterTaskManager#hasActiveTask() */ public boolean hasActiveTask() { return taskManager.hasActiveTask(); } /** @see MasterTaskManager#getCurrentTask() */ @Nullable public PetTask getCurrentTask() { return taskManager.getCurrentTask(); } /** @see MasterTaskManager#setActiveTask(PetTask) */ public void setActiveTask(PetTask task) { taskManager.setActiveTask(task); } /** @see MasterTaskManager#clearActiveTask() */ public void clearActiveTask() { taskManager.clearActiveTask(); } /** @see MasterTaskManager#getTaskStartPosition() */ @Nullable public net.minecraft.world.phys.Vec3 getTaskStartPosition() { return taskManager.getTaskStartPosition(); } /** @see MasterTaskManager#setTaskStartPosition(net.minecraft.world.phys.Vec3) */ public void setTaskStartPosition(net.minecraft.world.phys.Vec3 position) { taskManager.setTaskStartPosition(position); } /** @see MasterTaskManager#getRequestedItem() */ @Nullable public net.minecraft.world.item.Item getRequestedItem() { return taskManager.getRequestedItem(); } /** @see MasterTaskManager#setRequestedItem(net.minecraft.world.item.Item) */ public void setRequestedItem(net.minecraft.world.item.Item item) { taskManager.setRequestedItem(item); } /** * Get the task assign goal for force assignment. */ @Nullable public MasterTaskAssignGoal getTaskAssignGoal() { return taskAssignGoal; } // LEASH MANAGEMENT (delegates to MasterPetManager) /** @see MasterPetManager#attachLeashToPet() */ public void attachLeashToPet() { petManager.attachLeashToPet(); } /** @see MasterPetManager#detachLeashFromPet() */ public void detachLeashFromPet() { petManager.detachLeashFromPet(); } /** @see MasterPetManager#isPetLeashed() */ public boolean isPetLeashed() { return petManager.isPetLeashed(); } // ENGAGEMENT CADENCE (delegates to MasterTaskManager) /** @see MasterTaskManager#markEngagement() */ public void markEngagement() { taskManager.markEngagement(); } /** @see MasterTaskManager#getEngagementMultiplier() */ public float getEngagementMultiplier() { return taskManager.getEngagementMultiplier(); } // COLD SHOULDER (delegates to MasterTaskManager) /** @see MasterTaskManager#isGivingColdShoulder() */ public boolean isGivingColdShoulder() { return taskManager.isGivingColdShoulder(); } /** @see MasterTaskManager#startColdShoulder(int) */ public void startColdShoulder(int durationTicks) { taskManager.startColdShoulder(durationTicks); } /** @see MasterPetManager#putPetCollar(ServerPlayer) */ public void putPetCollar(ServerPlayer player) { petManager.putPetCollar(player); } /** @see MasterPetManager#freePet(ServerPlayer) */ public void freePet(ServerPlayer player) { petManager.freePet(player); syncState(); } /** * Check if player has pet play collar from this master. */ public static boolean hasPetCollar(Player player) { ItemStack collarStack = V2EquipmentHelper.getInRegion( player, BodyRegionV2.NECK ); if (collarStack.isEmpty()) return false; CompoundTag tag = collarStack.getTag(); return tag != null && tag.getBoolean("petPlayMode"); } /** @see MasterPetManager#syncDataToCollar(ServerPlayer) */ private void syncDataToCollar(ServerPlayer pet) { petManager.syncDataToCollar(pet); } /** * Get master UUID from player's pet collar. */ @Nullable public static UUID getMasterUUID(Player player) { ItemStack collarStack = V2EquipmentHelper.getInRegion( player, BodyRegionV2.NECK ); if (collarStack.isEmpty()) return null; CompoundTag tag = collarStack.getTag(); if (tag != null && tag.hasUUID("masterUUID")) { return tag.getUUID("masterUUID"); } return null; } // STATE ACCESSORS public MasterStateManager getStateManager() { return stateManager; } public void setMasterState(MasterState state) { stateManager.setCurrentState(state); syncState(); } /** * Get and consume the forced punishment type (returns null if none pending). */ @Nullable public PunishmentType consumeForcedPunishment() { PunishmentType type = this.pendingForcedPunishment; this.pendingForcedPunishment = null; return type; } /** * Get and consume the attack punishment flag. * Returns true if this punishment was triggered by pet attacking the master. */ public boolean consumeAttackPunishment() { boolean pending = this.pendingAttackPunishment; this.pendingAttackPunishment = false; return pending; } // NBT SERIALIZATION @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.putString(NBT_MASTER_STATE, stateManager.serializeState()); String petUUID = stateManager.serializePetUUID(); if (petUUID != null) { tag.putString(NBT_PET_UUID, petUUID); } // Save task, engagement, and cold shoulder data taskManager.save(tag); // Save dogwalk mode preference tag.putBoolean(NBT_DOGWALK_MASTER_LEADS, dogwalkMasterLeads); // FIX: Save placed block position for cleanup after restart if (placeBlockGoal != null) { net.minecraft.core.BlockPos placedPos = placeBlockGoal.getPlacedBlockPos(); if (placedPos != null) { tag.putInt(NBT_PLACED_BLOCK_POS + "X", placedPos.getX()); tag.putInt(NBT_PLACED_BLOCK_POS + "Y", placedPos.getY()); tag.putInt(NBT_PLACED_BLOCK_POS + "Z", placedPos.getZ()); } } } @Override public void readAdditionalSaveData(CompoundTag tag) { super.readAdditionalSaveData(tag); if (tag.contains(NBT_MASTER_STATE)) { stateManager.deserializeState(tag.getString(NBT_MASTER_STATE)); } if (tag.contains(NBT_PET_UUID)) { stateManager.deserializePetUUID(tag.getString(NBT_PET_UUID)); } // Load task, engagement, and cold shoulder data taskManager.load(tag); // Load dogwalk mode preference if (tag.contains(NBT_DOGWALK_MASTER_LEADS)) { dogwalkMasterLeads = tag.getBoolean(NBT_DOGWALK_MASTER_LEADS); } // FIX: Load placed block position and schedule cleanup if ( tag.contains(NBT_PLACED_BLOCK_POS + "X") && placeBlockGoal != null ) { net.minecraft.core.BlockPos placedPos = new net.minecraft.core.BlockPos( tag.getInt(NBT_PLACED_BLOCK_POS + "X"), tag.getInt(NBT_PLACED_BLOCK_POS + "Y"), tag.getInt(NBT_PLACED_BLOCK_POS + "Z") ); placeBlockGoal.setPlacedBlockPos(placedPos); // Schedule cleanup for next tick (level not fully loaded yet) placeBlockGoal.cleanupOrphanedBlock(); } } @Override public void remove(RemovalReason reason) { // MEDIUM FIX: Master-specific cleanup (pet is NOT in captiveManager) if (!this.level().isClientSide) { // 0. If master was in HUMAN_CHAIR, clean up the pet pose. // discard() triggers remove() but NOT goal.stop(), so without this // the pet keeps the humanChairMode bind and freeze effects forever. if (stateManager.getCurrentState() == MasterState.HUMAN_CHAIR) { ServerPlayer pet = getPetPlayer(); if (pet != null) { PlayerBindState bindState = PlayerBindState.getInstance( pet ); if (bindState != null && bindState.isTiedUp()) { net.minecraft.world.item.ItemStack bind = bindState.getEquipment(BodyRegionV2.ARMS); if (!bind.isEmpty()) { net.minecraft.nbt.CompoundTag tag = bind.getTag(); if ( tag != null && tag.getBoolean( com.tiedup.remake.util.HumanChairHelper.NBT_KEY ) ) { bindState.unequip(BodyRegionV2.ARMS); } } } pet.removeEffect( net.minecraft.world.effect.MobEffects.MOVEMENT_SLOWDOWN ); pet.removeEffect( net.minecraft.world.effect.MobEffects.JUMP ); TiedUpMod.LOGGER.info( "[EntityMaster] {} cleanup: removed human chair pose from pet", getNpcName() ); } } // 1. Detach leash from pet // This prevents the player from being leashed to a non-existent entity this.detachLeashFromPet(); // 2. End any active struggle session for the pet // If master vanishes, the struggle session must end or it will bug out UUID petUUID = stateManager.getPetPlayerUUID(); if (petUUID != null) { com.tiedup.remake.minigame.StruggleSessionManager.getInstance().endContinuousStruggleSession( petUUID, false ); TiedUpMod.LOGGER.info( "[EntityMaster] {} cleanup: detached leash and ended struggle session for pet {}", getNpcName(), petUUID.toString().substring(0, 8) ); } } // Call super to handle any standard kidnapper cleanup (if used) super.remove(reason); } // STATE HOST IMPLEMENTATION private class MasterStateHost implements IMasterStateHost { @Override public String getNpcName() { return EntityMaster.this.getNpcName(); } @Override public long getCurrentTick() { return EntityMaster.this.level().getGameTime(); } @Override public void onDistracted() { // Could play animation or particles TiedUpMod.LOGGER.debug( "[EntityMaster] {} became distracted", getNpcName() ); } @Override public void onDistractionEnd() { TiedUpMod.LOGGER.debug( "[EntityMaster] {} is no longer distracted", getNpcName() ); } @Override public void onStruggleDetected() { // Already handled in onStruggleDetected method } } // BODY ROTATION OVERRIDE @Override protected float tickHeadTurn(float yRot, float animStep) { // While sitting on pet, skip BodyRotationControl and force body to match // entity yRot (which IS network-synced, unlike yBodyRot). // Server: positionOnPet() sets yRot = sideYaw → synced to client. // Client: we read the synced yRot and apply it to yBodyRot. if (isSitting()) { this.yBodyRot = this.getYRot(); this.yBodyRotO = this.yRotO; return animStep; } return super.tickHeadTurn(yRot, animStep); } // COLLISION OVERRIDE @Override public boolean isPushable() { // Disable entity pushing during human chair so master can sit on pet if (getMasterState() == MasterState.HUMAN_CHAIR) { return false; } return super.isPushable(); } @Override protected void pushEntities() { // Don't push anyone while sitting on pet (blocks master→player collision direction) if (getMasterState() == MasterState.HUMAN_CHAIR) { return; } super.pushEntities(); } }