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
1192 lines
39 KiB
Java
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();
|
|
}
|
|
}
|