Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityKidnapper;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to approach and buy a player from a Kidnapper.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Master spawns 50 blocks away
|
||||
* 2. Master walks towards the Kidnapper
|
||||
* 3. Master arrives and greets Kidnapper (RP dialogue)
|
||||
* 4. Master negotiates/inspects the "merchandise"
|
||||
* 5. Master completes purchase
|
||||
* 6. Kidnapper leaves, Master takes ownership
|
||||
*/
|
||||
public class MasterBuyPlayerGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Search radius for kidnappers (if not pre-set) */
|
||||
private static final double SEARCH_RADIUS = 64.0;
|
||||
|
||||
/** Distance to start negotiation dialogue */
|
||||
private static final double GREETING_DISTANCE = 8.0;
|
||||
|
||||
/** Distance to complete purchase */
|
||||
private static final double PURCHASE_DISTANCE = 3.0;
|
||||
|
||||
/** Target kidnapper selling a player */
|
||||
private EntityKidnapper targetKidnapper = null;
|
||||
|
||||
/** Purchase phase */
|
||||
private PurchasePhase phase = PurchasePhase.APPROACHING;
|
||||
|
||||
/** Timer for current phase */
|
||||
private int phaseTimer = 0;
|
||||
|
||||
/** Phase durations (in ticks) */
|
||||
private static final int GREETING_DURATION = 60; // 3 seconds
|
||||
private static final int INSPECT_DURATION = 80; // 4 seconds
|
||||
private static final int NEGOTIATE_DURATION = 60; // 3 seconds
|
||||
private static final int PURCHASE_DURATION = 60; // 3 seconds
|
||||
|
||||
/** Whether we've said the approach dialogue */
|
||||
private boolean hasSaidApproach = false;
|
||||
|
||||
/** Pathfinding recalculation cooldown to avoid path thrashing */
|
||||
private int pathRecalcCooldown = 0;
|
||||
|
||||
private enum PurchasePhase {
|
||||
APPROACHING, // Walking to kidnapper
|
||||
GREETING, // Arrived, greeting
|
||||
INSPECTING, // Looking at the merchandise
|
||||
NEGOTIATING, // Discussing price
|
||||
PURCHASING, // Completing purchase
|
||||
COMPLETE, // Done
|
||||
}
|
||||
|
||||
public MasterBuyPlayerGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
// Preserve state for ALL phases in progress (including APPROACHING)
|
||||
// This prevents goal from restarting during approach
|
||||
if (targetKidnapper != null && phase != PurchasePhase.COMPLETE) {
|
||||
// Validate target is still valid
|
||||
if (targetKidnapper.isAlive()) {
|
||||
return true; // Continue without restarting
|
||||
}
|
||||
// Target died - reset
|
||||
targetKidnapper = null;
|
||||
phase = PurchasePhase.APPROACHING;
|
||||
}
|
||||
|
||||
// Must be in purchasing state or idle without pet
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
if (state != MasterState.PURCHASING && state != MasterState.IDLE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't search if already has a pet
|
||||
if (master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First check if we have a pre-set selling kidnapper
|
||||
if (master.hasSellingKidnapper()) {
|
||||
targetKidnapper = master.getSellingKidnapper();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise search for one nearby
|
||||
targetKidnapper = findSellingKidnapper();
|
||||
return targetKidnapper != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
// Once we've started greeting, we're committed to the purchase
|
||||
if (
|
||||
phase.ordinal() >= PurchasePhase.GREETING.ordinal() &&
|
||||
phase != PurchasePhase.COMPLETE
|
||||
) {
|
||||
// Only stop if kidnapper dies or we completed
|
||||
return targetKidnapper != null && targetKidnapper.isAlive();
|
||||
}
|
||||
|
||||
// During approach phase, check if target is still valid
|
||||
if (targetKidnapper == null || !targetKidnapper.isAlive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IRestrainable captive = targetKidnapper.getCaptive();
|
||||
if (captive == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop if already has pet (purchase completed)
|
||||
return !master.hasPet() && phase != PurchasePhase.COMPLETE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
// Don't reset if we're already in progress (goal was briefly interrupted or resuming)
|
||||
// This includes APPROACHING phase - only reset when starting from scratch or COMPLETE
|
||||
if (targetKidnapper != null && phase != PurchasePhase.COMPLETE) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] Resuming {} at phase {}",
|
||||
master.getNpcName(),
|
||||
phase
|
||||
);
|
||||
// Ensure state is set
|
||||
master.setMasterState(MasterState.PURCHASING);
|
||||
return;
|
||||
}
|
||||
|
||||
// Starting fresh - reset everything
|
||||
this.phaseTimer = 0;
|
||||
this.phase = PurchasePhase.APPROACHING;
|
||||
this.hasSaidApproach = false;
|
||||
this.pathRecalcCooldown = 0;
|
||||
master.setMasterState(MasterState.PURCHASING);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] {} approaching {} to buy player",
|
||||
master.getNpcName(),
|
||||
targetKidnapper != null
|
||||
? targetKidnapper.getNpcName()
|
||||
: "unknown"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
master.getNavigation().stop();
|
||||
|
||||
// Only fully reset if purchase completed or truly failed
|
||||
if (phase == PurchasePhase.COMPLETE || master.hasPet()) {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] {} completed purchase, transitioning to FOLLOWING",
|
||||
master.getNpcName()
|
||||
);
|
||||
this.targetKidnapper = null;
|
||||
this.phaseTimer = 0;
|
||||
this.phase = PurchasePhase.APPROACHING;
|
||||
this.hasSaidApproach = false;
|
||||
master.clearSellingKidnapper();
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
// Otherwise keep state for resume (goal was just temporarily interrupted)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (targetKidnapper == null) return;
|
||||
|
||||
// Look at kidnapper
|
||||
master.getLookControl().setLookAt(targetKidnapper, 30.0F, 30.0F);
|
||||
|
||||
double distSq = master.distanceToSqr(targetKidnapper);
|
||||
double dist = Math.sqrt(distSq);
|
||||
|
||||
switch (phase) {
|
||||
case APPROACHING -> tickApproaching(dist);
|
||||
case GREETING -> tickGreeting();
|
||||
case INSPECTING -> tickInspecting();
|
||||
case NEGOTIATING -> tickNegotiating();
|
||||
case PURCHASING -> tickPurchasing();
|
||||
case COMPLETE -> {
|
||||
} // Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
private void tickApproaching(double dist) {
|
||||
// Approach dialogue when getting close (once)
|
||||
if (!hasSaidApproach && dist < 30) {
|
||||
IRestrainable captive = targetKidnapper.getCaptive();
|
||||
if (
|
||||
captive != null &&
|
||||
captive.asLivingEntity() instanceof Player player
|
||||
) {
|
||||
DialogueBridge.talkTo(master, player, "purchase.interested");
|
||||
}
|
||||
hasSaidApproach = true;
|
||||
}
|
||||
|
||||
if (dist > GREETING_DISTANCE) {
|
||||
// Recalculate path every 20 ticks (1 second) instead of every tick
|
||||
// This prevents path thrashing which dramatically slows movement
|
||||
if (pathRecalcCooldown <= 0) {
|
||||
master.getNavigation().moveTo(targetKidnapper, 1.0);
|
||||
pathRecalcCooldown = 20;
|
||||
} else {
|
||||
pathRecalcCooldown--;
|
||||
}
|
||||
} else {
|
||||
// Arrived - start greeting
|
||||
master.getNavigation().stop();
|
||||
phase = PurchasePhase.GREETING;
|
||||
phaseTimer = 0;
|
||||
pathRecalcCooldown = 0;
|
||||
|
||||
// Greeting dialogue
|
||||
IRestrainable captive = targetKidnapper.getCaptive();
|
||||
if (
|
||||
captive != null &&
|
||||
captive.asLivingEntity() instanceof Player player
|
||||
) {
|
||||
DialogueBridge.talkTo(master, player, "idle.greeting_stranger");
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] {} arrived, greeting {}",
|
||||
master.getNpcName(),
|
||||
targetKidnapper.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void tickGreeting() {
|
||||
phaseTimer++;
|
||||
|
||||
// Move closer during greeting
|
||||
double dist = Math.sqrt(master.distanceToSqr(targetKidnapper));
|
||||
if (dist > PURCHASE_DISTANCE) {
|
||||
master.getNavigation().moveTo(targetKidnapper, 0.5);
|
||||
}
|
||||
|
||||
if (phaseTimer >= GREETING_DURATION) {
|
||||
phase = PurchasePhase.INSPECTING;
|
||||
phaseTimer = 0;
|
||||
|
||||
// Kidnapper offers sale dialogue
|
||||
IRestrainable captive = targetKidnapper.getCaptive();
|
||||
if (
|
||||
captive != null &&
|
||||
captive.asLivingEntity() instanceof Player player
|
||||
) {
|
||||
targetKidnapper.talkTo(
|
||||
player,
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.SALE_OFFER
|
||||
);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] {} inspecting merchandise",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void tickInspecting() {
|
||||
phaseTimer++;
|
||||
|
||||
// Look at the captive during inspection
|
||||
IRestrainable captive = targetKidnapper.getCaptive();
|
||||
if (captive != null) {
|
||||
master
|
||||
.getLookControl()
|
||||
.setLookAt(captive.asLivingEntity(), 30.0F, 30.0F);
|
||||
}
|
||||
|
||||
if (phaseTimer >= INSPECT_DURATION) {
|
||||
phase = PurchasePhase.NEGOTIATING;
|
||||
phaseTimer = 0;
|
||||
|
||||
// Master negotiates
|
||||
if (
|
||||
captive != null &&
|
||||
captive.asLivingEntity() instanceof Player player
|
||||
) {
|
||||
DialogueBridge.talkTo(master, player, "purchase.negotiating");
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] {} negotiating",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void tickNegotiating() {
|
||||
phaseTimer++;
|
||||
|
||||
// Look back at kidnapper
|
||||
master.getLookControl().setLookAt(targetKidnapper, 30.0F, 30.0F);
|
||||
|
||||
if (phaseTimer >= NEGOTIATE_DURATION) {
|
||||
phase = PurchasePhase.PURCHASING;
|
||||
phaseTimer = 0;
|
||||
|
||||
// Sale complete dialogue from kidnapper
|
||||
IRestrainable captive = targetKidnapper.getCaptive();
|
||||
if (
|
||||
captive != null &&
|
||||
captive.asLivingEntity() instanceof Player player
|
||||
) {
|
||||
targetKidnapper.talkTo(
|
||||
player,
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.SALE_COMPLETE
|
||||
);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] {} completing purchase",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void tickPurchasing() {
|
||||
phaseTimer++;
|
||||
|
||||
if (phaseTimer >= PURCHASE_DURATION) {
|
||||
completePurchase();
|
||||
phase = PurchasePhase.COMPLETE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a nearby kidnapper who is selling a player.
|
||||
*/
|
||||
private EntityKidnapper findSellingKidnapper() {
|
||||
List<EntityKidnapper> kidnappers = master
|
||||
.level()
|
||||
.getEntitiesOfClass(
|
||||
EntityKidnapper.class,
|
||||
master.getBoundingBox().inflate(SEARCH_RADIUS),
|
||||
k -> {
|
||||
if (!k.isAlive() || k.isTiedUp()) return false;
|
||||
IRestrainable captive = k.getCaptive();
|
||||
if (captive == null) return false;
|
||||
if (!captive.isForSell()) return false;
|
||||
// Only buy players, not NPCs
|
||||
return captive.asLivingEntity() instanceof Player;
|
||||
}
|
||||
);
|
||||
|
||||
if (kidnappers.isEmpty()) return null;
|
||||
|
||||
// Return closest one
|
||||
return kidnappers
|
||||
.stream()
|
||||
.min((a, b) ->
|
||||
Double.compare(master.distanceToSqr(a), master.distanceToSqr(b))
|
||||
)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the purchase - take the player from kidnapper.
|
||||
* Properly transfers the captive with their bindings to the Master.
|
||||
*/
|
||||
private void completePurchase() {
|
||||
if (targetKidnapper == null) return;
|
||||
|
||||
IRestrainable captive = targetKidnapper.getCaptive();
|
||||
if (captive == null) return;
|
||||
|
||||
if (!(captive.asLivingEntity() instanceof ServerPlayer player)) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[MasterBuyPlayerGoal] Captive is not a player!"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] {} bought {} from {}",
|
||||
master.getNpcName(),
|
||||
player.getName().getString(),
|
||||
targetKidnapper.getNpcName()
|
||||
);
|
||||
|
||||
// Purchase complete dialogue to player
|
||||
DialogueBridge.talkTo(master, player, "purchase.complete");
|
||||
|
||||
// Cancel the sale before transfer
|
||||
captive.cancelSale();
|
||||
|
||||
// IMPORTANT: Enable captive transfer flag on Kidnapper BEFORE transfer
|
||||
// This is required for transferCaptivityTo() to work
|
||||
targetKidnapper.setAllowCaptiveTransferFlag(true);
|
||||
|
||||
// Use transferCaptivityTo for proper leash transfer
|
||||
// This detaches the leash from Kidnapper and attaches it to Master
|
||||
captive.transferCaptivityTo(master);
|
||||
|
||||
// Disable transfer flag after transfer
|
||||
targetKidnapper.setAllowCaptiveTransferFlag(false);
|
||||
|
||||
// Set up pet relationship in Master's state manager
|
||||
master.setPetPlayer(player);
|
||||
|
||||
// IMPORTANT: Detach leash after purchase
|
||||
// The leash should only be attached during specific activities (walks, etc.)
|
||||
// Pet play mode uses collar control, not constant leashing
|
||||
if (
|
||||
player instanceof com.tiedup.remake.state.IPlayerLeashAccess access
|
||||
) {
|
||||
access.tiedup$detachLeash();
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] Detached leash from {} after purchase",
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
// Replace shock collar with choke collar for pet play
|
||||
master.putPetCollar(player);
|
||||
|
||||
// Remove bindings (arms/legs) but keep the collar
|
||||
// The pet starts fresh without restraints (events can add them later)
|
||||
removeBindingsFromNewPet(captive);
|
||||
|
||||
// Collaring dialogue
|
||||
DialogueBridge.talkTo(master, player, "purchase.collaring");
|
||||
|
||||
// Notify kidnapper that purchase is complete via the WaitForBuyer goal
|
||||
var buyerGoal = targetKidnapper.getWaitForBuyerGoal();
|
||||
if (buyerGoal != null) {
|
||||
buyerGoal.onMasterPurchaseComplete();
|
||||
} else {
|
||||
// Fallback: just set get out state directly
|
||||
targetKidnapper.setGetOutState(true);
|
||||
}
|
||||
|
||||
// Introduction dialogue
|
||||
DialogueBridge.talkTo(master, player, "purchase.introduction");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] Transfer complete - {} now owns {}",
|
||||
master.getNpcName(),
|
||||
player.getName().getString()
|
||||
);
|
||||
|
||||
targetKidnapper = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove bindings from the new pet (but keep the collar and clothes).
|
||||
* The pet starts with only the choke collar - events may add restraints later.
|
||||
*
|
||||
* <p><b>Note:</b> We cannot use {@code untie(false)} because that clears ALL slots
|
||||
* including the collar. Instead, we manually remove specific regions.</p>
|
||||
*
|
||||
* @param captive The captive IRestrainable state
|
||||
*/
|
||||
private void removeBindingsFromNewPet(IRestrainable captive) {
|
||||
if (!(captive.asLivingEntity() instanceof ServerPlayer player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually remove each region EXCEPT neck (collar) and torso (clothes)
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.unequipFromRegion(
|
||||
player, com.tiedup.remake.v2.BodyRegionV2.ARMS, true
|
||||
);
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.unequipFromRegion(
|
||||
player, com.tiedup.remake.v2.BodyRegionV2.MOUTH, true
|
||||
);
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.unequipFromRegion(
|
||||
player, com.tiedup.remake.v2.BodyRegionV2.EYES, true
|
||||
);
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.unequipFromRegion(
|
||||
player, com.tiedup.remake.v2.BodyRegionV2.EARS, true
|
||||
);
|
||||
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.unequipFromRegion(
|
||||
player, com.tiedup.remake.v2.BodyRegionV2.HANDS, true
|
||||
);
|
||||
|
||||
// V1 speed reduction handled by MovementStyleManager (V2 tick-based).
|
||||
// See H6 fix — removing V1 calls prevents double stacking.
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterBuyPlayerGoal] Removed bindings from new pet {}",
|
||||
captive.getKidnappedName()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.state.IPlayerLeashAccess;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import java.util.EnumSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster dogwalk mode.
|
||||
*
|
||||
* Two modes:
|
||||
* - Master leads (masterLeads=true): Master walks, player is pulled by leash
|
||||
* - Pet leads (masterLeads=false): Master follows the pet at close distance
|
||||
*
|
||||
* The leash physics automatically pulls the player when Master moves away,
|
||||
* handled by the LeashProxyEntity and player leash mixin.
|
||||
*/
|
||||
public class MasterDogwalkGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** If true, Master walks and pulls pet. If false, Master follows pet. */
|
||||
private boolean masterLeads = false;
|
||||
|
||||
/** Distance to maintain from pet when following */
|
||||
private static final double LEASH_DISTANCE_PET_LEADS = 3.5;
|
||||
|
||||
/** Maximum distance before waiting for pet (when master leads) */
|
||||
private static final double MAX_DISTANCE = 10.0;
|
||||
|
||||
/** Navigation speed (used by masterLeads) */
|
||||
private static final double WALK_SPEED = 0.6;
|
||||
|
||||
/** Speed when pet is close (3.5-6 blocks) - gentle follow */
|
||||
private static final double FOLLOW_CLOSE_SPEED = 0.5;
|
||||
|
||||
/** Speed when pet is far (6+ blocks) - catching up */
|
||||
private static final double FOLLOW_CATCHUP_SPEED = 0.9;
|
||||
|
||||
/** Stop following when closer than this */
|
||||
private static final double FOLLOW_STOP_DISTANCE = 2.5;
|
||||
|
||||
/** Timer for random direction changes when master leads */
|
||||
private int directionChangeTimer = 0;
|
||||
|
||||
/** Current walk destination (when master leads) */
|
||||
private Vec3 walkTarget = null;
|
||||
|
||||
/** Cooldown for "wait for pet" dialogue */
|
||||
private int waitDialogueCooldown = 0;
|
||||
|
||||
/** Maximum dogwalk duration (ticks) - 1-2 minutes */
|
||||
private static final int MIN_WALK_DURATION = 1200; // 1 minute
|
||||
private static final int MAX_WALK_DURATION = 2400; // 2 minutes
|
||||
|
||||
/** Current walk duration for this session */
|
||||
private int walkDuration = 0;
|
||||
|
||||
/** Walk timer */
|
||||
private int walkTimer = 0;
|
||||
|
||||
/** FIX: Stuck timer - counts ticks while waiting for pet */
|
||||
private int stuckTimer = 0;
|
||||
|
||||
/** FIX: Maximum stuck time before teleporting pet (15 seconds) */
|
||||
private static final int MAX_STUCK_TIME = 300;
|
||||
|
||||
public MasterDogwalkGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether Master leads the walk.
|
||||
*/
|
||||
public void setMasterLeads(boolean masterLeads) {
|
||||
this.masterLeads = masterLeads;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
return (
|
||||
master.getStateManager().getCurrentState() == MasterState.DOGWALK &&
|
||||
master.hasPet()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (!master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null || !pet.isAlive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// End walk if duration exceeded
|
||||
if (walkTimer >= walkDuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
master.getStateManager().getCurrentState() == MasterState.DOGWALK
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.directionChangeTimer = 0;
|
||||
this.walkTarget = null;
|
||||
this.waitDialogueCooldown = 0;
|
||||
this.walkTimer = 0;
|
||||
this.stuckTimer = 0;
|
||||
|
||||
// Random walk duration between 2-3 minutes
|
||||
this.walkDuration =
|
||||
MIN_WALK_DURATION +
|
||||
master.getRandom().nextInt(MAX_WALK_DURATION - MIN_WALK_DURATION);
|
||||
|
||||
// Get current mode from Master
|
||||
this.masterLeads = master.isDogwalkMasterLeads();
|
||||
|
||||
// When pet leads, give extra leash slack so they can walk ahead
|
||||
if (!masterLeads) {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet instanceof IPlayerLeashAccess access) {
|
||||
access.tiedup$setLeashSlack(5.0); // 3+5=8 blocks before pull
|
||||
}
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterDogwalkGoal] {} started dogwalk (masterLeads={}, duration={} ticks)",
|
||||
master.getNpcName(),
|
||||
masterLeads,
|
||||
walkDuration
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
master.getNavigation().stop();
|
||||
this.walkTarget = null;
|
||||
|
||||
// Clean up: remove dogbind and detach leash
|
||||
cleanupDogwalk();
|
||||
|
||||
// FIX: Transition out of DOGWALK state so the goal doesn't restart
|
||||
if (master.getStateManager().getCurrentState() == MasterState.DOGWALK) {
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterDogwalkGoal] {} stopped dogwalk",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after dogwalk ends.
|
||||
* Removes dogbind from player and detaches leash.
|
||||
*/
|
||||
private void cleanupDogwalk() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Reset leash slack
|
||||
if (pet instanceof IPlayerLeashAccess access) {
|
||||
access.tiedup$setLeashSlack(0.0);
|
||||
}
|
||||
|
||||
// Detach leash
|
||||
master.detachLeashFromPet();
|
||||
|
||||
// Remove dogbind from player
|
||||
PlayerBindState state = PlayerBindState.getInstance(pet);
|
||||
if (state != null && state.isTiedUp()) {
|
||||
state.unequip(BodyRegionV2.ARMS);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterDogwalkGoal] Removed dogbind from {} after walk",
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
// Dialogue for walk end
|
||||
DialogueBridge.talkTo(master, pet, "petplay.walk_end");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Always look at pet
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
double dist = master.distanceTo(pet);
|
||||
|
||||
// Increment walk timer
|
||||
walkTimer++;
|
||||
|
||||
// Decrement dialogue cooldown
|
||||
if (waitDialogueCooldown > 0) {
|
||||
waitDialogueCooldown--;
|
||||
}
|
||||
|
||||
if (masterLeads) {
|
||||
tickMasterLeads(pet, dist);
|
||||
} else {
|
||||
tickMasterFollows(pet, dist);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick when Master leads the walk.
|
||||
* Master walks in a direction, pet is pulled by leash physics.
|
||||
*/
|
||||
private void tickMasterLeads(ServerPlayer pet, double dist) {
|
||||
// If pet is too far behind, wait for them
|
||||
if (dist > MAX_DISTANCE) {
|
||||
master.getNavigation().stop();
|
||||
stuckTimer++;
|
||||
|
||||
// Occasionally tell pet to keep up
|
||||
if (waitDialogueCooldown <= 0) {
|
||||
DialogueBridge.talkTo(master, pet, "petplay.wait_pet");
|
||||
waitDialogueCooldown = 200; // 10 seconds cooldown
|
||||
}
|
||||
|
||||
// FIX: If stuck too long, teleport pet to master
|
||||
if (stuckTimer >= MAX_STUCK_TIME) {
|
||||
teleportPetToMaster(pet);
|
||||
stuckTimer = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Pet caught up, reset stuck timer
|
||||
stuckTimer = 0;
|
||||
|
||||
// Update direction timer
|
||||
directionChangeTimer--;
|
||||
|
||||
// Pick new random direction periodically or if no target
|
||||
if (
|
||||
directionChangeTimer <= 0 ||
|
||||
walkTarget == null ||
|
||||
!master.getNavigation().isInProgress()
|
||||
) {
|
||||
pickNewWalkDirection();
|
||||
directionChangeTimer = 100 + master.getRandom().nextInt(100); // 5-10 seconds
|
||||
}
|
||||
|
||||
// Navigate to target
|
||||
if (walkTarget != null && !master.getNavigation().isInProgress()) {
|
||||
master
|
||||
.getNavigation()
|
||||
.moveTo(walkTarget.x, walkTarget.y, walkTarget.z, WALK_SPEED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a new random direction for walking.
|
||||
*/
|
||||
private void pickNewWalkDirection() {
|
||||
// Random angle
|
||||
double angle = master.getRandom().nextDouble() * Math.PI * 2;
|
||||
double distance = 8.0 + master.getRandom().nextDouble() * 8.0; // 8-16 blocks
|
||||
|
||||
double x = master.getX() + Math.cos(angle) * distance;
|
||||
double z = master.getZ() + Math.sin(angle) * distance;
|
||||
|
||||
// FIX: Use safe ground finder instead of heightmap (works indoors)
|
||||
int groundY = findSafeGroundY(
|
||||
master.level(),
|
||||
(int) x,
|
||||
(int) z,
|
||||
(int) master.getY() + 10
|
||||
);
|
||||
double y = groundY;
|
||||
|
||||
walkTarget = new Vec3(x, y, z);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterDogwalkGoal] {} picked new walk target: ({}, {}, {})",
|
||||
master.getNpcName(),
|
||||
(int) x,
|
||||
(int) y,
|
||||
(int) z
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a safe ground Y position by scanning downward.
|
||||
* Works correctly indoors (doesn't return roof height like heightmap).
|
||||
*
|
||||
* @param level The level
|
||||
* @param x Target X coordinate
|
||||
* @param z Target Z coordinate
|
||||
* @param startY Starting Y to scan from (usually entity Y + some offset)
|
||||
* @return A safe Y position with solid ground below and 2 air blocks above
|
||||
*/
|
||||
private int findSafeGroundY(Level level, int x, int z, int startY) {
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(
|
||||
x,
|
||||
startY,
|
||||
z
|
||||
);
|
||||
|
||||
// Scan downward to find a solid floor with 2 air blocks above
|
||||
for (int y = startY; y > level.getMinBuildHeight(); y--) {
|
||||
pos.setY(y);
|
||||
BlockState below = level.getBlockState(pos.below());
|
||||
BlockState at = level.getBlockState(pos);
|
||||
BlockState above = level.getBlockState(pos.above());
|
||||
|
||||
// Solid floor + 2 blocks of clearance above
|
||||
if (below.isSolid() && !at.isSolid() && !above.isSolid()) {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
return startY; // Fallback if nothing found
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick when Master follows the pet.
|
||||
* Uses progressive speed ramp: slow when close, faster to catch up.
|
||||
*/
|
||||
private void tickMasterFollows(ServerPlayer pet, double dist) {
|
||||
if (dist > LEASH_DISTANCE_PET_LEADS) {
|
||||
// Speed ramp: slow when close, faster to catch up
|
||||
double speed;
|
||||
if (dist > 6.0) {
|
||||
speed = FOLLOW_CATCHUP_SPEED; // 0.9 - catching up
|
||||
} else {
|
||||
speed = FOLLOW_CLOSE_SPEED; // 0.5 - gentle follow
|
||||
}
|
||||
master.getNavigation().moveTo(pet, speed);
|
||||
} else if (dist < FOLLOW_STOP_DISTANCE) {
|
||||
master.getNavigation().stop();
|
||||
}
|
||||
// Between FOLLOW_STOP_DISTANCE and LEASH_DISTANCE_PET_LEADS:
|
||||
// keep current movement, don't snap
|
||||
}
|
||||
|
||||
/**
|
||||
* FIX: Teleport pet to master when stuck too long.
|
||||
*/
|
||||
private void teleportPetToMaster(ServerPlayer pet) {
|
||||
double angle = master.getRandom().nextDouble() * Math.PI * 2;
|
||||
double distance = 1.5;
|
||||
|
||||
double x = master.getX() + Math.cos(angle) * distance;
|
||||
double z = master.getZ() + Math.sin(angle) * distance;
|
||||
double y = master.getY();
|
||||
|
||||
pet.teleportTo(x, y, z);
|
||||
|
||||
DialogueBridge.talkTo(master, pet, "petplay.come_here");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterDogwalkGoal] {} teleported stuck pet {} to master",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* End the dogwalk and return to FOLLOWING state.
|
||||
* Cleanup (remove dogbind, detach leash) happens automatically in stop().
|
||||
*/
|
||||
public void endDogwalk() {
|
||||
// Just change state - stop() will be called automatically and handle cleanup
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.util.teleport.Position;
|
||||
import com.tiedup.remake.util.teleport.TeleportHelper;
|
||||
import java.util.EnumSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to follow their pet player.
|
||||
*
|
||||
* Unlike normal follower mechanics, the Master follows the player.
|
||||
* Maintains a distance of 2-8 blocks from the player.
|
||||
*/
|
||||
public class MasterFollowPlayerGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Minimum distance to maintain from pet */
|
||||
private static final double MIN_DISTANCE = 2.0;
|
||||
|
||||
/** Ideal distance to maintain from pet (active following) */
|
||||
private static final double IDEAL_DISTANCE = 4.0;
|
||||
|
||||
/** Maximum distance before starting to follow */
|
||||
private static final double MAX_DISTANCE = 8.0;
|
||||
|
||||
/** Distance at which to teleport (if too far) */
|
||||
private static final double TELEPORT_DISTANCE = 32.0;
|
||||
|
||||
/** Navigation speed */
|
||||
private static final double FOLLOW_SPEED = 1.0;
|
||||
|
||||
/** Path recalculation cooldown (ticks) */
|
||||
private static final int PATH_RECALC_COOLDOWN = 10;
|
||||
|
||||
private int pathRecalcCooldown = 0;
|
||||
private double targetX, targetY, targetZ;
|
||||
|
||||
public MasterFollowPlayerGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
// Must have a pet
|
||||
if (!master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must be in a following-compatible state
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
if (
|
||||
state != MasterState.FOLLOWING &&
|
||||
state != MasterState.OBSERVING &&
|
||||
state != MasterState.DISTRACTED
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pet must be online
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null || !pet.isAlive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIX: Always activate in FOLLOWING state - distance is managed in tick()
|
||||
// This fixes the bug where Master wouldn't move after buying player
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (!master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null || !pet.isAlive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only continue in following-compatible states
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
if (
|
||||
state != MasterState.FOLLOWING &&
|
||||
state != MasterState.OBSERVING &&
|
||||
state != MasterState.DISTRACTED
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.pathRecalcCooldown = 0;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterFollowPlayerGoal] {} started following pet",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
master.getNavigation().stop();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterFollowPlayerGoal] {} stopped following pet",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Check for dimension change - teleport to pet's dimension if different
|
||||
if (!master.level().dimension().equals(pet.level().dimension())) {
|
||||
teleportToPetDimension(pet);
|
||||
return;
|
||||
}
|
||||
|
||||
// Look at pet
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
double distSq = master.distanceToSqr(pet);
|
||||
|
||||
// Teleport if too far (pet probably teleported within same dimension)
|
||||
if (distSq > TELEPORT_DISTANCE * TELEPORT_DISTANCE) {
|
||||
teleportNearPet(pet);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update path periodically
|
||||
this.pathRecalcCooldown--;
|
||||
if (this.pathRecalcCooldown <= 0) {
|
||||
this.pathRecalcCooldown = PATH_RECALC_COOLDOWN;
|
||||
|
||||
// FIX: Active following behavior
|
||||
// Problem: If Master stops in "comfort zone" (2-8 blocks) and player
|
||||
// is on leash, nobody moves = deadlock.
|
||||
// Solution: Master actively follows, maintaining IDEAL_DISTANCE from pet.
|
||||
|
||||
if (distSq < MIN_DISTANCE * MIN_DISTANCE) {
|
||||
// Too close - stop and let pet have some space
|
||||
master.getNavigation().stop();
|
||||
} else if (distSq > IDEAL_DISTANCE * IDEAL_DISTANCE) {
|
||||
// Beyond ideal distance - follow the pet
|
||||
master.getNavigation().moveTo(pet, FOLLOW_SPEED);
|
||||
} else if (!master.getNavigation().isInProgress()) {
|
||||
// At ideal distance and not moving - pick a random spot near pet
|
||||
// This creates natural wandering behavior around the pet
|
||||
wanderNearPet(pet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wander to a random position near the pet.
|
||||
* Creates natural movement behavior instead of standing still.
|
||||
*/
|
||||
private void wanderNearPet(ServerPlayer pet) {
|
||||
// Pick a random angle and position around the pet
|
||||
double angle = master.getRandom().nextDouble() * Math.PI * 2;
|
||||
double distance =
|
||||
IDEAL_DISTANCE + (master.getRandom().nextDouble() - 0.5) * 2;
|
||||
|
||||
double x = pet.getX() + Math.cos(angle) * distance;
|
||||
double z = pet.getZ() + Math.sin(angle) * distance;
|
||||
|
||||
// FIX: Use safe ground finder instead of heightmap (works indoors)
|
||||
int groundY = findSafeGroundY(
|
||||
pet.level(),
|
||||
(int) x,
|
||||
(int) z,
|
||||
(int) pet.getY() + 10
|
||||
);
|
||||
double y = groundY;
|
||||
|
||||
// Move to the spot at slower speed (wandering, not chasing)
|
||||
master.getNavigation().moveTo(x, y, z, FOLLOW_SPEED * 0.6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleport master near pet when too far (same dimension).
|
||||
*/
|
||||
private void teleportNearPet(ServerPlayer pet) {
|
||||
double angle = master.getRandom().nextDouble() * Math.PI * 2;
|
||||
double distance = MIN_DISTANCE + master.getRandom().nextDouble() * 2;
|
||||
|
||||
double x = pet.getX() + Math.cos(angle) * distance;
|
||||
double z = pet.getZ() + Math.sin(angle) * distance;
|
||||
|
||||
// FIX: Use safe ground finder instead of heightmap (works indoors)
|
||||
int groundY = findSafeGroundY(
|
||||
pet.level(),
|
||||
(int) x,
|
||||
(int) z,
|
||||
(int) pet.getY() + 10
|
||||
);
|
||||
double y = groundY;
|
||||
|
||||
master.teleportTo(x, y, z);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterFollowPlayerGoal] {} teleported near pet {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleport master to pet's dimension.
|
||||
* Used when pet changes dimension (nether, end, etc.)
|
||||
*/
|
||||
private void teleportToPetDimension(ServerPlayer pet) {
|
||||
// Calculate position near pet
|
||||
double angle = master.getRandom().nextDouble() * Math.PI * 2;
|
||||
double distance = MIN_DISTANCE + master.getRandom().nextDouble() * 2;
|
||||
|
||||
double x = pet.getX() + Math.cos(angle) * distance;
|
||||
double z = pet.getZ() + Math.sin(angle) * distance;
|
||||
|
||||
// FIX: Use safe ground finder instead of heightmap (works indoors)
|
||||
int groundY = findSafeGroundY(
|
||||
pet.level(),
|
||||
(int) x,
|
||||
(int) z,
|
||||
(int) pet.getY() + 10
|
||||
);
|
||||
double y = groundY;
|
||||
|
||||
// Create position with dimension info
|
||||
Position targetPos = new Position(
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
master.getYRot(),
|
||||
master.getXRot(),
|
||||
pet.level().dimension()
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterFollowPlayerGoal] {} teleporting to pet's dimension: {} -> {}",
|
||||
master.getNpcName(),
|
||||
master.level().dimension().location(),
|
||||
pet.level().dimension().location()
|
||||
);
|
||||
|
||||
// Use TeleportHelper for cross-dimension teleportation
|
||||
TeleportHelper.teleportEntity(master, targetPos);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterFollowPlayerGoal] {} arrived in {} near pet {}",
|
||||
master.getNpcName(),
|
||||
pet.level().dimension().location(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a safe ground Y position by scanning downward.
|
||||
* Works correctly indoors (doesn't return roof height like heightmap).
|
||||
*
|
||||
* @param level The level
|
||||
* @param x Target X coordinate
|
||||
* @param z Target Z coordinate
|
||||
* @param startY Starting Y to scan from (usually entity Y + some offset)
|
||||
* @return A safe Y position with solid ground below and 2 air blocks above
|
||||
*/
|
||||
private int findSafeGroundY(Level level, int x, int z, int startY) {
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(
|
||||
x,
|
||||
startY,
|
||||
z
|
||||
);
|
||||
|
||||
// Scan downward to find a solid floor with 2 air blocks above
|
||||
for (int y = startY; y > level.getMinBuildHeight(); y--) {
|
||||
pos.setY(y);
|
||||
BlockState below = level.getBlockState(pos.below());
|
||||
BlockState at = level.getBlockState(pos);
|
||||
BlockState above = level.getBlockState(pos.above());
|
||||
|
||||
// Solid floor + 2 blocks of clearance above
|
||||
if (below.isSolid() && !at.isSolid() && !above.isSolid()) {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
return startY; // Fallback if nothing found
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import com.tiedup.remake.items.base.BindVariant;
|
||||
import com.tiedup.remake.state.HumanChairHelper;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import java.util.EnumSet;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.effect.MobEffectInstance;
|
||||
import net.minecraft.world.effect.MobEffects;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to use the pet as human furniture.
|
||||
*
|
||||
* The pet is forced on all fours (dogbind pose without visible restraint),
|
||||
* frozen in place, while the Master sits on them.
|
||||
* Lasts ~2 minutes. During this time the Master does idle behaviors:
|
||||
* looks around, comments, observes nearby entities.
|
||||
*
|
||||
* Flow:
|
||||
* 1. APPROACHING: Master walks to pet
|
||||
* 2. SETTLING: Master positions on pet, applies pose + freeze
|
||||
* 3. SITTING: Main phase - Master sits, does idle stuff
|
||||
* 4. GETTING_UP: Master stands, cleanup
|
||||
*/
|
||||
public class MasterHumanChairGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Duration of the sitting phase (ticks) - ~2 minutes */
|
||||
private static final int SITTING_DURATION = 2400;
|
||||
|
||||
/** Time to settle into position (ticks) */
|
||||
private static final int SETTLE_DURATION = 40;
|
||||
|
||||
/** Approach distance to start settling */
|
||||
private static final double APPROACH_DISTANCE = 1.5;
|
||||
|
||||
/** Interval between idle comments (ticks) */
|
||||
private static final int IDLE_COMMENT_INTERVAL = 400; // 20 seconds
|
||||
|
||||
/** Y offset to place the master on the pet's back (compensates sitting animation lowering ~0.25 blocks) */
|
||||
private static final double PET_BACK_Y_OFFSET = 0.55;
|
||||
|
||||
/** NBT tag to mark the temporary chair dogbind */
|
||||
private static final String NBT_HUMAN_CHAIR_BIND = HumanChairHelper.NBT_KEY;
|
||||
|
||||
private enum Phase {
|
||||
APPROACHING,
|
||||
SETTLING,
|
||||
SITTING,
|
||||
GETTING_UP,
|
||||
}
|
||||
|
||||
private Phase phase = Phase.APPROACHING;
|
||||
private int phaseTimer = 0;
|
||||
private int totalTimer = 0;
|
||||
private int lastCommentTime = 0;
|
||||
private boolean hasAppliedPose = false;
|
||||
/** Pet facing direction locked at pose start — decoupled from camera */
|
||||
private float lockedPetFacing = 0f;
|
||||
|
||||
/** Persistent look target — refreshed every tick to prevent vanilla LookControl snap-back */
|
||||
private double lastLookX, lastLookY, lastLookZ;
|
||||
|
||||
public MasterHumanChairGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
return (
|
||||
master.hasPet() &&
|
||||
master.getPetPlayer() != null &&
|
||||
master.getStateManager().getCurrentState() ==
|
||||
MasterState.HUMAN_CHAIR
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
return (
|
||||
pet != null &&
|
||||
pet.isAlive() &&
|
||||
master.getStateManager().getCurrentState() ==
|
||||
MasterState.HUMAN_CHAIR &&
|
||||
phase != Phase.GETTING_UP
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.phase = Phase.APPROACHING;
|
||||
this.phaseTimer = 0;
|
||||
this.totalTimer = 0;
|
||||
this.lastCommentTime = 0;
|
||||
this.hasAppliedPose = false;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterHumanChairGoal] {} starting human chair",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
// Cleanup: remove pose, effects, and standing up
|
||||
cleanupPetPose();
|
||||
|
||||
// Master stands up
|
||||
master.setSitting(false);
|
||||
master.setNoGravity(false);
|
||||
master.noPhysics = false;
|
||||
// Return to following
|
||||
if (master.hasPet()) {
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
|
||||
this.phase = Phase.APPROACHING;
|
||||
this.phaseTimer = 0;
|
||||
this.totalTimer = 0;
|
||||
this.hasAppliedPose = false;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterHumanChairGoal] {} ended human chair",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
totalTimer++;
|
||||
|
||||
switch (phase) {
|
||||
case APPROACHING -> tickApproaching(pet);
|
||||
case SETTLING -> tickSettling(pet);
|
||||
case SITTING -> tickSitting(pet);
|
||||
case GETTING_UP -> {
|
||||
} // handled by canContinueToUse returning false
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PHASE: APPROACHING
|
||||
// ========================================
|
||||
|
||||
private void tickApproaching(ServerPlayer pet) {
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
double distSq = master.distanceToSqr(pet);
|
||||
if (distSq > APPROACH_DISTANCE * APPROACH_DISTANCE) {
|
||||
master.getNavigation().moveTo(pet, 1.0);
|
||||
} else {
|
||||
// Close enough - start settling
|
||||
master.getNavigation().stop();
|
||||
phase = Phase.SETTLING;
|
||||
phaseTimer = 0;
|
||||
|
||||
// Tell pet what's about to happen
|
||||
DialogueBridge.talkTo(master, pet, "petplay.human_chair_command");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PHASE: SETTLING
|
||||
// ========================================
|
||||
|
||||
private void tickSettling(ServerPlayer pet) {
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
master.getNavigation().stop();
|
||||
|
||||
// Apply the pose on first tick of settling
|
||||
if (!hasAppliedPose) {
|
||||
lockedPetFacing = pet.getYRot();
|
||||
applyPetPose(pet);
|
||||
hasAppliedPose = true;
|
||||
}
|
||||
|
||||
phaseTimer++;
|
||||
|
||||
if (phaseTimer >= SETTLE_DURATION) {
|
||||
// Settled - Master sits down
|
||||
master.setSitting(true);
|
||||
master.setNoGravity(true);
|
||||
master.noPhysics = true;
|
||||
|
||||
// Position master centered on pet's back
|
||||
positionOnPet(pet);
|
||||
|
||||
// Initialize look target: forward along sideways direction
|
||||
float sideYaw = lockedPetFacing + 90f;
|
||||
float sideRad = (float) Math.toRadians(sideYaw);
|
||||
lastLookX = master.getX() + (-Math.sin(sideRad)) * 5;
|
||||
lastLookY = master.getEyeY();
|
||||
lastLookZ = master.getZ() + Math.cos(sideRad) * 5;
|
||||
|
||||
phase = Phase.SITTING;
|
||||
phaseTimer = 0;
|
||||
lastCommentTime = totalTimer;
|
||||
|
||||
DialogueBridge.talkTo(master, pet, "petplay.human_chair_sitting");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterHumanChairGoal] {} settled on {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PHASE: SITTING
|
||||
// ========================================
|
||||
|
||||
private void tickSitting(ServerPlayer pet) {
|
||||
phaseTimer++;
|
||||
|
||||
// Force master position on pet every tick to prevent drifting
|
||||
positionOnPet(pet);
|
||||
|
||||
// Refresh slowness to keep pet frozen
|
||||
if (phaseTimer % 60 == 0) {
|
||||
pet.addEffect(
|
||||
new MobEffectInstance(
|
||||
MobEffects.MOVEMENT_SLOWDOWN,
|
||||
80,
|
||||
255,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
pet.addEffect(
|
||||
new MobEffectInstance(
|
||||
MobEffects.JUMP,
|
||||
80,
|
||||
128,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Idle behaviors during sitting (may update lastLook target)
|
||||
tickIdleBehavior(pet);
|
||||
|
||||
// Refresh look target every tick to prevent vanilla LookControl snap-back
|
||||
// (vanilla lookAtCooldown = 2 → target expires after 2 ticks → head snaps to center)
|
||||
master
|
||||
.getLookControl()
|
||||
.setLookAt(lastLookX, lastLookY, lastLookZ, 10f, 10f);
|
||||
|
||||
// Check if duration expired
|
||||
if (phaseTimer >= SITTING_DURATION) {
|
||||
phase = Phase.GETTING_UP;
|
||||
|
||||
DialogueBridge.talkTo(master, pet, "petplay.human_chair_end");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterHumanChairGoal] {} getting up from {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IDLE BEHAVIORS DURING SITTING
|
||||
// ========================================
|
||||
|
||||
private void tickIdleBehavior(ServerPlayer pet) {
|
||||
// Periodically look at nearby entities or comment
|
||||
if (totalTimer - lastCommentTime >= IDLE_COMMENT_INTERVAL) {
|
||||
lastCommentTime = totalTimer;
|
||||
|
||||
// 50% chance: look at a nearby entity, 50% chance: idle comment
|
||||
if (master.getRandom().nextFloat() < 0.5f) {
|
||||
lookAtNearbyEntity(pet);
|
||||
} else {
|
||||
// Idle comment about sitting/pet
|
||||
String[] idleDialogues = {
|
||||
"petplay.human_chair_idle",
|
||||
"idle.content",
|
||||
"idle.observing",
|
||||
};
|
||||
String dialogue = idleDialogues[master
|
||||
.getRandom()
|
||||
.nextInt(idleDialogues.length)];
|
||||
DialogueBridge.talkTo(master, pet, dialogue);
|
||||
}
|
||||
}
|
||||
|
||||
// Between comments, slowly look around
|
||||
if (phaseTimer % 100 == 0) {
|
||||
float randomYaw =
|
||||
master.getYRot() +
|
||||
(master.getRandom().nextFloat() - 0.5f) * 120;
|
||||
lastLookX = master.getX() + Math.sin(Math.toRadians(randomYaw)) * 5;
|
||||
lastLookY = master.getEyeY();
|
||||
lastLookZ = master.getZ() + Math.cos(Math.toRadians(randomYaw)) * 5;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look at a random nearby living entity (NPC, player, mob).
|
||||
*/
|
||||
private void lookAtNearbyEntity(ServerPlayer pet) {
|
||||
AABB searchBox = master.getBoundingBox().inflate(12.0);
|
||||
var nearbyEntities = master
|
||||
.level()
|
||||
.getEntitiesOfClass(
|
||||
LivingEntity.class,
|
||||
searchBox,
|
||||
e -> e != master && e != pet && e.isAlive()
|
||||
);
|
||||
|
||||
if (!nearbyEntities.isEmpty()) {
|
||||
LivingEntity target = nearbyEntities.get(
|
||||
master.getRandom().nextInt(nearbyEntities.size())
|
||||
);
|
||||
lastLookX = target.getX();
|
||||
lastLookY = target.getEyeY();
|
||||
lastLookZ = target.getZ();
|
||||
|
||||
// Comment about the entity
|
||||
if (target instanceof Player) {
|
||||
DialogueBridge.talkTo(
|
||||
master,
|
||||
pet,
|
||||
"petplay.human_chair_notice_player"
|
||||
);
|
||||
} else {
|
||||
DialogueBridge.talkTo(
|
||||
master,
|
||||
pet,
|
||||
"petplay.human_chair_notice_entity"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// POSITIONING
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Position master on pet's back, facing sideways (perpendicular).
|
||||
* Called every tick during SITTING to prevent any drift from collision/physics.
|
||||
* Body rotation is locked so only the head turns when looking at entities.
|
||||
*/
|
||||
private void positionOnPet(ServerPlayer pet) {
|
||||
// Use locked facing — decoupled from player camera
|
||||
float petYaw = (float) Math.toRadians(lockedPetFacing);
|
||||
double offsetX = -Math.sin(petYaw) * 0.15;
|
||||
double offsetZ = Math.cos(petYaw) * 0.15;
|
||||
|
||||
// Face perpendicular to the pet (sitting sideways)
|
||||
float sideYaw = lockedPetFacing + 90f;
|
||||
|
||||
master.moveTo(
|
||||
pet.getX() + offsetX,
|
||||
pet.getY() + PET_BACK_Y_OFFSET,
|
||||
pet.getZ() + offsetZ,
|
||||
sideYaw,
|
||||
0.0F
|
||||
);
|
||||
|
||||
// Lock body rotation so look-at only turns the head
|
||||
master.yBodyRot = sideYaw;
|
||||
master.yBodyRotO = sideYaw;
|
||||
|
||||
// Zero out velocity to prevent physics drift
|
||||
master.setDeltaMovement(0, 0, 0);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// POSE MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Apply the human chair pose to the pet:
|
||||
* - Temporary invisible dogbind for the on-all-fours animation
|
||||
* - Slowness 255 to freeze movement
|
||||
* - Jump boost negative to prevent jumping
|
||||
*/
|
||||
private void applyPetPose(ServerPlayer pet) {
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState == null) return;
|
||||
|
||||
// Apply invisible dogbind for the pose animation
|
||||
if (!bindState.isTiedUp()) {
|
||||
ItemStack dogbind = new ItemStack(
|
||||
ModItems.getBind(BindVariant.DOGBINDER)
|
||||
);
|
||||
CompoundTag tag = dogbind.getOrCreateTag();
|
||||
tag.putBoolean(NBT_HUMAN_CHAIR_BIND, true);
|
||||
tag.putBoolean("tempMasterEvent", true);
|
||||
tag.putLong(
|
||||
"expirationTime",
|
||||
master.level().getGameTime() + SITTING_DURATION + 200
|
||||
);
|
||||
tag.putUUID("masterUUID", master.getUUID());
|
||||
tag.putFloat(HumanChairHelper.NBT_FACING_KEY, pet.getYRot());
|
||||
bindState.equip(BodyRegionV2.ARMS, dogbind);
|
||||
}
|
||||
|
||||
// Freeze the pet
|
||||
pet.addEffect(
|
||||
new MobEffectInstance(
|
||||
MobEffects.MOVEMENT_SLOWDOWN,
|
||||
SITTING_DURATION + 100,
|
||||
255,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
pet.addEffect(
|
||||
new MobEffectInstance(
|
||||
MobEffects.JUMP,
|
||||
SITTING_DURATION + 100,
|
||||
128,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterHumanChairGoal] Applied chair pose to {}",
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the human chair pose from the pet.
|
||||
*/
|
||||
private void cleanupPetPose() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Remove the temporary dogbind
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState != null && bindState.isTiedUp()) {
|
||||
ItemStack bind = bindState.getEquipment(BodyRegionV2.ARMS);
|
||||
if (!bind.isEmpty()) {
|
||||
CompoundTag tag = bind.getTag();
|
||||
if (tag != null && tag.getBoolean(NBT_HUMAN_CHAIR_BIND)) {
|
||||
bindState.unequip(BodyRegionV2.ARMS);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterHumanChairGoal] Removed chair bind from {}",
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove slowness and jump effects
|
||||
pet.removeEffect(MobEffects.MOVEMENT_SLOWDOWN);
|
||||
pet.removeEffect(MobEffects.JUMP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is a human chair temporary bind.
|
||||
*/
|
||||
public static boolean isHumanChairBind(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean(NBT_HUMAN_CHAIR_BIND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.items.ItemTaser;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.entity.monster.Creeper;
|
||||
import net.minecraft.world.entity.monster.Monster;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/**
|
||||
* AI Goal for Masters to hunt and kill monsters near their pet.
|
||||
*
|
||||
* Purpose: Protect the pet player from hostile mobs
|
||||
*
|
||||
* This goal:
|
||||
* 1. Only activates when master has a pet
|
||||
* 2. Scans for monsters near the pet (not the master)
|
||||
* 3. Equips taser and attacks with stun effects
|
||||
* 4. Returns to pet after killing monster
|
||||
*
|
||||
* Combat bonuses vs monsters:
|
||||
* - 2x damage dealt (masters are elite fighters)
|
||||
* - Taser stun effects (Slowness + Weakness)
|
||||
* - Creeper explosion cancelled on hit
|
||||
*
|
||||
* Priority: 1 (high - pet protection is paramount)
|
||||
*/
|
||||
public class MasterHuntMonstersGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
private LivingEntity targetMonster;
|
||||
|
||||
/** Scan radius around pet */
|
||||
private static final int SCAN_RADIUS = 12;
|
||||
|
||||
/** Ticks between attacks */
|
||||
private static final int ATTACK_COOLDOWN = 15;
|
||||
|
||||
/** Damage multiplier against monsters */
|
||||
private static final float MONSTER_DAMAGE_MULTIPLIER = 2.0f;
|
||||
|
||||
/** Current attack cooldown timer */
|
||||
private int attackCooldown = 0;
|
||||
|
||||
public MasterHuntMonstersGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
// Must have a pet to protect
|
||||
if (!master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null || !pet.isAlive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find monsters near the pet (not the master)
|
||||
this.targetMonster = findNearestMonsterNearPet(pet);
|
||||
return this.targetMonster != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (this.targetMonster == null || !this.targetMonster.isAlive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop if pet is gone
|
||||
if (!master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop if monster is too far from master (chase limit)
|
||||
return master.distanceToSqr(this.targetMonster) < 400; // 20 blocks
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.attackCooldown = 0;
|
||||
|
||||
// Equip taser
|
||||
this.master.setItemInHand(
|
||||
InteractionHand.MAIN_HAND,
|
||||
new ItemStack(ModItems.TASER.get())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.targetMonster = null;
|
||||
this.master.getNavigation().stop();
|
||||
|
||||
// Unequip taser
|
||||
this.master.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (this.targetMonster == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Look at the monster
|
||||
this.master.getLookControl().setLookAt(this.targetMonster);
|
||||
|
||||
double distSq = this.master.distanceToSqr(this.targetMonster);
|
||||
|
||||
// Move closer if not in attack range
|
||||
if (distSq > 4.0) {
|
||||
this.master.getNavigation().moveTo(this.targetMonster, 1.3);
|
||||
} else {
|
||||
// In attack range - stop and attack
|
||||
this.master.getNavigation().stop();
|
||||
|
||||
if (this.attackCooldown <= 0) {
|
||||
attackMonsterWithBonus(this.targetMonster);
|
||||
this.attackCooldown = ATTACK_COOLDOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement cooldown
|
||||
if (this.attackCooldown > 0) {
|
||||
this.attackCooldown--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attack a monster with taser - bonus damage and stun effects.
|
||||
* Masters deal 2x damage to monsters and apply taser stun.
|
||||
* Creepers have their explosion cancelled.
|
||||
*
|
||||
* @param target The monster to attack
|
||||
*/
|
||||
private void attackMonsterWithBonus(LivingEntity target) {
|
||||
// Swing arm animation
|
||||
this.master.swing(InteractionHand.MAIN_HAND);
|
||||
|
||||
// Get base attack damage and apply multiplier
|
||||
float baseDamage = (float) this.master.getAttributeValue(
|
||||
Attributes.ATTACK_DAMAGE
|
||||
);
|
||||
float bonusDamage = baseDamage * MONSTER_DAMAGE_MULTIPLIER;
|
||||
|
||||
// Deal damage directly with bonus
|
||||
boolean damaged = target.hurt(
|
||||
this.master.damageSources().mobAttack(this.master),
|
||||
bonusDamage
|
||||
);
|
||||
|
||||
// Apply taser effects if damage was dealt
|
||||
if (damaged) {
|
||||
ItemStack heldItem = this.master.getItemInHand(
|
||||
InteractionHand.MAIN_HAND
|
||||
);
|
||||
if (heldItem.getItem() instanceof ItemTaser taserItem) {
|
||||
taserItem.hurtEnemy(heldItem, target, this.master);
|
||||
}
|
||||
|
||||
// Special: Cancel creeper explosion
|
||||
if (target instanceof Creeper creeper) {
|
||||
creeper.setSwellDir(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nearest monster within scan radius of the pet.
|
||||
*
|
||||
* @param pet The pet player to protect
|
||||
* @return The nearest monster, or null if none found
|
||||
*/
|
||||
private LivingEntity findNearestMonsterNearPet(ServerPlayer pet) {
|
||||
AABB searchBox = pet.getBoundingBox().inflate(SCAN_RADIUS);
|
||||
|
||||
List<Monster> monsters = this.master.level().getEntitiesOfClass(
|
||||
Monster.class,
|
||||
searchBox,
|
||||
m -> m.isAlive() && !m.isSpectator()
|
||||
);
|
||||
|
||||
if (monsters.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find nearest to pet
|
||||
Monster nearest = null;
|
||||
double nearestDist = Double.MAX_VALUE;
|
||||
|
||||
for (Monster m : monsters) {
|
||||
double dist = pet.distanceToSqr(m);
|
||||
if (dist < nearestDist) {
|
||||
nearestDist = dist;
|
||||
nearest = m;
|
||||
}
|
||||
}
|
||||
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import java.util.EnumSet;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to perform idle micro-behaviors.
|
||||
*
|
||||
* Fills the gap between tasks/events with small, natural-looking actions.
|
||||
* Lower priority than tasks/events/inspect, but higher than look-at goals.
|
||||
*/
|
||||
public class MasterIdleBehaviorGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Chance to start idle behavior per tick */
|
||||
private static final float IDLE_CHANCE = 0.003f; // ~6% per 10 seconds
|
||||
|
||||
/** Minimum time between idle behaviors (ticks) - 20 seconds */
|
||||
private static final int IDLE_COOLDOWN = 400;
|
||||
|
||||
/** Distance to approach pet for close behaviors */
|
||||
private static final double APPROACH_DISTANCE = 2.0;
|
||||
|
||||
/** Distance to walk away for CHECK_SURROUNDINGS */
|
||||
private static final double SCOUT_DISTANCE = 8.0;
|
||||
|
||||
/**
|
||||
* Types of idle micro-behaviors.
|
||||
*/
|
||||
private enum IdleBehavior {
|
||||
LOOK_AROUND(60), // 3s - random look angles
|
||||
EXAMINE_PET(80), // 4s - walk to pet, stare
|
||||
PAT_HEAD(60), // 3s - approach, pat, step back
|
||||
ADJUST_COLLAR(60), // 3s - approach, adjust collar dialogue
|
||||
IDLE_COMMENT(20), // 1s - random idle comment
|
||||
STRETCH(40), // 2s - stop, do nothing
|
||||
CHECK_SURROUNDINGS(100); // 5s - walk away, look around, return
|
||||
|
||||
final int duration;
|
||||
|
||||
IdleBehavior(int duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
|
||||
private long lastIdleTime = 0;
|
||||
private IdleBehavior currentBehavior = null;
|
||||
private int behaviorTimer = 0;
|
||||
private boolean hasPerformedAction = false;
|
||||
|
||||
/** For CHECK_SURROUNDINGS: the position to walk to */
|
||||
private Vec3 scoutTarget = null;
|
||||
/** For CHECK_SURROUNDINGS: whether we've reached the scout target */
|
||||
private boolean reachedScoutTarget = false;
|
||||
|
||||
public MasterIdleBehaviorGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
if (!master.hasPet()) return false;
|
||||
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
|
||||
// Only idle behaviors from FOLLOWING or OBSERVING
|
||||
if (state != MasterState.FOLLOWING && state != MasterState.OBSERVING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No idle behaviors if there's an active task
|
||||
if (master.hasActiveTask()) return false;
|
||||
|
||||
// No idle behaviors during cold shoulder
|
||||
if (master.isGivingColdShoulder()) return false;
|
||||
|
||||
// Check cooldown
|
||||
long currentTime = master.level().getGameTime();
|
||||
if (currentTime - lastIdleTime < IDLE_COOLDOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Random chance (modulated by engagement cadence)
|
||||
float multiplier = master.getEngagementMultiplier();
|
||||
return (
|
||||
multiplier > 0 &&
|
||||
master.getRandom().nextFloat() < IDLE_CHANCE * multiplier
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (!master.hasPet()) return false;
|
||||
if (currentBehavior == null) return false;
|
||||
if (behaviorTimer >= currentBehavior.duration) return false;
|
||||
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
return state == MasterState.FOLLOWING || state == MasterState.OBSERVING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.behaviorTimer = 0;
|
||||
this.hasPerformedAction = false;
|
||||
this.scoutTarget = null;
|
||||
this.reachedScoutTarget = false;
|
||||
|
||||
// Select random idle behavior
|
||||
IdleBehavior[] behaviors = IdleBehavior.values();
|
||||
this.currentBehavior = behaviors[master
|
||||
.getRandom()
|
||||
.nextInt(behaviors.length)];
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterIdleBehaviorGoal] {} starting idle: {}",
|
||||
master.getNpcName(),
|
||||
currentBehavior
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
lastIdleTime = master.level().getGameTime();
|
||||
master.markEngagement();
|
||||
|
||||
this.currentBehavior = null;
|
||||
this.behaviorTimer = 0;
|
||||
this.hasPerformedAction = false;
|
||||
this.scoutTarget = null;
|
||||
this.reachedScoutTarget = false;
|
||||
|
||||
master.getNavigation().stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
behaviorTimer++;
|
||||
|
||||
switch (currentBehavior) {
|
||||
case LOOK_AROUND -> tickLookAround();
|
||||
case EXAMINE_PET -> tickExaminePet(pet);
|
||||
case PAT_HEAD -> tickPatHead(pet);
|
||||
case ADJUST_COLLAR -> tickAdjustCollar(pet);
|
||||
case IDLE_COMMENT -> tickIdleComment(pet);
|
||||
case STRETCH -> tickStretch();
|
||||
case CHECK_SURROUNDINGS -> tickCheckSurroundings(pet);
|
||||
}
|
||||
}
|
||||
|
||||
private void tickLookAround() {
|
||||
// Randomly change look direction every 20 ticks
|
||||
if (behaviorTimer % 20 == 1) {
|
||||
float yaw =
|
||||
master.getYRot() +
|
||||
(master.getRandom().nextFloat() - 0.5f) * 120;
|
||||
float pitch = (master.getRandom().nextFloat() - 0.5f) * 40;
|
||||
master
|
||||
.getLookControl()
|
||||
.setLookAt(
|
||||
master.getX() + Math.sin(Math.toRadians(-yaw)) * 5,
|
||||
master.getEyeY() + pitch * 0.1,
|
||||
master.getZ() + Math.cos(Math.toRadians(-yaw)) * 5
|
||||
);
|
||||
}
|
||||
master.getNavigation().stop();
|
||||
}
|
||||
|
||||
private void tickExaminePet(ServerPlayer pet) {
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > APPROACH_DISTANCE) {
|
||||
master.getNavigation().moveTo(pet, 0.8);
|
||||
} else {
|
||||
master.getNavigation().stop();
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
if (!hasPerformedAction) {
|
||||
DialogueBridge.talkTo(master, pet, "idle.examine");
|
||||
hasPerformedAction = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickPatHead(ServerPlayer pet) {
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > APPROACH_DISTANCE) {
|
||||
master.getNavigation().moveTo(pet, 0.8);
|
||||
} else {
|
||||
master.getNavigation().stop();
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
if (!hasPerformedAction) {
|
||||
DialogueBridge.talkTo(master, pet, "idle.pat_head");
|
||||
hasPerformedAction = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickAdjustCollar(ServerPlayer pet) {
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > APPROACH_DISTANCE) {
|
||||
master.getNavigation().moveTo(pet, 0.8);
|
||||
} else {
|
||||
master.getNavigation().stop();
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
if (!hasPerformedAction) {
|
||||
DialogueBridge.talkTo(master, pet, "idle.adjust_collar");
|
||||
hasPerformedAction = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickIdleComment(ServerPlayer pet) {
|
||||
master.getNavigation().stop();
|
||||
if (!hasPerformedAction) {
|
||||
// Pick randomly between existing idle dialogues
|
||||
String dialogueId = master.getRandom().nextBoolean()
|
||||
? "idle.content"
|
||||
: "idle.bored";
|
||||
DialogueBridge.talkTo(master, pet, dialogueId);
|
||||
hasPerformedAction = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void tickStretch() {
|
||||
// Do nothing - natural pause
|
||||
master.getNavigation().stop();
|
||||
}
|
||||
|
||||
private void tickCheckSurroundings(ServerPlayer pet) {
|
||||
if (scoutTarget == null) {
|
||||
// Pick a random direction to walk
|
||||
double angle = master.getRandom().nextDouble() * Math.PI * 2;
|
||||
double dist = 5 + master.getRandom().nextDouble() * 3; // 5-8 blocks
|
||||
scoutTarget = new Vec3(
|
||||
master.getX() + Math.cos(angle) * dist,
|
||||
master.getY(),
|
||||
master.getZ() + Math.sin(angle) * dist
|
||||
);
|
||||
}
|
||||
|
||||
if (!reachedScoutTarget) {
|
||||
// Walk to scout target
|
||||
master
|
||||
.getNavigation()
|
||||
.moveTo(scoutTarget.x, scoutTarget.y, scoutTarget.z, 0.8);
|
||||
|
||||
// Check if we arrived or enough time passed
|
||||
double distToTarget = master.position().distanceTo(scoutTarget);
|
||||
if (
|
||||
distToTarget < 2.0 ||
|
||||
behaviorTimer > currentBehavior.duration / 2
|
||||
) {
|
||||
reachedScoutTarget = true;
|
||||
master.getNavigation().stop();
|
||||
|
||||
if (!hasPerformedAction) {
|
||||
DialogueBridge.talkTo(
|
||||
master,
|
||||
pet,
|
||||
"idle.check_surroundings"
|
||||
);
|
||||
hasPerformedAction = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Look around at scout target, then return is handled by goal ending
|
||||
// (follow goal will bring master back to pet)
|
||||
if (behaviorTimer % 20 == 0) {
|
||||
float yaw =
|
||||
master.getYRot() +
|
||||
(master.getRandom().nextFloat() - 0.5f) * 180;
|
||||
master
|
||||
.getLookControl()
|
||||
.setLookAt(
|
||||
master.getX() + Math.sin(Math.toRadians(-yaw)) * 5,
|
||||
master.getEyeY(),
|
||||
master.getZ() + Math.cos(Math.toRadians(-yaw)) * 5
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to inspect pet's inventory for contraband.
|
||||
*
|
||||
* Contraband items:
|
||||
* - Lockpicks, knives, shears
|
||||
* - Weapons (swords, axes)
|
||||
* - Items tagged with tiedup:escape_tool or tiedup:weapon
|
||||
*
|
||||
* Behavior:
|
||||
* 1. Periodically checks inventory (random chance)
|
||||
* 2. Confiscates contraband items
|
||||
* 3. Punishes if contraband found
|
||||
*/
|
||||
public class MasterInventoryInspectGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Chance to start inspection when following (per tick) */
|
||||
private static final float INSPECT_CHANCE = 0.002f; // ~4% per 10 seconds
|
||||
|
||||
/** Minimum time between inspections (ticks) - 2.5 minutes */
|
||||
private static final int INSPECT_COOLDOWN = 3000;
|
||||
|
||||
/** Distance to inspect */
|
||||
private static final double INSPECT_DISTANCE = 2.0;
|
||||
|
||||
/** Inspection duration (ticks) */
|
||||
private static final int INSPECT_DURATION = 40; // 2 seconds
|
||||
|
||||
private int inspectTimer = 0;
|
||||
private long lastInspectTime = 0;
|
||||
private List<ItemStack> confiscatedItems = new ArrayList<>();
|
||||
|
||||
/** Tag for escape tools */
|
||||
private static final TagKey<Item> ESCAPE_TOOL_TAG = TagKey.create(
|
||||
Registries.ITEM,
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "escape_tool")
|
||||
);
|
||||
|
||||
/** Tag for weapons */
|
||||
private static final TagKey<Item> WEAPON_TAG = TagKey.create(
|
||||
Registries.ITEM,
|
||||
ResourceLocation.fromNamespaceAndPath("tiedup", "weapon")
|
||||
);
|
||||
|
||||
public MasterInventoryInspectGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
if (!master.hasPet()) return false;
|
||||
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
|
||||
// Already inspecting
|
||||
if (state == MasterState.INSPECT) return true;
|
||||
|
||||
// Can start new inspection
|
||||
if (state != MasterState.FOLLOWING && state != MasterState.OBSERVING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
long currentTime = master.level().getGameTime();
|
||||
if (currentTime - lastInspectTime < INSPECT_COOLDOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Random chance to inspect (modulated by engagement cadence)
|
||||
float multiplier = master.getEngagementMultiplier();
|
||||
return (
|
||||
multiplier > 0 &&
|
||||
master.getRandom().nextFloat() < INSPECT_CHANCE * multiplier
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
return (
|
||||
master.hasPet() &&
|
||||
master.getStateManager().getCurrentState() == MasterState.INSPECT &&
|
||||
inspectTimer < INSPECT_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.inspectTimer = 0;
|
||||
this.confiscatedItems.clear();
|
||||
master.setMasterState(MasterState.INSPECT);
|
||||
master.markEngagement();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterInventoryInspectGoal] {} starting inspection",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
lastInspectTime = master.level().getGameTime();
|
||||
|
||||
// Announce results
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet != null && !confiscatedItems.isEmpty()) {
|
||||
pet.sendSystemMessage(
|
||||
Component.literal(
|
||||
master.getNpcName() +
|
||||
" confiscated " +
|
||||
confiscatedItems.size() +
|
||||
" contraband item(s) from you!"
|
||||
).withStyle(
|
||||
Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR)
|
||||
)
|
||||
);
|
||||
|
||||
// Transition to punish state
|
||||
master.setMasterState(MasterState.PUNISH);
|
||||
} else if (master.hasPet()) {
|
||||
// Clean inspection
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
|
||||
this.inspectTimer = 0;
|
||||
this.confiscatedItems.clear();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterInventoryInspectGoal] {} inspection complete",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Look at pet
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
double distSq = master.distanceToSqr(pet);
|
||||
|
||||
if (distSq > INSPECT_DISTANCE * INSPECT_DISTANCE) {
|
||||
// Move close
|
||||
master.getNavigation().moveTo(pet, 1.0);
|
||||
} else {
|
||||
master.getNavigation().stop();
|
||||
|
||||
// Perform inspection on first tick when close
|
||||
if (inspectTimer == 0) {
|
||||
performInspection(pet);
|
||||
}
|
||||
}
|
||||
|
||||
inspectTimer++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect pet's inventory and confiscate contraband.
|
||||
*/
|
||||
private void performInspection(ServerPlayer pet) {
|
||||
pet.sendSystemMessage(
|
||||
Component.literal(
|
||||
master.getNpcName() + " is inspecting your inventory..."
|
||||
).withStyle(Style.EMPTY.withColor(0xFFFF00))
|
||||
);
|
||||
|
||||
// Check all inventory slots
|
||||
for (int i = 0; i < pet.getInventory().getContainerSize(); i++) {
|
||||
ItemStack stack = pet.getInventory().getItem(i);
|
||||
if (stack.isEmpty()) continue;
|
||||
|
||||
if (isContraband(stack)) {
|
||||
// Confiscate
|
||||
confiscatedItems.add(stack.copy());
|
||||
pet.getInventory().setItem(i, ItemStack.EMPTY);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterInventoryInspectGoal] {} confiscated {} from {}",
|
||||
master.getNpcName(),
|
||||
stack.getDisplayName().getString(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is contraband.
|
||||
*/
|
||||
private boolean isContraband(ItemStack stack) {
|
||||
Item item = stack.getItem();
|
||||
|
||||
// Check tags
|
||||
if (stack.is(ESCAPE_TOOL_TAG) || stack.is(WEAPON_TAG)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check specific vanilla items
|
||||
if (
|
||||
item == Items.SHEARS ||
|
||||
item == Items.IRON_SWORD ||
|
||||
item == Items.DIAMOND_SWORD ||
|
||||
item == Items.NETHERITE_SWORD ||
|
||||
item == Items.IRON_AXE ||
|
||||
item == Items.DIAMOND_AXE ||
|
||||
item == Items.NETHERITE_AXE ||
|
||||
item == Items.BOW ||
|
||||
item == Items.CROSSBOW
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for mod items by registry name
|
||||
ResourceLocation registryName =
|
||||
net.minecraftforge.registries.ForgeRegistries.ITEMS.getKey(item);
|
||||
if (
|
||||
registryName != null && registryName.getNamespace().equals("tiedup")
|
||||
) {
|
||||
String path = registryName.getPath();
|
||||
// Lockpicks and knives are contraband
|
||||
if (path.contains("lockpick") || path.contains("knife")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import java.util.EnumSet;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to observe their pet player.
|
||||
*
|
||||
* In OBSERVING state, the Master watches the player intently.
|
||||
* This state prevents escape attempts from succeeding.
|
||||
*
|
||||
* The Master periodically enters DISTRACTED state, creating
|
||||
* windows where the player can attempt to struggle free.
|
||||
*/
|
||||
public class MasterObservePlayerGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** How long to observe before potentially doing something else (ticks) */
|
||||
private static final int OBSERVE_DURATION = 200; // 10 seconds
|
||||
|
||||
/** Chance to transition to OBSERVING each tick when FOLLOWING */
|
||||
private static final float OBSERVE_CHANCE = 0.005f; // ~10% per 10 seconds
|
||||
|
||||
private int observeTimer = 0;
|
||||
|
||||
public MasterObservePlayerGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
// Must have a pet
|
||||
if (!master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
|
||||
// If already observing, continue
|
||||
if (state == MasterState.OBSERVING) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Random chance to start observing when following
|
||||
if (state == MasterState.FOLLOWING) {
|
||||
return master.getRandom().nextFloat() < OBSERVE_CHANCE;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (!master.hasPet()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null || !pet.isAlive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Continue observing until timer expires
|
||||
return (
|
||||
observeTimer < OBSERVE_DURATION &&
|
||||
master.getStateManager().getCurrentState() == MasterState.OBSERVING
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.observeTimer = 0;
|
||||
master.setMasterState(MasterState.OBSERVING);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterObservePlayerGoal] {} started observing pet",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.observeTimer = 0;
|
||||
|
||||
// Return to following if still has pet
|
||||
if (
|
||||
master.hasPet() &&
|
||||
master.getStateManager().getCurrentState() == MasterState.OBSERVING
|
||||
) {
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterObservePlayerGoal] {} stopped observing",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Stare at pet intensely
|
||||
master.getLookControl().setLookAt(pet, 180.0F, 180.0F);
|
||||
|
||||
observeTimer++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.v2.V2Blocks;
|
||||
import com.tiedup.remake.v2.blocks.PetBedBlock;
|
||||
import com.tiedup.remake.v2.blocks.PetBowlBlock;
|
||||
import com.tiedup.remake.v2.blocks.PetBowlBlockEntity;
|
||||
import java.util.EnumSet;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to place Bowl/Bed blocks for pet.
|
||||
*
|
||||
* This goal is triggered by specific pet needs:
|
||||
* - Hungry: Place PET_BOWL, wait for pet to eat, then retrieve
|
||||
* - Tired: Place PET_BED, wait for pet to rest, then retrieve
|
||||
*
|
||||
* The Master places blocks temporarily and retrieves them after use.
|
||||
* Can be triggered automatically (pet hunger) or manually via menu.
|
||||
*
|
||||
* Uses V2 blocks:
|
||||
* - Bowl: V2Blocks.PET_BOWL for pet feeding
|
||||
* - Bed: V2Blocks.PET_BED for pet sleeping
|
||||
*/
|
||||
public class MasterPlaceBlockGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Current action being performed */
|
||||
private PlaceAction currentAction = PlaceAction.NONE;
|
||||
|
||||
/** Position where block was placed */
|
||||
private BlockPos placedBlockPos = null;
|
||||
|
||||
/** Timer for current action */
|
||||
private int actionTimer = 0;
|
||||
|
||||
/** Distance to place blocks from pet */
|
||||
private static final double PLACE_DISTANCE = 2.0;
|
||||
|
||||
/** Time to wait for pet to use the block (ticks) */
|
||||
private static final int USE_WAIT_TIME = 600; // 30 seconds (increased from 10s)
|
||||
|
||||
/** Time to wait before retrieving block (ticks) */
|
||||
private static final int RETRIEVE_DELAY = 100; // 5 seconds after use
|
||||
|
||||
private enum PlaceAction {
|
||||
NONE,
|
||||
PLACING_BOWL, // Walking to place bowl
|
||||
WAITING_EAT, // Waiting for pet to eat
|
||||
PLACING_PET_BED, // Walking to place pet bed
|
||||
WAITING_SLEEP, // Waiting for pet to rest
|
||||
RETRIEVING, // Picking up blocks
|
||||
}
|
||||
|
||||
public MasterPlaceBlockGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
// Must have a pet
|
||||
if (!master.hasPet()) return false;
|
||||
|
||||
// If an action is triggered manually (via triggerFeeding/triggerResting), always allow
|
||||
if (currentAction != PlaceAction.NONE) return true;
|
||||
|
||||
// For automatic triggers, must be in following state
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
if (state != MasterState.FOLLOWING) return false;
|
||||
|
||||
// Check pet needs (only trigger on actual needs)
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return false;
|
||||
|
||||
// Check if pet is hungry (food level < 10)
|
||||
if (pet.getFoodData().getFoodLevel() < 10) {
|
||||
currentAction = PlaceAction.PLACING_BOWL;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if pet is tired (could check for sleep deprivation effect, etc.)
|
||||
// For now, this is disabled - only triggered manually or via events
|
||||
// if (isPetTired(pet)) {
|
||||
// currentAction = PlaceAction.PLACING_PET_BED;
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (!master.hasPet()) return false;
|
||||
return currentAction != PlaceAction.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.actionTimer = 0;
|
||||
this.placedBlockPos = null;
|
||||
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Announce intent based on action
|
||||
switch (currentAction) {
|
||||
case PLACING_BOWL -> {
|
||||
DialogueBridge.talkTo(master, pet, "petplay.feeding");
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPlaceBlockGoal] {} placing food bowl for {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
case PLACING_PET_BED -> {
|
||||
DialogueBridge.talkTo(master, pet, "petplay.resting");
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPlaceBlockGoal] {} placing pet bed for {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
// Cleanup - retrieve any placed blocks
|
||||
if (
|
||||
placedBlockPos != null &&
|
||||
master.level() instanceof ServerLevel serverLevel
|
||||
) {
|
||||
serverLevel.setBlock(
|
||||
placedBlockPos,
|
||||
Blocks.AIR.defaultBlockState(),
|
||||
3
|
||||
);
|
||||
placedBlockPos = null;
|
||||
}
|
||||
|
||||
this.currentAction = PlaceAction.NONE;
|
||||
this.actionTimer = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) {
|
||||
currentAction = PlaceAction.NONE;
|
||||
return;
|
||||
}
|
||||
|
||||
actionTimer++;
|
||||
|
||||
switch (currentAction) {
|
||||
case PLACING_BOWL -> tickPlacingBowl(pet);
|
||||
case WAITING_EAT -> tickWaitingEat(pet);
|
||||
case PLACING_PET_BED -> tickPlacingPetBed(pet);
|
||||
case WAITING_SLEEP -> tickWaitingSleep(pet);
|
||||
case RETRIEVING -> tickRetrieving();
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void tickPlacingBowl(ServerPlayer pet) {
|
||||
// Move near pet
|
||||
double distSq = master.distanceToSqr(pet);
|
||||
if (distSq > PLACE_DISTANCE * PLACE_DISTANCE + 4) {
|
||||
master.getNavigation().moveTo(pet, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
master.getNavigation().stop();
|
||||
|
||||
// Find placement position and place bowl (facing toward the pet)
|
||||
BlockPos pos = findPlacementPos(pet);
|
||||
Direction facing = Direction.fromYRot(master.getYRot()).getOpposite();
|
||||
BlockState bowlState = V2Blocks.PET_BOWL.get()
|
||||
.defaultBlockState()
|
||||
.setValue(PetBowlBlock.FACING, facing);
|
||||
if (pos != null && placeBlock(pos, bowlState)) {
|
||||
placedBlockPos = pos;
|
||||
currentAction = PlaceAction.WAITING_EAT;
|
||||
actionTimer = 0;
|
||||
|
||||
// Fill the bowl with food (20 = full)
|
||||
if (master.level() instanceof ServerLevel serverLevel) {
|
||||
if (
|
||||
serverLevel.getBlockEntity(pos) instanceof
|
||||
PetBowlBlockEntity bowl
|
||||
) {
|
||||
bowl.fillBowl(20);
|
||||
}
|
||||
}
|
||||
|
||||
// Tell pet to eat
|
||||
DialogueBridge.talkTo(master, pet, "petplay.eat_command");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPlaceBlockGoal] {} placed food bowl at {}",
|
||||
master.getNpcName(),
|
||||
pos
|
||||
);
|
||||
} else {
|
||||
// Can't place, abort
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[MasterPlaceBlockGoal] {} couldn't find place for bowl",
|
||||
master.getNpcName()
|
||||
);
|
||||
currentAction = PlaceAction.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private void tickWaitingEat(ServerPlayer pet) {
|
||||
// Look at pet
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
// Check if pet has eaten (food level restored)
|
||||
if (pet.getFoodData().getFoodLevel() >= 16) {
|
||||
// Pet finished eating
|
||||
DialogueBridge.talkTo(master, pet, "petplay.good_pet");
|
||||
currentAction = PlaceAction.RETRIEVING;
|
||||
actionTimer = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout - pet didn't eat
|
||||
if (actionTimer > USE_WAIT_TIME) {
|
||||
DialogueBridge.talkTo(master, pet, "petplay.disappointed");
|
||||
currentAction = PlaceAction.RETRIEVING;
|
||||
actionTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void tickPlacingPetBed(ServerPlayer pet) {
|
||||
// Move near pet
|
||||
double distSq = master.distanceToSqr(pet);
|
||||
if (distSq > PLACE_DISTANCE * PLACE_DISTANCE + 4) {
|
||||
master.getNavigation().moveTo(pet, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
master.getNavigation().stop();
|
||||
|
||||
// Find placement position and place bed (facing toward the pet)
|
||||
BlockPos pos = findPlacementPos(pet);
|
||||
Direction facing = Direction.fromYRot(master.getYRot()).getOpposite();
|
||||
BlockState bedState = V2Blocks.PET_BED.get()
|
||||
.defaultBlockState()
|
||||
.setValue(PetBedBlock.FACING, facing);
|
||||
if (pos != null && placeBlock(pos, bedState)) {
|
||||
placedBlockPos = pos;
|
||||
currentAction = PlaceAction.WAITING_SLEEP;
|
||||
actionTimer = 0;
|
||||
|
||||
// Tell pet to rest
|
||||
DialogueBridge.talkTo(master, pet, "petplay.rest_command");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPlaceBlockGoal] {} placed pet bed at {}",
|
||||
master.getNpcName(),
|
||||
pos
|
||||
);
|
||||
} else {
|
||||
// Can't place, abort
|
||||
currentAction = PlaceAction.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private void tickWaitingSleep(ServerPlayer pet) {
|
||||
// Look at pet
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
// FIX: Check if pet is currently sleeping
|
||||
if (pet.isSleeping()) {
|
||||
// Pet is sleeping - reset timer to let them sleep as long as they want
|
||||
// Only start counting down after they wake up
|
||||
actionTimer = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Pet is not sleeping (either hasn't started or has woken up)
|
||||
// Timeout - retrieve pet bed after some time of NOT sleeping
|
||||
if (actionTimer > USE_WAIT_TIME) {
|
||||
DialogueBridge.talkTo(master, pet, "petplay.wake_up");
|
||||
currentAction = PlaceAction.RETRIEVING;
|
||||
actionTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void tickRetrieving() {
|
||||
if (actionTimer > RETRIEVE_DELAY) {
|
||||
// FIX: Safety check - don't remove block if pet is still sleeping
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet != null && pet.isSleeping()) {
|
||||
// Pet is still sleeping - wait for them to wake up
|
||||
actionTimer = 0; // Reset timer
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the placed block
|
||||
if (
|
||||
placedBlockPos != null &&
|
||||
master.level() instanceof ServerLevel serverLevel
|
||||
) {
|
||||
serverLevel.setBlock(
|
||||
placedBlockPos,
|
||||
Blocks.AIR.defaultBlockState(),
|
||||
3
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPlaceBlockGoal] {} retrieved block at {}",
|
||||
master.getNpcName(),
|
||||
placedBlockPos
|
||||
);
|
||||
|
||||
placedBlockPos = null;
|
||||
}
|
||||
|
||||
// Done with this action
|
||||
currentAction = PlaceAction.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a suitable position to place a block near the pet.
|
||||
*/
|
||||
private BlockPos findPlacementPos(ServerPlayer pet) {
|
||||
BlockPos petPos = pet.blockPosition();
|
||||
|
||||
// Search for valid placement nearby
|
||||
for (int dx = -2; dx <= 2; dx++) {
|
||||
for (int dz = -2; dz <= 2; dz++) {
|
||||
if (dx == 0 && dz == 0) continue; // Not on pet
|
||||
|
||||
BlockPos pos = petPos.offset(dx, 0, dz);
|
||||
if (isValidPlacement(pos)) {
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Try one block lower
|
||||
BlockPos lower = pos.below();
|
||||
if (isValidPlacement(lower)) {
|
||||
return lower;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if position is valid for block placement.
|
||||
*/
|
||||
private boolean isValidPlacement(BlockPos pos) {
|
||||
BlockState below = master.level().getBlockState(pos.below());
|
||||
BlockState at = master.level().getBlockState(pos);
|
||||
|
||||
return below.isSolidRender(master.level(), pos.below()) && at.isAir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a block at the given position.
|
||||
*/
|
||||
private boolean placeBlock(BlockPos pos, BlockState state) {
|
||||
if (!(master.level() instanceof ServerLevel serverLevel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
serverLevel.setBlock(pos, state, 3);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger feeding action (called from events/commands).
|
||||
*/
|
||||
public void triggerFeeding() {
|
||||
if (currentAction == PlaceAction.NONE && master.hasPet()) {
|
||||
currentAction = PlaceAction.PLACING_BOWL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger resting action (called from events/commands).
|
||||
*/
|
||||
public void triggerResting() {
|
||||
if (currentAction == PlaceAction.NONE && master.hasPet()) {
|
||||
currentAction = PlaceAction.PLACING_PET_BED;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FIX: NBT PERSISTENCE FOR PLACED BLOCKS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the currently placed block position (for NBT saving).
|
||||
*/
|
||||
public BlockPos getPlacedBlockPos() {
|
||||
return placedBlockPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the placed block position (for NBT loading).
|
||||
* Also cleans up the block if position is set on load.
|
||||
*/
|
||||
public void setPlacedBlockPos(BlockPos pos) {
|
||||
this.placedBlockPos = pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup orphaned blocks after entity load.
|
||||
* Called from EntityMaster after loading NBT if a position was saved.
|
||||
*/
|
||||
public void cleanupOrphanedBlock() {
|
||||
if (
|
||||
placedBlockPos != null &&
|
||||
master.level() instanceof ServerLevel serverLevel
|
||||
) {
|
||||
BlockState state = serverLevel.getBlockState(placedBlockPos);
|
||||
// Only remove if it's one of our V2 pet blocks
|
||||
if (
|
||||
state.is(V2Blocks.PET_BOWL.get()) ||
|
||||
state.is(V2Blocks.PET_BED.get())
|
||||
) {
|
||||
serverLevel.setBlock(
|
||||
placedBlockPos,
|
||||
Blocks.AIR.defaultBlockState(),
|
||||
3
|
||||
);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[MasterPlaceBlockGoal] {} cleaned up orphaned block at {}",
|
||||
master.getNpcName(),
|
||||
placedBlockPos
|
||||
);
|
||||
}
|
||||
placedBlockPos = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.items.ItemChokeCollar;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import com.tiedup.remake.items.base.BindVariant;
|
||||
import com.tiedup.remake.items.base.BlindfoldVariant;
|
||||
import com.tiedup.remake.items.base.GagVariant;
|
||||
import com.tiedup.remake.items.base.MittensVariant;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to punish their pet player.
|
||||
*
|
||||
* Triggered when:
|
||||
* - Struggle attempt detected while master was watching
|
||||
* - Contraband found during inspection
|
||||
* - Task failure
|
||||
* - Pet attacks the master (dual punishment: choke + physical restraint)
|
||||
*
|
||||
* Punishment types (selected from available options):
|
||||
* - Choke collar activation (requires choke collar)
|
||||
* - Temporary blindfold (requires empty blindfold slot)
|
||||
* - Temporary gag (requires empty gag slot)
|
||||
* - Temporary mittens (requires empty mittens slot)
|
||||
* - Tighten restraints / apply armbinder (requires pet unbound)
|
||||
* - Cold shoulder (always available)
|
||||
* - Leash tug (always available)
|
||||
*/
|
||||
public class MasterPunishGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Approach distance for punishment */
|
||||
private static final double PUNISH_DISTANCE = 2.0;
|
||||
|
||||
/** Duration of punishment sequence (ticks) */
|
||||
private static final int PUNISH_DURATION = 80;
|
||||
|
||||
/** Maximum choke duration before deactivating (ticks) - 3 seconds */
|
||||
private static final int MAX_CHOKE_TIME = 60;
|
||||
|
||||
/** Duration of temporary punishment items (2 minutes) */
|
||||
private static final int TEMP_ITEM_DURATION = 2400;
|
||||
|
||||
/** Fallback damage if no punishment method available */
|
||||
private static final float FALLBACK_DAMAGE = 2.0f;
|
||||
|
||||
/** Delay between primary choke and secondary restraint (ticks) - 0.75s */
|
||||
private static final int SECONDARY_PUNISHMENT_DELAY = 15;
|
||||
|
||||
/** Maximum punishment duration safety cap (ticks) - 8 seconds */
|
||||
private static final int MAX_PUNISH_DURATION = 160;
|
||||
|
||||
/** NBT tags for temporary punishment items */
|
||||
private static final String NBT_TEMP_MASTER_EVENT = "tempMasterEvent";
|
||||
private static final String NBT_EXPIRATION_TIME = "expirationTime";
|
||||
|
||||
private int punishTimer = 0;
|
||||
private boolean hasAppliedPunishment = false;
|
||||
private PunishmentType selectedPunishment = null;
|
||||
|
||||
/** Timer for tracking choke duration - 0 means not choking */
|
||||
private int chokeActiveTimer = 0;
|
||||
|
||||
/** Reference to the active choke collar for deactivation */
|
||||
private ItemStack activeChokeCollar = ItemStack.EMPTY;
|
||||
|
||||
/** Leash tug timer */
|
||||
private int leashTugTimer = 0;
|
||||
|
||||
// --- Attack punishment (dual: choke + physical restraint) ---
|
||||
|
||||
/** Whether this is an attack-triggered dual punishment */
|
||||
private boolean isAttackPunishment = false;
|
||||
|
||||
/** The physical restraint to apply after the choke */
|
||||
private PunishmentType secondaryPunishment = null;
|
||||
|
||||
/** Whether the secondary restraint has been applied */
|
||||
private boolean hasAppliedSecondary = false;
|
||||
|
||||
/** Tick count when the primary punishment was applied (for delay timing) */
|
||||
private int primaryAppliedAtTick = -1;
|
||||
|
||||
public MasterPunishGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.MOVE, Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
return (
|
||||
master.getStateManager().getCurrentState() == MasterState.PUNISH &&
|
||||
master.hasPet() &&
|
||||
master.getPetPlayer() != null
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (master.getStateManager().getCurrentState() != MasterState.PUNISH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extend duration if waiting for secondary punishment, with safety cap
|
||||
if (
|
||||
isAttackPunishment &&
|
||||
!hasAppliedSecondary &&
|
||||
punishTimer < MAX_PUNISH_DURATION
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return punishTimer < PUNISH_DURATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.punishTimer = 0;
|
||||
this.hasAppliedPunishment = false;
|
||||
this.chokeActiveTimer = 0;
|
||||
this.activeChokeCollar = ItemStack.EMPTY;
|
||||
this.leashTugTimer = 0;
|
||||
this.isAttackPunishment = false;
|
||||
this.secondaryPunishment = null;
|
||||
this.hasAppliedSecondary = false;
|
||||
this.primaryAppliedAtTick = -1;
|
||||
|
||||
// Check if this is an attack-triggered punishment (dual: choke + restraint)
|
||||
if (master.consumeAttackPunishment()) {
|
||||
this.isAttackPunishment = true;
|
||||
this.selectedPunishment = PunishmentType.CHOKE_COLLAR;
|
||||
this.secondaryPunishment = selectPhysicalRestraint();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPunishGoal] {} starting ATTACK punishment: {} + {}",
|
||||
master.getNpcName(),
|
||||
selectedPunishment,
|
||||
secondaryPunishment
|
||||
);
|
||||
} else {
|
||||
this.selectedPunishment = selectPunishment();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPunishGoal] {} starting punishment: {}",
|
||||
master.getNpcName(),
|
||||
selectedPunishment
|
||||
);
|
||||
}
|
||||
|
||||
master.markEngagement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
deactivateChoke();
|
||||
|
||||
this.punishTimer = 0;
|
||||
this.hasAppliedPunishment = false;
|
||||
this.chokeActiveTimer = 0;
|
||||
this.activeChokeCollar = ItemStack.EMPTY;
|
||||
this.leashTugTimer = 0;
|
||||
this.selectedPunishment = null;
|
||||
this.isAttackPunishment = false;
|
||||
this.secondaryPunishment = null;
|
||||
this.hasAppliedSecondary = false;
|
||||
this.primaryAppliedAtTick = -1;
|
||||
|
||||
if (master.hasPet()) {
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterPunishGoal] {} punishment complete",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
double distSq = master.distanceToSqr(pet);
|
||||
|
||||
// Move close for punishment
|
||||
if (distSq > PUNISH_DISTANCE * PUNISH_DISTANCE) {
|
||||
master.getNavigation().moveTo(pet, 1.2);
|
||||
} else {
|
||||
master.getNavigation().stop();
|
||||
|
||||
if (!hasAppliedPunishment) {
|
||||
applyPunishment(pet);
|
||||
hasAppliedPunishment = true;
|
||||
primaryAppliedAtTick = punishTimer;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply secondary punishment after delay (attack punishment only)
|
||||
if (
|
||||
isAttackPunishment &&
|
||||
!hasAppliedSecondary &&
|
||||
primaryAppliedAtTick >= 0 &&
|
||||
punishTimer >= primaryAppliedAtTick + SECONDARY_PUNISHMENT_DELAY
|
||||
) {
|
||||
applySecondaryPunishment(pet);
|
||||
hasAppliedSecondary = true;
|
||||
}
|
||||
|
||||
// Track choke duration
|
||||
if (chokeActiveTimer > 0) {
|
||||
chokeActiveTimer++;
|
||||
if (chokeActiveTimer >= MAX_CHOKE_TIME) {
|
||||
deactivateChoke();
|
||||
}
|
||||
}
|
||||
|
||||
// Track leash tug
|
||||
if (leashTugTimer > 0) {
|
||||
leashTugTimer--;
|
||||
if (leashTugTimer <= 0) {
|
||||
master.detachLeashFromPet();
|
||||
}
|
||||
}
|
||||
|
||||
punishTimer++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a punishment type from available options (normal punishments).
|
||||
*/
|
||||
private PunishmentType selectPunishment() {
|
||||
// Check for forced punishment (e.g., from other systems)
|
||||
PunishmentType forced = master.consumeForcedPunishment();
|
||||
if (forced != null) return forced;
|
||||
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return PunishmentType.COLD_SHOULDER;
|
||||
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
List<PunishmentType> available = new ArrayList<>();
|
||||
|
||||
// CHOKE: only if pet has choke collar
|
||||
if (bindState != null && bindState.hasCollar()) {
|
||||
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemChokeCollar) {
|
||||
available.add(PunishmentType.CHOKE_COLLAR);
|
||||
}
|
||||
}
|
||||
|
||||
// BLINDFOLD: only if eyes region is empty
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.EYES)) {
|
||||
available.add(PunishmentType.BLINDFOLD);
|
||||
}
|
||||
|
||||
// GAG: only if mouth region is empty
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.MOUTH)) {
|
||||
available.add(PunishmentType.GAG);
|
||||
}
|
||||
|
||||
// MITTENS: only if hands region is empty
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.HANDS)) {
|
||||
available.add(PunishmentType.MITTENS);
|
||||
}
|
||||
|
||||
// TIGHTEN: only if pet is not already bound
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.ARMS)) {
|
||||
available.add(PunishmentType.TIGHTEN_RESTRAINTS);
|
||||
}
|
||||
|
||||
// COLD_SHOULDER and LEASH_TUG: always available
|
||||
available.add(PunishmentType.COLD_SHOULDER);
|
||||
available.add(PunishmentType.LEASH_TUG);
|
||||
|
||||
return available.get(master.getRandom().nextInt(available.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a physical restraint for attack punishment secondary.
|
||||
* Excludes CHOKE_COLLAR (already primary) and COLD_SHOULDER (not physical).
|
||||
* Always includes LEASH_TUG as fallback when all equipment slots are full.
|
||||
*/
|
||||
private PunishmentType selectPhysicalRestraint() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return PunishmentType.LEASH_TUG;
|
||||
|
||||
List<PunishmentType> available = new ArrayList<>();
|
||||
|
||||
// BLINDFOLD: only if eyes region is empty
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.EYES)) {
|
||||
available.add(PunishmentType.BLINDFOLD);
|
||||
}
|
||||
|
||||
// GAG: only if mouth region is empty
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.MOUTH)) {
|
||||
available.add(PunishmentType.GAG);
|
||||
}
|
||||
|
||||
// MITTENS: only if hands region is empty
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.HANDS)) {
|
||||
available.add(PunishmentType.MITTENS);
|
||||
}
|
||||
|
||||
// TIGHTEN: only if pet is not already bound
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, BodyRegionV2.ARMS)) {
|
||||
available.add(PunishmentType.TIGHTEN_RESTRAINTS);
|
||||
}
|
||||
|
||||
// LEASH_TUG: always available as fallback
|
||||
available.add(PunishmentType.LEASH_TUG);
|
||||
|
||||
return available.get(master.getRandom().nextInt(available.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the selected punishment to the pet.
|
||||
*/
|
||||
private void applyPunishment(ServerPlayer pet) {
|
||||
if (selectedPunishment == null) {
|
||||
selectedPunishment = PunishmentType.COLD_SHOULDER;
|
||||
}
|
||||
|
||||
// Send dialogue
|
||||
DialogueBridge.talkTo(master, pet, selectedPunishment.getDialogueId());
|
||||
|
||||
switch (selectedPunishment) {
|
||||
case CHOKE_COLLAR -> applyChoke(pet);
|
||||
case BLINDFOLD -> applyTempAccessory(pet, BodyRegionV2.EYES);
|
||||
case GAG -> applyTempAccessory(pet, BodyRegionV2.MOUTH);
|
||||
case MITTENS -> applyTempAccessory(pet, BodyRegionV2.HANDS);
|
||||
case TIGHTEN_RESTRAINTS -> applyTighten(pet);
|
||||
case COLD_SHOULDER -> applyColdShoulder();
|
||||
case LEASH_TUG -> applyLeashTug(pet);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[MasterPunishGoal] {} applied {} to {}",
|
||||
master.getNpcName(),
|
||||
selectedPunishment,
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the secondary physical restraint after the choke (attack punishment only).
|
||||
*/
|
||||
private void applySecondaryPunishment(ServerPlayer pet) {
|
||||
if (secondaryPunishment == null) return;
|
||||
|
||||
// Send dialogue for the secondary punishment
|
||||
DialogueBridge.talkTo(master, pet, secondaryPunishment.getDialogueId());
|
||||
|
||||
switch (secondaryPunishment) {
|
||||
case BLINDFOLD -> applyTempAccessory(pet, BodyRegionV2.EYES);
|
||||
case GAG -> applyTempAccessory(pet, BodyRegionV2.MOUTH);
|
||||
case MITTENS -> applyTempAccessory(pet, BodyRegionV2.HANDS);
|
||||
case TIGHTEN_RESTRAINTS -> applyTighten(pet);
|
||||
case LEASH_TUG -> applyLeashTug(pet);
|
||||
default -> {
|
||||
} // CHOKE_COLLAR and COLD_SHOULDER excluded from secondary
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[MasterPunishGoal] {} applied secondary {} to {}",
|
||||
master.getNpcName(),
|
||||
secondaryPunishment,
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply choke collar punishment (existing logic).
|
||||
*/
|
||||
private void applyChoke(ServerPlayer pet) {
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState != null && bindState.hasCollar()) {
|
||||
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
|
||||
chokeCollar.setChoking(collar, true);
|
||||
this.activeChokeCollar = collar;
|
||||
this.chokeActiveTimer = 1;
|
||||
|
||||
pet
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
pet.getX(),
|
||||
pet.getY(),
|
||||
pet.getZ(),
|
||||
SoundEvents.PLAYER_HURT,
|
||||
SoundSource.HOSTILE,
|
||||
0.8f,
|
||||
0.5f + master.getRandom().nextFloat() * 0.2f
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Fallback
|
||||
pet.hurt(pet.damageSources().magic(), FALLBACK_DAMAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a temporary accessory (blindfold, gag, or mittens) as punishment.
|
||||
* Uses the same temp-item pattern as MasterRandomEventGoal.
|
||||
*/
|
||||
private void applyTempAccessory(ServerPlayer pet, BodyRegionV2 region) {
|
||||
ItemStack accessory = createAccessory(region);
|
||||
if (accessory.isEmpty()) return;
|
||||
|
||||
// Mark as temporary with expiration
|
||||
long expirationTime = master.level().getGameTime() + TEMP_ITEM_DURATION;
|
||||
CompoundTag tag = accessory.getOrCreateTag();
|
||||
tag.putBoolean(NBT_TEMP_MASTER_EVENT, true);
|
||||
tag.putLong(NBT_EXPIRATION_TIME, expirationTime);
|
||||
tag.putUUID("masterUUID", master.getUUID());
|
||||
|
||||
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(pet);
|
||||
if (equip != null) {
|
||||
equip.setInRegion(region, accessory);
|
||||
V2EquipmentHelper.sync(pet);
|
||||
}
|
||||
|
||||
pet
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
pet.getX(),
|
||||
pet.getY(),
|
||||
pet.getZ(),
|
||||
SoundEvents.ARMOR_EQUIP_LEATHER,
|
||||
SoundSource.HOSTILE,
|
||||
1.0f,
|
||||
0.8f
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an accessory item for the given body region.
|
||||
*/
|
||||
private ItemStack createAccessory(BodyRegionV2 region) {
|
||||
return switch (region) {
|
||||
case EYES -> new ItemStack(
|
||||
ModItems.getBlindfold(BlindfoldVariant.CLASSIC)
|
||||
);
|
||||
case MOUTH -> new ItemStack(ModItems.getGag(GagVariant.BALL_GAG));
|
||||
case HANDS -> new ItemStack(
|
||||
ModItems.getMittens(MittensVariant.LEATHER)
|
||||
);
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply armbinder as punishment.
|
||||
*/
|
||||
private void applyTighten(ServerPlayer pet) {
|
||||
ItemStack armbinder = new ItemStack(
|
||||
ModItems.getBind(BindVariant.ARMBINDER)
|
||||
);
|
||||
|
||||
// Mark as temporary
|
||||
long expirationTime = master.level().getGameTime() + TEMP_ITEM_DURATION;
|
||||
CompoundTag tag = armbinder.getOrCreateTag();
|
||||
tag.putBoolean(NBT_TEMP_MASTER_EVENT, true);
|
||||
tag.putLong(NBT_EXPIRATION_TIME, expirationTime);
|
||||
tag.putUUID("masterUUID", master.getUUID());
|
||||
|
||||
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(pet);
|
||||
if (equip != null) {
|
||||
equip.setInRegion(BodyRegionV2.ARMS, armbinder);
|
||||
V2EquipmentHelper.sync(pet);
|
||||
}
|
||||
|
||||
pet
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
pet.getX(),
|
||||
pet.getY(),
|
||||
pet.getZ(),
|
||||
SoundEvents.ARMOR_EQUIP_CHAIN,
|
||||
SoundSource.HOSTILE,
|
||||
1.0f,
|
||||
0.7f
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply cold shoulder - master ignores pet interactions.
|
||||
*/
|
||||
private void applyColdShoulder() {
|
||||
master.startColdShoulder(
|
||||
PunishmentType.COLD_SHOULDER.getDurationTicks()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply leash tug - yank pet toward master.
|
||||
*/
|
||||
private void applyLeashTug(ServerPlayer pet) {
|
||||
// Attach temp leash
|
||||
master.attachLeashToPet();
|
||||
this.leashTugTimer = PunishmentType.LEASH_TUG.getDurationTicks();
|
||||
|
||||
// Apply velocity toward master
|
||||
Vec3 direction = master.position().subtract(pet.position()).normalize();
|
||||
pet.setDeltaMovement(direction.scale(1.2));
|
||||
pet.hurtMarked = true;
|
||||
|
||||
pet
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
pet.getX(),
|
||||
pet.getY(),
|
||||
pet.getZ(),
|
||||
SoundEvents.CHAIN_HIT,
|
||||
SoundSource.HOSTILE,
|
||||
1.0f,
|
||||
1.0f
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the choke collar if active.
|
||||
*/
|
||||
private void deactivateChoke() {
|
||||
if (
|
||||
!activeChokeCollar.isEmpty() &&
|
||||
activeChokeCollar.getItem() instanceof ItemChokeCollar chokeCollar
|
||||
) {
|
||||
chokeCollar.setChoking(activeChokeCollar, false);
|
||||
}
|
||||
this.activeChokeCollar = ItemStack.EMPTY;
|
||||
this.chokeActiveTimer = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.items.ModItems;
|
||||
import com.tiedup.remake.items.base.BindVariant;
|
||||
import com.tiedup.remake.items.base.BlindfoldVariant;
|
||||
import com.tiedup.remake.items.base.GagVariant;
|
||||
import com.tiedup.remake.items.base.MittensVariant;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to trigger random events:
|
||||
* - Random bind/accessory placement (temporary)
|
||||
* - Master-initiated dogwalk
|
||||
*
|
||||
* Events are less frequent than tasks and add variety to gameplay.
|
||||
*/
|
||||
public class MasterRandomEventGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Chance to trigger an event per tick */
|
||||
private static final float EVENT_CHANCE = 0.002f; // ~4% per 10 seconds
|
||||
|
||||
/** Minimum time between events (ticks) - 2 minutes */
|
||||
private static final int EVENT_COOLDOWN = 2400;
|
||||
|
||||
/** Duration of random bind event (ticks) - 2 minutes */
|
||||
private static final int RANDOM_BIND_DURATION = 2400;
|
||||
|
||||
/** NBT tag key for temporary master event items */
|
||||
private static final String NBT_TEMP_MASTER_EVENT = "tempMasterEvent";
|
||||
private static final String NBT_EXPIRATION_TIME = "expirationTime";
|
||||
|
||||
/** Event types that can be triggered randomly */
|
||||
private enum RandomEvent {
|
||||
RANDOM_BIND,
|
||||
DOGWALK,
|
||||
HUMAN_CHAIR,
|
||||
}
|
||||
|
||||
/** Accessory regions that can be randomly equipped (eyes, mouth, hands) */
|
||||
private static final List<BodyRegionV2> RANDOM_ACCESSORY_REGIONS = List.of(
|
||||
BodyRegionV2.EYES,
|
||||
BodyRegionV2.MOUTH,
|
||||
BodyRegionV2.HANDS
|
||||
);
|
||||
|
||||
private long lastEventTime = 0;
|
||||
private RandomEvent currentEvent = null;
|
||||
private int eventTimer = 0;
|
||||
|
||||
/** For RANDOM_BIND: the body region that was equipped */
|
||||
private BodyRegionV2 appliedAccessoryRegion = null;
|
||||
private ItemStack appliedAccessory = ItemStack.EMPTY;
|
||||
|
||||
public MasterRandomEventGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
if (!master.hasPet()) return false;
|
||||
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
|
||||
// Don't trigger events during other activities
|
||||
if (state != MasterState.FOLLOWING && state != MasterState.OBSERVING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't trigger if there's an active task
|
||||
if (master.hasActiveTask()) return false;
|
||||
|
||||
// Check cooldown
|
||||
long currentTime = master.level().getGameTime();
|
||||
if (currentTime - lastEventTime < EVENT_COOLDOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Random chance (modulated by engagement cadence)
|
||||
float multiplier = master.getEngagementMultiplier();
|
||||
return (
|
||||
multiplier > 0 &&
|
||||
master.getRandom().nextFloat() < EVENT_CHANCE * multiplier
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
// Only continue for timed events like RANDOM_BIND
|
||||
return (
|
||||
currentEvent == RandomEvent.RANDOM_BIND &&
|
||||
eventTimer < RANDOM_BIND_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.eventTimer = 0;
|
||||
this.appliedAccessoryRegion = null;
|
||||
this.appliedAccessory = ItemStack.EMPTY;
|
||||
|
||||
// Select random event
|
||||
RandomEvent[] events = RandomEvent.values();
|
||||
this.currentEvent = events[master.getRandom().nextInt(events.length)];
|
||||
|
||||
master.markEngagement();
|
||||
triggerEvent();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterRandomEventGoal] {} triggered event: {}",
|
||||
master.getNpcName(),
|
||||
currentEvent
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
lastEventTime = master.level().getGameTime();
|
||||
|
||||
// Only remove accessory on natural completion (timer expired),
|
||||
// NOT on goal preemption (e.g. MasterObservePlayerGoal taking over LOOK flag).
|
||||
// If preempted, the NBT expiration timer in cleanupExpiredTempItems() handles removal.
|
||||
if (
|
||||
currentEvent == RandomEvent.RANDOM_BIND &&
|
||||
appliedAccessoryRegion != null &&
|
||||
eventTimer >= RANDOM_BIND_DURATION
|
||||
) {
|
||||
removeAppliedAccessory();
|
||||
}
|
||||
|
||||
this.currentEvent = null;
|
||||
this.eventTimer = 0;
|
||||
this.appliedAccessoryRegion = null;
|
||||
this.appliedAccessory = ItemStack.EMPTY;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterRandomEventGoal] {} event completed",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (currentEvent == RandomEvent.RANDOM_BIND) {
|
||||
eventTimer++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger the selected event.
|
||||
*/
|
||||
private void triggerEvent() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
switch (currentEvent) {
|
||||
case RANDOM_BIND -> triggerRandomBind(pet);
|
||||
case DOGWALK -> triggerDogwalk(pet);
|
||||
case HUMAN_CHAIR -> triggerHumanChair(pet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger random bind event - apply a temporary accessory.
|
||||
*/
|
||||
private void triggerRandomBind(ServerPlayer pet) {
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState == null) return;
|
||||
|
||||
// Collect all empty accessory regions, then pick one at random
|
||||
List<BodyRegionV2> emptyRegions = new java.util.ArrayList<>();
|
||||
for (BodyRegionV2 region : RANDOM_ACCESSORY_REGIONS) {
|
||||
if (!V2EquipmentHelper.isRegionOccupied(pet, region)) {
|
||||
emptyRegions.add(region);
|
||||
}
|
||||
}
|
||||
|
||||
BodyRegionV2 selectedRegion = null;
|
||||
if (!emptyRegions.isEmpty()) {
|
||||
selectedRegion = emptyRegions.get(
|
||||
master.getRandom().nextInt(emptyRegions.size())
|
||||
);
|
||||
} else if (master.getRandom().nextFloat() < 0.3f) {
|
||||
// If no empty region, pick random and replace (less likely)
|
||||
selectedRegion = RANDOM_ACCESSORY_REGIONS.get(
|
||||
master.getRandom().nextInt(RANDOM_ACCESSORY_REGIONS.size())
|
||||
);
|
||||
}
|
||||
|
||||
if (selectedRegion == null) {
|
||||
// Pet is fully accessorized, skip event
|
||||
currentEvent = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the accessory item
|
||||
ItemStack accessory = createRandomAccessory(selectedRegion);
|
||||
if (accessory.isEmpty()) {
|
||||
currentEvent = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark the accessory as temporary with expiration time
|
||||
// This ensures cleanup even after server restart
|
||||
long expirationTime =
|
||||
master.level().getGameTime() + RANDOM_BIND_DURATION;
|
||||
CompoundTag tag = accessory.getOrCreateTag();
|
||||
tag.putBoolean(NBT_TEMP_MASTER_EVENT, true);
|
||||
tag.putLong(NBT_EXPIRATION_TIME, expirationTime);
|
||||
tag.putUUID("masterUUID", master.getUUID());
|
||||
|
||||
// Apply the accessory
|
||||
this.appliedAccessoryRegion = selectedRegion;
|
||||
this.appliedAccessory = accessory;
|
||||
|
||||
IV2BondageEquipment equip = V2EquipmentHelper.getEquipment(pet);
|
||||
if (equip != null) {
|
||||
equip.setInRegion(selectedRegion, accessory);
|
||||
V2EquipmentHelper.sync(pet);
|
||||
}
|
||||
|
||||
// Dialogue - both messages sent (random_bind is the action, random_bind_done is the completion)
|
||||
DialogueBridge.talkTo(master, pet, "petplay.random_bind");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterRandomEventGoal] {} applied {} to {}",
|
||||
master.getNpcName(),
|
||||
selectedRegion,
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random accessory item for the given body region.
|
||||
*/
|
||||
private ItemStack createRandomAccessory(BodyRegionV2 region) {
|
||||
Item item = switch (region) {
|
||||
case EYES -> ModItems.getBlindfold(BlindfoldVariant.CLASSIC);
|
||||
case MOUTH -> ModItems.getGag(GagVariant.BALL_GAG);
|
||||
case HANDS -> ModItems.getMittens(MittensVariant.LEATHER);
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (item == null) return ItemStack.EMPTY;
|
||||
return new ItemStack(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the applied accessory when event ends.
|
||||
*/
|
||||
private void removeAppliedAccessory() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
if (appliedAccessoryRegion != null) {
|
||||
// Check if it's still the same item we applied
|
||||
ItemStack current = V2EquipmentHelper.getInRegion(
|
||||
pet,
|
||||
appliedAccessoryRegion
|
||||
);
|
||||
if (current.getItem() == appliedAccessory.getItem()) {
|
||||
V2EquipmentHelper.unequipFromRegion(
|
||||
pet,
|
||||
appliedAccessoryRegion,
|
||||
true
|
||||
);
|
||||
|
||||
// Dialogue
|
||||
DialogueBridge.talkTo(
|
||||
master,
|
||||
pet,
|
||||
"petplay.random_bind_remove"
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterRandomEventGoal] {} removed {} from {}",
|
||||
master.getNpcName(),
|
||||
appliedAccessoryRegion,
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger dogwalk event - Master initiates a walk.
|
||||
*/
|
||||
private void triggerDogwalk(ServerPlayer pet) {
|
||||
// Put pet in dogbind if not already tied
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState != null && !bindState.isTiedUp()) {
|
||||
ItemStack dogbind = new ItemStack(
|
||||
ModItems.getBind(BindVariant.DOGBINDER)
|
||||
);
|
||||
bindState.equip(BodyRegionV2.ARMS, dogbind);
|
||||
}
|
||||
|
||||
// Attach leash
|
||||
master.attachLeashToPet();
|
||||
|
||||
// Set dogwalk mode - master leads (pulls the pet)
|
||||
master.setDogwalkMode(true);
|
||||
master.setMasterState(MasterState.DOGWALK);
|
||||
|
||||
// Dialogue
|
||||
DialogueBridge.talkTo(master, pet, "petplay.start_dogwalk");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterRandomEventGoal] {} started dogwalk with {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
|
||||
// Event is instant - dogwalk continues until manually ended or timeout
|
||||
this.currentEvent = null; // Don't continue in this goal
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger human chair event - Master uses pet as furniture.
|
||||
* Delegates to MasterHumanChairGoal for the actual behavior.
|
||||
*/
|
||||
private void triggerHumanChair(ServerPlayer pet) {
|
||||
// Don't trigger if pet is already tied up (dogbind would conflict)
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState != null && bindState.isTiedUp()) {
|
||||
// Fall back to random bind instead
|
||||
currentEvent = RandomEvent.RANDOM_BIND;
|
||||
triggerRandomBind(pet);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set state - MasterHumanChairGoal will handle the rest
|
||||
master.setMasterState(MasterState.HUMAN_CHAIR);
|
||||
|
||||
// Dialogue
|
||||
DialogueBridge.talkTo(master, pet, "petplay.human_chair_start");
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterRandomEventGoal] {} started human chair with {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
|
||||
// Event is instant - human chair continues in dedicated goal
|
||||
this.currentEvent = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STATIC CLEANUP METHODS
|
||||
// ========================================
|
||||
|
||||
/** All body regions that can have temporary master event items */
|
||||
private static final List<BodyRegionV2> TEMP_ITEM_CLEANUP_REGIONS =
|
||||
List.of(
|
||||
BodyRegionV2.ARMS,
|
||||
BodyRegionV2.EYES,
|
||||
BodyRegionV2.MOUTH,
|
||||
BodyRegionV2.HANDS
|
||||
);
|
||||
|
||||
/**
|
||||
* Check and cleanup any expired temporary master event items on a player.
|
||||
* Should be called periodically from EntityMaster.tick() or on player login.
|
||||
*
|
||||
* @param pet The player to check
|
||||
* @param currentTime Current game time
|
||||
*/
|
||||
public static void cleanupExpiredTempItems(
|
||||
ServerPlayer pet,
|
||||
long currentTime
|
||||
) {
|
||||
for (BodyRegionV2 region : TEMP_ITEM_CLEANUP_REGIONS) {
|
||||
ItemStack item = V2EquipmentHelper.getInRegion(pet, region);
|
||||
if (item.isEmpty()) continue;
|
||||
|
||||
CompoundTag tag = item.getTag();
|
||||
if (tag == null) continue;
|
||||
|
||||
if (tag.getBoolean(NBT_TEMP_MASTER_EVENT)) {
|
||||
long expirationTime = tag.getLong(NBT_EXPIRATION_TIME);
|
||||
if (currentTime >= expirationTime) {
|
||||
// Item expired - use proper removal to trigger onUnequipped callbacks
|
||||
// ARMS (bind) needs PlayerBindState.unequip(BodyRegionV2.ARMS) for speed/animation cleanup
|
||||
if (region == BodyRegionV2.ARMS) {
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(
|
||||
pet
|
||||
);
|
||||
if (bindState != null && bindState.isTiedUp()) {
|
||||
bindState.unequip(BodyRegionV2.ARMS);
|
||||
}
|
||||
} else {
|
||||
V2EquipmentHelper.unequipFromRegion(pet, region, true);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[MasterRandomEventGoal] Removed expired temp {} from {}",
|
||||
region,
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an item is a temporary master event item.
|
||||
*/
|
||||
public static boolean isTempMasterEventItem(ItemStack stack) {
|
||||
if (stack.isEmpty()) return false;
|
||||
CompoundTag tag = stack.getTag();
|
||||
return tag != null && tag.getBoolean(NBT_TEMP_MASTER_EVENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
/**
|
||||
* Enum defining the behavioral states of a Master entity.
|
||||
*
|
||||
* Master NPC follows a different pattern than Kidnapper:
|
||||
* - FOLLOWING: Master follows their pet (player)
|
||||
* - OBSERVING: Master is watching the pet (can detect struggle)
|
||||
* - DISTRACTED: Master is distracted (pet can struggle safely)
|
||||
* - TASK_ASSIGN: Master is assigning a task to pet
|
||||
* - TASK_WATCH: Master is watching pet perform a task
|
||||
* - INSPECT: Master is inspecting pet's inventory for contraband
|
||||
* - PUNISH: Master is punishing pet for misbehavior
|
||||
* - PURCHASING: Master is buying a pet from a Kidnapper
|
||||
*/
|
||||
public enum MasterState {
|
||||
/**
|
||||
* Idle state - Master has nothing specific to do.
|
||||
* Default state when no other action is available.
|
||||
*/
|
||||
IDLE,
|
||||
|
||||
/**
|
||||
* Purchasing state - Master is buying a captive from a Kidnapper.
|
||||
* This is the initial state when Master spawns.
|
||||
*/
|
||||
PURCHASING,
|
||||
|
||||
/**
|
||||
* Following state - Master follows the pet player.
|
||||
* Unlike normal NPCs, the Master follows the player, not vice versa.
|
||||
* Maintains 2-8 block distance.
|
||||
*/
|
||||
FOLLOWING,
|
||||
|
||||
/**
|
||||
* Observing state - Master is actively watching the pet.
|
||||
* Can detect struggle attempts in this state.
|
||||
* Pet cannot escape while being observed.
|
||||
*/
|
||||
OBSERVING,
|
||||
|
||||
/**
|
||||
* Distracted state - Master is temporarily distracted.
|
||||
* Pet can safely struggle in this state.
|
||||
* Lasts 30-60 seconds, occurs every 2-5 minutes.
|
||||
*/
|
||||
DISTRACTED,
|
||||
|
||||
/**
|
||||
* Task assign state - Master is assigning a task to the pet.
|
||||
* Transitions to TASK_WATCH after assignment.
|
||||
*/
|
||||
TASK_ASSIGN,
|
||||
|
||||
/**
|
||||
* Task watch state - Master is supervising pet's task execution.
|
||||
* Similar to OBSERVING but focused on task completion.
|
||||
*/
|
||||
TASK_WATCH,
|
||||
|
||||
/**
|
||||
* Inspect state - Master is checking pet's inventory for contraband.
|
||||
* Will confiscate lockpicks, knives, weapons, etc.
|
||||
*/
|
||||
INSPECT,
|
||||
|
||||
/**
|
||||
* Punish state - Master is punishing the pet.
|
||||
* Uses shock collar, paddle, or other punishment methods.
|
||||
* Triggered by: struggle detected, task failed, contraband found.
|
||||
*/
|
||||
PUNISH,
|
||||
|
||||
/**
|
||||
* Dogwalk state - Master is taking pet on a walk (event).
|
||||
* Similar to FOLLOWING but with specific destination.
|
||||
*/
|
||||
DOGWALK,
|
||||
|
||||
/**
|
||||
* Human Chair state - Master uses pet as furniture.
|
||||
* Pet is forced on all fours, cannot move. Master sits on pet.
|
||||
* Lasts ~2 minutes with idle dialogue.
|
||||
*/
|
||||
HUMAN_CHAIR;
|
||||
|
||||
/**
|
||||
* Check if this state allows the master to detect struggles.
|
||||
* OBSERVING, TASK_WATCH, and FOLLOWING can detect struggles.
|
||||
*/
|
||||
public boolean canDetectStruggle() {
|
||||
return this == OBSERVING || this == TASK_WATCH || this == FOLLOWING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this state is a "watching" state where Master pays attention.
|
||||
*/
|
||||
public boolean isWatching() {
|
||||
return this == OBSERVING || this == TASK_WATCH || this == INSPECT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this state allows the pet to struggle without detection.
|
||||
*/
|
||||
public boolean allowsSafeStruggle() {
|
||||
return this == DISTRACTED || this == IDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Master can transition to punishment from this state.
|
||||
*/
|
||||
public boolean canTransitionToPunish() {
|
||||
return this != PUNISH && this != PURCHASING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Master is actively engaged with pet (not idle).
|
||||
*/
|
||||
public boolean isEngaged() {
|
||||
return this != IDLE && this != DISTRACTED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.util.MessageDispatcher;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.enchantment.EnchantmentHelper;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to assign tasks to their pet player.
|
||||
*
|
||||
* Tasks include:
|
||||
* - HEEL: Stay close to master (< 3 blocks)
|
||||
* - WAIT_HERE: Stay at current location (< 1 block movement)
|
||||
* - FETCH_ITEM: Bring a specific item
|
||||
*
|
||||
* After assignment, transitions to TASK_WATCH where MasterTaskWatchGoal monitors compliance.
|
||||
*/
|
||||
public class MasterTaskAssignGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Chance to assign task when following (per tick) */
|
||||
private static final float TASK_CHANCE = 0.003f; // ~6% per 10 seconds
|
||||
|
||||
/** Minimum time between tasks (ticks) - 1 minute */
|
||||
private static final int TASK_COOLDOWN = 1200;
|
||||
|
||||
/** Task assignment duration (ticks) */
|
||||
private static final int ASSIGN_DURATION = 40;
|
||||
|
||||
/** Items that can be requested for FETCH_ITEM task */
|
||||
private static final List<Item> FETCHABLE_ITEMS = List.of(
|
||||
Items.APPLE,
|
||||
Items.BREAD,
|
||||
Items.COOKED_BEEF,
|
||||
Items.COOKED_CHICKEN,
|
||||
Items.GOLDEN_APPLE,
|
||||
Items.COOKIE,
|
||||
Items.DIAMOND,
|
||||
Items.EMERALD,
|
||||
Items.IRON_INGOT,
|
||||
Items.GOLD_INGOT,
|
||||
Items.BONE,
|
||||
Items.FLOWER_BANNER_PATTERN,
|
||||
Items.ROSE_BUSH,
|
||||
Items.DANDELION,
|
||||
Items.POPPY
|
||||
);
|
||||
|
||||
/** Tasks that can be randomly assigned (excludes events like RANDOM_BIND, DOGWALK) */
|
||||
private static final PetTask[] ASSIGNABLE_TASKS = {
|
||||
PetTask.HEEL,
|
||||
PetTask.WAIT_HERE,
|
||||
PetTask.FETCH_ITEM,
|
||||
PetTask.KNEEL,
|
||||
PetTask.COME,
|
||||
PetTask.PRESENT,
|
||||
PetTask.SPEAK,
|
||||
PetTask.DROP,
|
||||
PetTask.FOLLOW_CLOSE,
|
||||
PetTask.DEMAND,
|
||||
};
|
||||
|
||||
/** Items the Master considers valuable enough to demand (high-value items) */
|
||||
private static final Set<Item> HIGH_VALUE_ITEMS = Set.of(
|
||||
Items.DIAMOND,
|
||||
Items.EMERALD,
|
||||
Items.GOLD_INGOT,
|
||||
Items.GOLDEN_APPLE,
|
||||
Items.ENCHANTED_GOLDEN_APPLE,
|
||||
Items.NETHERITE_INGOT,
|
||||
Items.NETHER_STAR,
|
||||
Items.TOTEM_OF_UNDYING,
|
||||
Items.DIAMOND_SWORD,
|
||||
Items.DIAMOND_PICKAXE,
|
||||
Items.DIAMOND_AXE,
|
||||
Items.DIAMOND_CHESTPLATE,
|
||||
Items.DIAMOND_HELMET,
|
||||
Items.DIAMOND_LEGGINGS,
|
||||
Items.DIAMOND_BOOTS,
|
||||
Items.NETHERITE_SWORD,
|
||||
Items.NETHERITE_PICKAXE,
|
||||
Items.NETHERITE_AXE,
|
||||
Items.NETHERITE_CHESTPLATE,
|
||||
Items.NETHERITE_HELMET,
|
||||
Items.NETHERITE_LEGGINGS,
|
||||
Items.NETHERITE_BOOTS,
|
||||
Items.TRIDENT,
|
||||
Items.ELYTRA,
|
||||
Items.ENDER_PEARL,
|
||||
Items.BLAZE_ROD,
|
||||
Items.GHAST_TEAR
|
||||
);
|
||||
|
||||
/** Items of moderate value - fallback if no high-value items found */
|
||||
private static final Set<Item> MEDIUM_VALUE_ITEMS = Set.of(
|
||||
Items.IRON_INGOT,
|
||||
Items.GOLD_INGOT,
|
||||
Items.LAPIS_LAZULI,
|
||||
Items.REDSTONE,
|
||||
Items.IRON_SWORD,
|
||||
Items.IRON_PICKAXE,
|
||||
Items.IRON_AXE,
|
||||
Items.IRON_CHESTPLATE,
|
||||
Items.COOKED_BEEF,
|
||||
Items.COOKED_PORKCHOP,
|
||||
Items.GOLDEN_CARROT,
|
||||
Items.EXPERIENCE_BOTTLE,
|
||||
Items.BOOK,
|
||||
Items.NAME_TAG,
|
||||
Items.SADDLE,
|
||||
Items.COMPASS,
|
||||
Items.CLOCK,
|
||||
Items.SPYGLASS
|
||||
);
|
||||
|
||||
private int assignTimer = 0;
|
||||
private long lastTaskTime = 0;
|
||||
private boolean hasAssigned = false;
|
||||
private PetTask selectedTask = null;
|
||||
|
||||
public MasterTaskAssignGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
if (!master.hasPet()) return false;
|
||||
|
||||
MasterState state = master.getStateManager().getCurrentState();
|
||||
|
||||
// Already assigning
|
||||
if (state == MasterState.TASK_ASSIGN) return true;
|
||||
|
||||
// Don't assign if already has active task
|
||||
if (master.hasActiveTask()) return false;
|
||||
|
||||
// Can only assign from following/observing
|
||||
if (state != MasterState.FOLLOWING && state != MasterState.OBSERVING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
long currentTime = master.level().getGameTime();
|
||||
if (currentTime - lastTaskTime < TASK_COOLDOWN) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Random chance (modulated by engagement cadence)
|
||||
float multiplier = master.getEngagementMultiplier();
|
||||
return (
|
||||
multiplier > 0 &&
|
||||
master.getRandom().nextFloat() < TASK_CHANCE * multiplier
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
return (
|
||||
master.hasPet() &&
|
||||
master.getStateManager().getCurrentState() ==
|
||||
MasterState.TASK_ASSIGN &&
|
||||
assignTimer < ASSIGN_DURATION
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.assignTimer = 0;
|
||||
this.hasAssigned = false;
|
||||
this.selectedTask = selectRandomTask();
|
||||
master.setMasterState(MasterState.TASK_ASSIGN);
|
||||
master.markEngagement();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskAssignGoal] {} assigning task: {}",
|
||||
master.getNpcName(),
|
||||
selectedTask
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
lastTaskTime = master.level().getGameTime();
|
||||
this.assignTimer = 0;
|
||||
this.hasAssigned = false;
|
||||
|
||||
// Transition to task watch if task was assigned
|
||||
if (master.hasPet() && master.hasActiveTask()) {
|
||||
master.setMasterState(MasterState.TASK_WATCH);
|
||||
} else if (master.hasPet()) {
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
|
||||
this.selectedTask = null;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskAssignGoal] {} task assignment complete",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
// Look at pet
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
// Assign task at start
|
||||
if (!hasAssigned && selectedTask != null) {
|
||||
assignTask(pet, selectedTask);
|
||||
hasAssigned = true;
|
||||
}
|
||||
|
||||
assignTimer++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a random task to assign.
|
||||
*/
|
||||
private PetTask selectRandomTask() {
|
||||
return ASSIGNABLE_TASKS[master
|
||||
.getRandom()
|
||||
.nextInt(ASSIGNABLE_TASKS.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a task to the pet.
|
||||
*/
|
||||
private void assignTask(ServerPlayer pet, PetTask task) {
|
||||
String message;
|
||||
Item requestedItem = null;
|
||||
|
||||
// Handle task-specific setup
|
||||
switch (task) {
|
||||
case HEEL, FOLLOW_CLOSE -> {
|
||||
message = task.getMessageTemplate();
|
||||
// Distance tracked to master, no position needed
|
||||
}
|
||||
case WAIT_HERE -> {
|
||||
message = task.getMessageTemplate();
|
||||
// Store pet's current position as the anchor point
|
||||
master.setTaskStartPosition(pet.position());
|
||||
}
|
||||
case KNEEL -> {
|
||||
message = task.getMessageTemplate();
|
||||
// Store pet's current position - must not move
|
||||
master.setTaskStartPosition(pet.position());
|
||||
}
|
||||
case PRESENT -> {
|
||||
message = task.getMessageTemplate();
|
||||
// Distance tracked to master
|
||||
}
|
||||
case COME -> {
|
||||
message = task.getMessageTemplate();
|
||||
// Pet must reach master quickly - no special setup
|
||||
}
|
||||
case SPEAK -> {
|
||||
message = task.getMessageTemplate();
|
||||
// Pet must right-click on master - no special setup
|
||||
}
|
||||
case DROP -> {
|
||||
message = task.getMessageTemplate();
|
||||
// Pet must empty hands - no special setup
|
||||
}
|
||||
case FETCH_ITEM -> {
|
||||
// Select random item to fetch
|
||||
requestedItem = FETCHABLE_ITEMS.get(
|
||||
master.getRandom().nextInt(FETCHABLE_ITEMS.size())
|
||||
);
|
||||
String itemName = requestedItem.getDescription().getString();
|
||||
message = String.format(task.getMessageTemplate(), itemName);
|
||||
master.setRequestedItem(requestedItem);
|
||||
}
|
||||
case DEMAND -> {
|
||||
// Scan pet's inventory for the most valuable item
|
||||
requestedItem = findMostValuableItem(pet);
|
||||
if (requestedItem == null) {
|
||||
// Pet has nothing worth taking - fallback to FETCH_ITEM
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskAssignGoal] {} found nothing to demand, falling back to FETCH",
|
||||
master.getNpcName()
|
||||
);
|
||||
task = PetTask.FETCH_ITEM;
|
||||
requestedItem = FETCHABLE_ITEMS.get(
|
||||
master.getRandom().nextInt(FETCHABLE_ITEMS.size())
|
||||
);
|
||||
String fallbackName = requestedItem
|
||||
.getDescription()
|
||||
.getString();
|
||||
message = String.format(
|
||||
PetTask.FETCH_ITEM.getMessageTemplate(),
|
||||
fallbackName
|
||||
);
|
||||
master.setRequestedItem(requestedItem);
|
||||
master.setActiveTask(task);
|
||||
} else {
|
||||
String itemName = requestedItem
|
||||
.getDescription()
|
||||
.getString();
|
||||
message = String.format(
|
||||
task.getMessageTemplate(),
|
||||
itemName
|
||||
);
|
||||
master.setRequestedItem(requestedItem);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
message = task.getMessageTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
// Set the active task on the master
|
||||
master.setActiveTask(task);
|
||||
|
||||
// FIX: Use MessageDispatcher for consistency with earplug system
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"" + message + "\""
|
||||
).withStyle(Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR))
|
||||
);
|
||||
|
||||
// Also use dialogue system for more variation
|
||||
String dialogueId = switch (task) {
|
||||
case HEEL, FOLLOW_CLOSE -> "petplay.task_heel";
|
||||
case WAIT_HERE -> "petplay.task_wait";
|
||||
case FETCH_ITEM -> "petplay.task_fetch";
|
||||
case KNEEL -> "petplay.task_kneel";
|
||||
case COME -> "petplay.task_come";
|
||||
case PRESENT -> "petplay.task_present";
|
||||
case SPEAK -> "petplay.task_speak";
|
||||
case DROP -> "petplay.task_drop";
|
||||
case DEMAND -> "petplay.task_demand";
|
||||
default -> null;
|
||||
};
|
||||
|
||||
if (dialogueId != null) {
|
||||
DialogueBridge.talkTo(master, pet, dialogueId);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskAssignGoal] {} assigned {} task to {}{}",
|
||||
master.getNpcName(),
|
||||
task,
|
||||
pet.getName().getString(),
|
||||
requestedItem != null
|
||||
? " (item: " + requestedItem.getDescription().getString() + ")"
|
||||
: ""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force assign a specific task (used by external triggers like random events).
|
||||
*/
|
||||
public void forceAssignTask(PetTask task) {
|
||||
this.selectedTask = task;
|
||||
this.hasAssigned = false;
|
||||
master.setMasterState(MasterState.TASK_ASSIGN);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DEMAND TASK - INVENTORY SCANNING
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Scan pet's inventory and find the most valuable item to demand.
|
||||
* Prioritizes: enchanted items > high-value items > medium-value items.
|
||||
*
|
||||
* @param pet The pet player to scan
|
||||
* @return The most valuable item found, or null if inventory is empty/worthless
|
||||
*/
|
||||
@javax.annotation.Nullable
|
||||
private Item findMostValuableItem(ServerPlayer pet) {
|
||||
List<ItemStack> highValue = new ArrayList<>();
|
||||
List<ItemStack> mediumValue = new ArrayList<>();
|
||||
List<ItemStack> enchantedItems = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < pet.getInventory().getContainerSize(); i++) {
|
||||
ItemStack stack = pet.getInventory().getItem(i);
|
||||
if (stack.isEmpty()) continue;
|
||||
|
||||
Item item = stack.getItem();
|
||||
|
||||
// Enchanted items are always high priority (even enchanted books)
|
||||
if (
|
||||
stack.isEnchanted() ||
|
||||
EnchantmentHelper.getEnchantments(stack).size() > 0
|
||||
) {
|
||||
enchantedItems.add(stack);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HIGH_VALUE_ITEMS.contains(item)) {
|
||||
highValue.add(stack);
|
||||
} else if (MEDIUM_VALUE_ITEMS.contains(item)) {
|
||||
mediumValue.add(stack);
|
||||
}
|
||||
}
|
||||
|
||||
// Pick from the highest tier available
|
||||
if (!enchantedItems.isEmpty()) {
|
||||
return enchantedItems
|
||||
.get(master.getRandom().nextInt(enchantedItems.size()))
|
||||
.getItem();
|
||||
}
|
||||
if (!highValue.isEmpty()) {
|
||||
return highValue
|
||||
.get(master.getRandom().nextInt(highValue.size()))
|
||||
.getItem();
|
||||
}
|
||||
if (!mediumValue.isEmpty()) {
|
||||
return mediumValue
|
||||
.get(master.getRandom().nextInt(mediumValue.size()))
|
||||
.getItem();
|
||||
}
|
||||
|
||||
return null; // Nothing worth demanding
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.dialogue.DialogueBridge;
|
||||
import com.tiedup.remake.entities.EntityMaster;
|
||||
import com.tiedup.remake.items.ItemChokeCollar;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.util.MessageDispatcher;
|
||||
import java.util.EnumSet;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.ai.goal.Goal;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* AI Goal for EntityMaster to watch and enforce active tasks.
|
||||
*
|
||||
* Monitors task compliance and triggers choke collar punishment on violation:
|
||||
* - HEEL: Player must stay within 3 blocks of Master
|
||||
* - WAIT_HERE: Player must not move more than 1 block from start position
|
||||
* - FETCH_ITEM: Player must give item within time limit (handled separately via interaction)
|
||||
*/
|
||||
public class MasterTaskWatchGoal extends Goal {
|
||||
|
||||
private final EntityMaster master;
|
||||
|
||||
/** Ticks between compliance checks */
|
||||
private static final int CHECK_INTERVAL = 10;
|
||||
|
||||
/** Choke duration on violation (ticks) - 2 seconds */
|
||||
private static final int CHOKE_DURATION = 40;
|
||||
|
||||
/** Grace period before starting to check (ticks) - 3 seconds */
|
||||
private static final int GRACE_PERIOD = 60;
|
||||
|
||||
/** Warning messages when task is violated */
|
||||
private static final String[] HEEL_VIOLATION_MESSAGES = {
|
||||
"You're too far! Heel!",
|
||||
"Stay close, pet!",
|
||||
"*tugs at your collar*",
|
||||
};
|
||||
|
||||
private static final String[] WAIT_VIOLATION_MESSAGES = {
|
||||
"I said don't move!",
|
||||
"Stay still!",
|
||||
"*activates your collar*",
|
||||
};
|
||||
|
||||
private static final String[] KNEEL_VIOLATION_MESSAGES = {
|
||||
"I said kneel!",
|
||||
"Get back down!",
|
||||
"On your knees, pet!",
|
||||
};
|
||||
|
||||
private static final String[] COME_VIOLATION_MESSAGES = {
|
||||
"Too slow!",
|
||||
"I said come HERE!",
|
||||
"You're taking too long!",
|
||||
};
|
||||
|
||||
private static final String[] PRESENT_VIOLATION_MESSAGES = {
|
||||
"Stand still!",
|
||||
"Don't move away from me!",
|
||||
"Present yourself properly!",
|
||||
};
|
||||
|
||||
private static final String[] DROP_VIOLATION_MESSAGES = {
|
||||
"Drop it!",
|
||||
"Empty your hands. Now.",
|
||||
"I said drop what you're holding!",
|
||||
};
|
||||
|
||||
private static final String[] FOLLOW_CLOSE_VIOLATION_MESSAGES = {
|
||||
"Closer!",
|
||||
"Stay right beside me!",
|
||||
"*yanks your collar*",
|
||||
};
|
||||
|
||||
private int checkTimer = 0;
|
||||
private int taskTimer = 0;
|
||||
private int chokeTimer = 0;
|
||||
private ItemStack activeChokeCollar = ItemStack.EMPTY;
|
||||
|
||||
public MasterTaskWatchGoal(EntityMaster master) {
|
||||
this.master = master;
|
||||
this.setFlags(EnumSet.of(Goal.Flag.LOOK));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canUse() {
|
||||
// Only active in TASK_WATCH state with an active task
|
||||
return (
|
||||
master.getStateManager().getCurrentState() ==
|
||||
MasterState.TASK_WATCH &&
|
||||
master.hasPet() &&
|
||||
master.hasActiveTask()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canContinueToUse() {
|
||||
if (!master.hasPet()) return false;
|
||||
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null || !pet.isAlive()) return false;
|
||||
|
||||
// Continue while in TASK_WATCH state
|
||||
if (
|
||||
master.getStateManager().getCurrentState() != MasterState.TASK_WATCH
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if task duration expired
|
||||
PetTask task = master.getCurrentTask();
|
||||
if (
|
||||
task != null &&
|
||||
task.getDurationTicks() > 0 &&
|
||||
taskTimer >= task.getDurationTicks()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return master.hasActiveTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.checkTimer = 0;
|
||||
this.taskTimer = 0;
|
||||
this.chokeTimer = 0;
|
||||
this.activeChokeCollar = ItemStack.EMPTY;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskWatchGoal] {} started watching task: {}",
|
||||
master.getNpcName(),
|
||||
master.getCurrentTask()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
// Ensure choke is deactivated
|
||||
deactivateChoke();
|
||||
|
||||
// Check if task completed successfully
|
||||
PetTask task = master.getCurrentTask();
|
||||
|
||||
// Determine success based on task type
|
||||
boolean success = false;
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
|
||||
if (
|
||||
task != null &&
|
||||
task.getDurationTicks() > 0 &&
|
||||
taskTimer >= task.getDurationTicks()
|
||||
) {
|
||||
// Duration expired - check end-state success for specific tasks
|
||||
switch (task) {
|
||||
case FETCH_ITEM, DEMAND -> {
|
||||
// Reaching timeout = failure (success handled via handleFetchItemGive/mobInteract)
|
||||
success = false;
|
||||
}
|
||||
case SPEAK -> {
|
||||
// Reaching timeout = failure (success handled via mobInteract)
|
||||
success = false;
|
||||
}
|
||||
case COME -> {
|
||||
// Success if pet is within range at end
|
||||
success =
|
||||
pet != null &&
|
||||
master.distanceTo(pet) <= task.getMaxDistance();
|
||||
}
|
||||
case DROP -> {
|
||||
// Success if hands are empty at end
|
||||
success =
|
||||
pet != null &&
|
||||
pet.getMainHandItem().isEmpty() &&
|
||||
pet.getOffhandItem().isEmpty();
|
||||
}
|
||||
default -> {
|
||||
// Duration-based tasks: completing duration IS success
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
if (pet != null) {
|
||||
DialogueBridge.talkTo(master, pet, "petplay.task_complete");
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskWatchGoal] {} completed task {} successfully",
|
||||
pet.getName().getString(),
|
||||
task
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
task != null &&
|
||||
task.getDurationTicks() > 0 &&
|
||||
taskTimer >= task.getDurationTicks()
|
||||
) {
|
||||
// Task timed out without success
|
||||
if (pet != null) {
|
||||
DialogueBridge.talkTo(master, pet, "petplay.disappointed");
|
||||
// Trigger punishment for failure
|
||||
master.setMasterState(MasterState.PUNISH);
|
||||
master.clearActiveTask();
|
||||
this.checkTimer = 0;
|
||||
this.taskTimer = 0;
|
||||
this.chokeTimer = 0;
|
||||
this.activeChokeCollar = ItemStack.EMPTY;
|
||||
return; // Don't fall through to FOLLOWING
|
||||
}
|
||||
}
|
||||
|
||||
// Clear task and return to following
|
||||
master.clearActiveTask();
|
||||
|
||||
if (master.hasPet()) {
|
||||
master.setMasterState(MasterState.FOLLOWING);
|
||||
}
|
||||
|
||||
this.checkTimer = 0;
|
||||
this.taskTimer = 0;
|
||||
this.chokeTimer = 0;
|
||||
this.activeChokeCollar = ItemStack.EMPTY;
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskWatchGoal] {} stopped watching task (success={})",
|
||||
master.getNpcName(),
|
||||
success
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
ServerPlayer pet = master.getPetPlayer();
|
||||
if (pet == null) return;
|
||||
|
||||
PetTask task = master.getCurrentTask();
|
||||
if (task == null) return;
|
||||
|
||||
// Always look at pet
|
||||
master.getLookControl().setLookAt(pet, 30.0F, 30.0F);
|
||||
|
||||
// Increment task timer
|
||||
taskTimer++;
|
||||
|
||||
// Handle choke cooldown
|
||||
if (chokeTimer > 0) {
|
||||
chokeTimer--;
|
||||
if (chokeTimer <= 0) {
|
||||
deactivateChoke();
|
||||
}
|
||||
}
|
||||
|
||||
// Show progress every 5 seconds via action bar
|
||||
if (
|
||||
task.getDurationTicks() > 0 &&
|
||||
taskTimer % 100 == 0 &&
|
||||
taskTimer >= GRACE_PERIOD
|
||||
) {
|
||||
int remainingSec = (task.getDurationTicks() - taskTimer) / 20;
|
||||
if (remainingSec > 0) {
|
||||
MessageDispatcher.sendActionBar(
|
||||
pet,
|
||||
Component.literal(
|
||||
task.name() + " - " + remainingSec + "s remaining"
|
||||
).withStyle(Style.EMPTY.withColor(0xFFAA00))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip compliance checks during grace period
|
||||
if (taskTimer < GRACE_PERIOD) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check compliance periodically
|
||||
checkTimer++;
|
||||
if (checkTimer >= CHECK_INTERVAL) {
|
||||
checkTimer = 0;
|
||||
checkTaskCompliance(pet, task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if pet is complying with the current task.
|
||||
*/
|
||||
private void checkTaskCompliance(ServerPlayer pet, PetTask task) {
|
||||
boolean violation = false;
|
||||
String[] violationMessages = null;
|
||||
|
||||
switch (task) {
|
||||
case HEEL -> {
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > task.getMaxDistance()) {
|
||||
violation = true;
|
||||
violationMessages = HEEL_VIOLATION_MESSAGES;
|
||||
}
|
||||
}
|
||||
case WAIT_HERE -> {
|
||||
Vec3 startPos = master.getTaskStartPosition();
|
||||
if (startPos != null) {
|
||||
double dist = pet.position().distanceTo(startPos);
|
||||
if (dist > task.getMaxDistance()) {
|
||||
violation = true;
|
||||
violationMessages = WAIT_VIOLATION_MESSAGES;
|
||||
}
|
||||
}
|
||||
}
|
||||
case KNEEL -> {
|
||||
// Must be crouching AND near start position
|
||||
if (!pet.isCrouching()) {
|
||||
violation = true;
|
||||
violationMessages = KNEEL_VIOLATION_MESSAGES;
|
||||
} else {
|
||||
Vec3 startPos = master.getTaskStartPosition();
|
||||
if (startPos != null) {
|
||||
double dist = pet.position().distanceTo(startPos);
|
||||
if (dist > task.getMaxDistance()) {
|
||||
violation = true;
|
||||
violationMessages = KNEEL_VIOLATION_MESSAGES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case COME -> {
|
||||
// Grace period: only check after half the duration has passed
|
||||
double dist = master.distanceTo(pet);
|
||||
if (
|
||||
taskTimer > task.getDurationTicks() / 2 &&
|
||||
dist > task.getMaxDistance()
|
||||
) {
|
||||
violation = true;
|
||||
violationMessages = COME_VIOLATION_MESSAGES;
|
||||
}
|
||||
}
|
||||
case PRESENT -> {
|
||||
// Must stay near master
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > task.getMaxDistance()) {
|
||||
violation = true;
|
||||
violationMessages = PRESENT_VIOLATION_MESSAGES;
|
||||
}
|
||||
}
|
||||
case SPEAK -> {
|
||||
// No continuous check - just late warning at 75% time
|
||||
if (
|
||||
taskTimer >= (int) (task.getDurationTicks() * 0.75) &&
|
||||
taskTimer % 60 == 0
|
||||
) {
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"I'm waiting...\""
|
||||
).withStyle(
|
||||
Style.EMPTY.withColor(
|
||||
EntityMaster.MASTER_NAME_COLOR
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
case DEMAND -> {
|
||||
// No continuous check - just late warning at 60% time (more impatient)
|
||||
if (
|
||||
taskTimer >= (int) (task.getDurationTicks() * 0.6) &&
|
||||
taskTimer % 60 == 0
|
||||
) {
|
||||
String[] demandWarnings = {
|
||||
"I said give it to me.",
|
||||
"Don't make me ask again.",
|
||||
"Hand it over. Now.",
|
||||
"*extends hand impatiently*",
|
||||
};
|
||||
String warning = demandWarnings[master
|
||||
.getRandom()
|
||||
.nextInt(demandWarnings.length)];
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"" + warning + "\""
|
||||
).withStyle(
|
||||
Style.EMPTY.withColor(
|
||||
EntityMaster.MASTER_NAME_COLOR
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
case DROP -> {
|
||||
if (
|
||||
!pet.getMainHandItem().isEmpty() ||
|
||||
!pet.getOffhandItem().isEmpty()
|
||||
) {
|
||||
violation = true;
|
||||
violationMessages = DROP_VIOLATION_MESSAGES;
|
||||
}
|
||||
}
|
||||
case FOLLOW_CLOSE -> {
|
||||
double dist = master.distanceTo(pet);
|
||||
if (dist > task.getMaxDistance()) {
|
||||
violation = true;
|
||||
violationMessages = FOLLOW_CLOSE_VIOLATION_MESSAGES;
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
// Other tasks don't have continuous checks
|
||||
}
|
||||
}
|
||||
|
||||
if (violation && violationMessages != null) {
|
||||
triggerPunishment(pet, violationMessages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger choke collar punishment for task violation.
|
||||
*/
|
||||
private void triggerPunishment(ServerPlayer pet, String[] messages) {
|
||||
// Only punish if not already choking
|
||||
if (chokeTimer > 0) return;
|
||||
|
||||
// FIX: Use MessageDispatcher for consistency with earplug system
|
||||
String message = messages[master.getRandom().nextInt(messages.length)];
|
||||
MessageDispatcher.sendChat(
|
||||
pet,
|
||||
Component.literal(
|
||||
master.getNpcName() + ": \"" + message + "\""
|
||||
).withStyle(Style.EMPTY.withColor(EntityMaster.MASTER_NAME_COLOR))
|
||||
);
|
||||
|
||||
// Activate choke collar
|
||||
PlayerBindState bindState = PlayerBindState.getInstance(pet);
|
||||
if (bindState != null && bindState.hasCollar()) {
|
||||
ItemStack collar = bindState.getEquipment(BodyRegionV2.NECK);
|
||||
|
||||
if (collar.getItem() instanceof ItemChokeCollar chokeCollar) {
|
||||
chokeCollar.setChoking(collar, true);
|
||||
this.activeChokeCollar = collar;
|
||||
this.chokeTimer = CHOKE_DURATION;
|
||||
|
||||
// Play choking sound
|
||||
pet
|
||||
.level()
|
||||
.playSound(
|
||||
null,
|
||||
pet.getX(),
|
||||
pet.getY(),
|
||||
pet.getZ(),
|
||||
SoundEvents.PLAYER_HURT,
|
||||
SoundSource.HOSTILE,
|
||||
0.8f,
|
||||
0.5f + master.getRandom().nextFloat() * 0.2f
|
||||
);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskWatchGoal] {} triggered choke punishment on {}",
|
||||
master.getNpcName(),
|
||||
pet.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate the choke collar if active.
|
||||
*/
|
||||
private void deactivateChoke() {
|
||||
if (
|
||||
!activeChokeCollar.isEmpty() &&
|
||||
activeChokeCollar.getItem() instanceof ItemChokeCollar chokeCollar
|
||||
) {
|
||||
chokeCollar.setChoking(activeChokeCollar, false);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[MasterTaskWatchGoal] {} deactivated choke",
|
||||
master.getNpcName()
|
||||
);
|
||||
}
|
||||
this.activeChokeCollar = ItemStack.EMPTY;
|
||||
this.chokeTimer = 0;
|
||||
}
|
||||
}
|
||||
173
src/main/java/com/tiedup/remake/entities/ai/master/PetTask.java
Normal file
173
src/main/java/com/tiedup/remake/entities/ai/master/PetTask.java
Normal file
@@ -0,0 +1,173 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
/**
|
||||
* Enum defining the types of tasks a Master can assign to their pet.
|
||||
*/
|
||||
public enum PetTask {
|
||||
/**
|
||||
* Heel - Pet must stay within 3 blocks of Master.
|
||||
* Punishment: Choke if distance > 3 blocks
|
||||
*/
|
||||
HEEL("Heel, pet. Stay close to me. [Stay within 3 blocks]", 1200, 3.0),
|
||||
|
||||
/**
|
||||
* Wait Here - Pet cannot move from current position.
|
||||
* Punishment: Choke if moves more than 2 blocks
|
||||
*/
|
||||
WAIT_HERE("Stay here. Don't move. [Stay within 2 blocks]", 800, 2.0),
|
||||
|
||||
/**
|
||||
* Fetch Item - Pet must give Master a specific item.
|
||||
* Punishment: Choke if item not given within time limit
|
||||
*/
|
||||
FETCH_ITEM("Bring me %s. [Right-click on me with the item]", 2400, 0.0),
|
||||
|
||||
/**
|
||||
* Kneel - Pet must crouch and not move.
|
||||
* Punishment if pet stands up or moves.
|
||||
*/
|
||||
KNEEL("Kneel. [Hold shift, don't move]", 600, 1.5),
|
||||
|
||||
/**
|
||||
* Come - Pet must reach Master quickly.
|
||||
* Speed test - must be within range before time runs out.
|
||||
*/
|
||||
COME("Come here! Now! [Reach me within 10 seconds]", 200, 2.0),
|
||||
|
||||
/**
|
||||
* Present - Pet must stand near Master without moving.
|
||||
* Punishment if pet moves away.
|
||||
*/
|
||||
PRESENT(
|
||||
"Present yourself. Stand before me. [Stand still near me]",
|
||||
400,
|
||||
2.0
|
||||
),
|
||||
|
||||
/**
|
||||
* Speak - Pet must right-click on Master.
|
||||
* Interaction-based task.
|
||||
*/
|
||||
SPEAK("Speak, pet. [Right-click on me]", 300, 0.0),
|
||||
|
||||
/**
|
||||
* Drop - Pet must empty both hands.
|
||||
* Punishment if hands are not empty.
|
||||
*/
|
||||
DROP("Drop what you're holding. [Empty both hands]", 400, 0.0),
|
||||
|
||||
/**
|
||||
* Follow Close - Like HEEL but tighter distance, longer duration.
|
||||
* Punishment if pet strays more than 1.5 blocks.
|
||||
*/
|
||||
FOLLOW_CLOSE(
|
||||
"Stay right beside me. Don't stray. [Stay within 1.5 blocks]",
|
||||
1600,
|
||||
1.5
|
||||
),
|
||||
|
||||
/**
|
||||
* Demand - Master demands a valuable item from pet's inventory.
|
||||
* Scans inventory and picks the most precious item the pet has.
|
||||
* Interaction-based completion (same as FETCH_ITEM).
|
||||
*/
|
||||
DEMAND("Give me that %s. Now. [Right-click on me with the item]", 600, 0.0),
|
||||
|
||||
/**
|
||||
* Random Bind - Master puts a random bind/accessory on pet temporarily.
|
||||
* This is not really a "task" but an event.
|
||||
*/
|
||||
RANDOM_BIND("Hold still...", 0, 0.0),
|
||||
|
||||
/**
|
||||
* Dogwalk - Master initiates a walk with the pet.
|
||||
* Not a task per se, but a special event.
|
||||
*/
|
||||
DOGWALK("Time for a walk.", 0, 0.0);
|
||||
|
||||
private final String messageTemplate;
|
||||
private final int durationTicks;
|
||||
private final double maxDistance;
|
||||
|
||||
PetTask(String messageTemplate, int durationTicks, double maxDistance) {
|
||||
this.messageTemplate = messageTemplate;
|
||||
this.durationTicks = durationTicks;
|
||||
this.maxDistance = maxDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message template for this task.
|
||||
* May contain %s for item names.
|
||||
*/
|
||||
public String getMessageTemplate() {
|
||||
return messageTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration of this task in ticks.
|
||||
*/
|
||||
public int getDurationTicks() {
|
||||
return durationTicks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum allowed distance for distance-based tasks.
|
||||
* For HEEL: max distance from Master.
|
||||
* For WAIT_HERE: max distance from starting position.
|
||||
*/
|
||||
public double getMaxDistance() {
|
||||
return maxDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this task requires distance monitoring.
|
||||
*/
|
||||
public boolean requiresDistanceCheck() {
|
||||
return (
|
||||
this == HEEL ||
|
||||
this == WAIT_HERE ||
|
||||
this == FOLLOW_CLOSE ||
|
||||
this == KNEEL ||
|
||||
this == PRESENT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a real task (vs an event like RANDOM_BIND or DOGWALK).
|
||||
*/
|
||||
public boolean isRealTask() {
|
||||
return (
|
||||
this == HEEL ||
|
||||
this == WAIT_HERE ||
|
||||
this == FETCH_ITEM ||
|
||||
this == KNEEL ||
|
||||
this == COME ||
|
||||
this == PRESENT ||
|
||||
this == SPEAK ||
|
||||
this == DROP ||
|
||||
this == FOLLOW_CLOSE ||
|
||||
this == DEMAND
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this task requires the pet to be crouching.
|
||||
*/
|
||||
public boolean requiresCrouching() {
|
||||
return this == KNEEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this task requires an interaction to complete.
|
||||
*/
|
||||
public boolean requiresInteraction() {
|
||||
return this == SPEAK || this == DEMAND;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this task requires empty hands.
|
||||
*/
|
||||
public boolean requiresEmptyHands() {
|
||||
return this == DROP;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.tiedup.remake.entities.ai.master;
|
||||
|
||||
/**
|
||||
* Enum defining the types of punishments a Master can inflict on their pet.
|
||||
*/
|
||||
public enum PunishmentType {
|
||||
/** Activate choke collar - requires pet to have a choke collar */
|
||||
CHOKE_COLLAR(80, "punishment.choke"),
|
||||
|
||||
/** Apply temporary blindfold - requires blindfold slot empty */
|
||||
BLINDFOLD(2400, "punishment.blindfold"),
|
||||
|
||||
/** Apply temporary gag - requires gag slot empty */
|
||||
GAG(2400, "punishment.gag"),
|
||||
|
||||
/** Apply temporary mittens - requires mittens slot empty */
|
||||
MITTENS(2400, "punishment.mittens"),
|
||||
|
||||
/** Apply armbinder - requires pet to not be bound */
|
||||
TIGHTEN_RESTRAINTS(0, "punishment.tighten"),
|
||||
|
||||
/** Ignore pet interactions for a duration */
|
||||
COLD_SHOULDER(1200, "punishment.cold_shoulder"),
|
||||
|
||||
/** Yank pet toward master with brief leash */
|
||||
LEASH_TUG(40, "punishment.leash_tug");
|
||||
|
||||
private final int durationTicks;
|
||||
private final String dialogueId;
|
||||
|
||||
PunishmentType(int durationTicks, String dialogueId) {
|
||||
this.durationTicks = durationTicks;
|
||||
this.dialogueId = dialogueId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration of this punishment in ticks.
|
||||
*/
|
||||
public int getDurationTicks() {
|
||||
return durationTicks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dialogue ID for this punishment type.
|
||||
*/
|
||||
public String getDialogueId() {
|
||||
return dialogueId;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user