Files
TiedUp-/src/main/java/com/tiedup/remake/entities/EntityMaster.java
Adrien 32202dab97 feat(D-01/E): quickwins — debug toggle, HumanChairHelper move, collar equip
Q1: Remove F9 debug toggle from GltfAnimationApplier + delete dead
    GltfRenderLayer + remove keybind registration

Q2: Move HumanChairHelper from state/ to util/ — pure utility with
    no state dependency. 7 import updates.

Q3: Wire NECK collar equip flow in DataDrivenBondageItem:
    - Target must be tied up (V1 rule preserved)
    - Distance + line-of-sight validation
    - Owner added to NBT before equip via CollarHelper.addOwner()
    - V2EquipmentHelper handles conflict resolution
    - ModSounds.COLLAR_PUT played on success
    - OwnershipComponent.onEquipped registers in CollarRegistry
2026-04-15 10:57:01 +02:00

1192 lines
39 KiB
Java

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<String> DATA_MASTER_STATE =
SynchedEntityData.defineId(
EntityMaster.class,
EntityDataSerializers.STRING
);
/** Whether master is distracted */
private static final EntityDataAccessor<Boolean> 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<? extends EntityMaster> 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();
}
}