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,472 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.ai.damsel.*;
|
||||
import com.tiedup.remake.entities.ai.personality.*;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
import java.util.List;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.ai.goal.FloatGoal;
|
||||
import net.minecraft.world.entity.ai.goal.OpenDoorGoal;
|
||||
import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal;
|
||||
import net.minecraft.world.entity.ai.goal.target.TargetGoal;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Manages all AI-related systems for EntityDamsel:
|
||||
* - AI goals registration (25+ goals in priority order)
|
||||
* - Call for help system (captive calling for rescue)
|
||||
* - Leash traction system (prevents stuck while leashed)
|
||||
*
|
||||
* Phase 5: Extracted from EntityDamsel.java (~450 lines, 5 methods)
|
||||
*/
|
||||
public class DamselAIController {
|
||||
|
||||
private final EntityDamsel entity;
|
||||
private final IAIHost host;
|
||||
|
||||
// ========================================
|
||||
// CALL FOR HELP SYSTEM
|
||||
// ========================================
|
||||
|
||||
/** Cooldown between call for help messages */
|
||||
private int callForHelpCooldown = 0;
|
||||
|
||||
/** Radius to search for players to call for help (blocks) */
|
||||
private static final int CALL_FOR_HELP_RADIUS = 15;
|
||||
|
||||
/** Minimum cooldown for call for help (5 seconds) */
|
||||
private static final int CALL_FOR_HELP_COOLDOWN_MIN = 100;
|
||||
|
||||
/** Maximum cooldown for call for help (10 seconds) */
|
||||
private static final int CALL_FOR_HELP_COOLDOWN_MAX = 200;
|
||||
|
||||
// ========================================
|
||||
// LEASH TRACTION SYSTEM
|
||||
// ========================================
|
||||
|
||||
/** Counter for how long NPC has been stuck while leashed */
|
||||
private int leashStuckCounter = 0;
|
||||
|
||||
/** Previous X position for stuck detection */
|
||||
private double leashPrevX;
|
||||
|
||||
/** Previous Z position for stuck detection */
|
||||
private double leashPrevZ;
|
||||
|
||||
/** Distance from holder at which pulling starts (blocks) */
|
||||
private static final double LEASH_PULL_START_DISTANCE = 2.5;
|
||||
|
||||
/** Distance at which stuck teleport begins (blocks) */
|
||||
private static final double LEASH_TELEPORT_DISTANCE = 5.0;
|
||||
|
||||
/** Maximum distance before forced teleport (blocks) */
|
||||
private static final double LEASH_MAX_DISTANCE = 8.0;
|
||||
|
||||
/** Ticks stuck before teleport (40 ticks = 2 seconds) */
|
||||
private static final int LEASH_STUCK_THRESHOLD = 40;
|
||||
|
||||
/** FIX: Max traction force - reduced for smoother movement */
|
||||
private static final double LEASH_MAX_FORCE = 0.12;
|
||||
|
||||
/** FIX: Force ramp factor - gradual increase based on distance */
|
||||
private static final double LEASH_FORCE_RAMP = 0.05;
|
||||
|
||||
// ========================================
|
||||
// CONSTRUCTOR
|
||||
// ========================================
|
||||
|
||||
public DamselAIController(EntityDamsel entity, IAIHost host) {
|
||||
this.entity = entity;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// AI GOALS REGISTRATION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Register all AI goals for this entity.
|
||||
* Must be called from EntityDamsel.registerGoals() with goalSelector and targetSelector.
|
||||
*
|
||||
* @param goalSelector The goal selector from Mob
|
||||
* @param targetSelector The target selector from Mob
|
||||
*/
|
||||
public void registerGoals(
|
||||
net.minecraft.world.entity.ai.goal.GoalSelector goalSelector,
|
||||
net.minecraft.world.entity.ai.goal.GoalSelector targetSelector
|
||||
) {
|
||||
// Priority 0: Always swim (FloatGoal)
|
||||
goalSelector.addGoal(0, new FloatGoal(entity));
|
||||
|
||||
// Priority 1: Personality command goals (take precedence when active)
|
||||
goalSelector.addGoal(1, new NpcFollowCommandGoal(entity, 1.0));
|
||||
// NpcHeelCommandGoal removed - HEEL is now a FollowDistance mode in NpcFollowCommandGoal
|
||||
goalSelector.addGoal(1, new NpcStayCommandGoal(entity));
|
||||
goalSelector.addGoal(1, new NpcIdleCommandGoal(entity));
|
||||
goalSelector.addGoal(1, new NpcComeCommandGoal(entity, 1.2));
|
||||
goalSelector.addGoal(1, new NpcGoHomeGoal(entity));
|
||||
goalSelector.addGoal(1, new NpcSitCommandGoal(entity));
|
||||
goalSelector.addGoal(1, new NpcKneelCommandGoal(entity));
|
||||
goalSelector.addGoal(1, new NpcPatrolCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(1, new NpcGuardCommandGoal(entity));
|
||||
goalSelector.addGoal(1, new NpcFetchCommandGoal(entity, 1.1));
|
||||
goalSelector.addGoal(1, new NpcCollectCommandGoal(entity, 1.0));
|
||||
// NOTE: Combat goals (Defend, Attack, Capture) removed.
|
||||
// Combat behavior is now handled by NpcFollowCommandGoal based on main hand item.
|
||||
// Work commands (FARM, COOK, STORE) - chest-hub system
|
||||
goalSelector.addGoal(1, new NpcFarmCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(1, new NpcCookCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(1, new NpcTransferCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(1, new NpcShearCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(1, new NpcMineCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(1, new NpcBreedCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(1, new NpcFishCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(1, new NpcSortCommandGoal(entity, 1.0));
|
||||
|
||||
// Priority 2: Struggle goal (passive, runs alongside other goals - no flags)
|
||||
goalSelector.addGoal(2, new NpcStruggleGoal(entity));
|
||||
|
||||
// Priority 2: Auto-eat goal (passive, eats from inventory when hungry)
|
||||
goalSelector.addGoal(2, new NpcAutoEatGoal(entity));
|
||||
|
||||
// Priority 2: Auto-rest goal (passive, goes home to rest when tired)
|
||||
goalSelector.addGoal(2, new NpcAutoRestGoal(entity));
|
||||
|
||||
// Priority 2-5: Custom conditional AI goals
|
||||
goalSelector.addGoal(
|
||||
2,
|
||||
new DamselFleeFromPlayerGoal(entity, 10.0f, 1.1, 1.3)
|
||||
);
|
||||
goalSelector.addGoal(3, new DamselPanicGoal(entity, 1.0));
|
||||
goalSelector.addGoal(4, new DamselWanderGoal(entity, 1.2));
|
||||
goalSelector.addGoal(
|
||||
5,
|
||||
new DamselWatchPlayerGoal(entity, Player.class, 6.0f)
|
||||
);
|
||||
|
||||
// Priority 6-7: Basic vanilla goals
|
||||
goalSelector.addGoal(6, new RandomLookAroundGoal(entity));
|
||||
goalSelector.addGoal(7, new OpenDoorGoal(entity, false));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COMMAND GOALS UTILITY
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Register all NPC command goals on a goal selector.
|
||||
* Used by subclasses (EntityMaid, EntitySlaveTrader, EntityMaster) that override
|
||||
* registerGoals() but still need command functionality.
|
||||
*
|
||||
* @param goalSelector The goal selector to register on
|
||||
* @param entity The entity (must extend EntityDamsel)
|
||||
* @param priority The priority level for command goals
|
||||
*/
|
||||
public static void registerCommandGoals(
|
||||
net.minecraft.world.entity.ai.goal.GoalSelector goalSelector,
|
||||
EntityDamsel entity,
|
||||
int priority
|
||||
) {
|
||||
goalSelector.addGoal(priority, new NpcFollowCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(priority, new NpcStayCommandGoal(entity));
|
||||
goalSelector.addGoal(priority, new NpcIdleCommandGoal(entity));
|
||||
goalSelector.addGoal(priority, new NpcComeCommandGoal(entity, 1.2));
|
||||
goalSelector.addGoal(priority, new NpcGoHomeGoal(entity));
|
||||
goalSelector.addGoal(priority, new NpcSitCommandGoal(entity));
|
||||
goalSelector.addGoal(priority, new NpcKneelCommandGoal(entity));
|
||||
goalSelector.addGoal(priority, new NpcPatrolCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(priority, new NpcGuardCommandGoal(entity));
|
||||
goalSelector.addGoal(priority, new NpcFetchCommandGoal(entity, 1.1));
|
||||
goalSelector.addGoal(priority, new NpcCollectCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(priority, new NpcFarmCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(priority, new NpcCookCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(priority, new NpcTransferCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(priority, new NpcShearCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(priority, new NpcMineCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(priority, new NpcBreedCommandGoal(entity, 1.0));
|
||||
goalSelector.addGoal(priority, new NpcFishCommandGoal(entity, 0.9));
|
||||
goalSelector.addGoal(priority, new NpcSortCommandGoal(entity, 1.0));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CALL FOR HELP SYSTEM
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Periodically call for help to nearby players when being led as a captive.
|
||||
* Only works if:
|
||||
* - Damsel is tied up
|
||||
* - Damsel is a captive (being led)
|
||||
* - Not gagged (can still speak)
|
||||
* - There's a player nearby who is NOT the captor
|
||||
*
|
||||
* Call this from EntityDamsel.aiStep().
|
||||
*/
|
||||
public void tickCallForHelp() {
|
||||
// Decrement cooldown
|
||||
if (this.callForHelpCooldown > 0) {
|
||||
this.callForHelpCooldown--;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this entity can call for help (overridable in EntityKidnapper)
|
||||
if (!entity.canCallForHelp()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be tied and a captive to call for help
|
||||
if (!host.isTiedUp() || !host.isCaptive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Can't call for help if gagged
|
||||
if (host.isGagged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find nearby players who could help
|
||||
List<Player> nearbyPlayers = host
|
||||
.level()
|
||||
.getEntitiesOfClass(
|
||||
Player.class,
|
||||
host
|
||||
.getBoundingBox()
|
||||
.inflate(ModConfig.SERVER.dialogueRadius.get())
|
||||
);
|
||||
|
||||
// Get captor entity for comparison
|
||||
Entity captorEntity =
|
||||
host.getBondageManager().getCaptor() != null
|
||||
? host.getBondageManager().getCaptor().getEntity()
|
||||
: null;
|
||||
|
||||
boolean foundTarget = false;
|
||||
|
||||
for (Player player : nearbyPlayers) {
|
||||
// Skip creative/spectator
|
||||
if (player.isCreative() || player.isSpectator()) continue;
|
||||
|
||||
// Skip the captor - don't ask your captor for help!
|
||||
if (captorEntity != null && player == captorEntity) continue;
|
||||
|
||||
// Call for help to this player
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} calling for help to {}",
|
||||
host.getNpcName(),
|
||||
player.getName().getString()
|
||||
);
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.callForHelp(
|
||||
entity,
|
||||
player
|
||||
);
|
||||
|
||||
// Found a target, set flag
|
||||
foundTarget = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset cooldown
|
||||
int baseCooldown = ModConfig.SERVER.dialogueCooldown.get();
|
||||
if (foundTarget) {
|
||||
// Full cooldown if we talked
|
||||
this.callForHelpCooldown =
|
||||
baseCooldown + host.getRandom().nextInt(baseCooldown);
|
||||
} else {
|
||||
// Short cooldown if no one found (prevent spam) - 2 to 4 seconds
|
||||
this.callForHelpCooldown = 40 + host.getRandom().nextInt(40);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// LEASH TRACTION SYSTEM
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Tick leash traction system for NPCs.
|
||||
* Detects when NPC is stuck while leashed and teleports to holder.
|
||||
* This mirrors the player leash system in MixinServerPlayer.
|
||||
*
|
||||
* FIX: Now applies ACTIVE traction force towards holder (was missing).
|
||||
* FIX: Increases step height temporarily when being pulled to climb stairs.
|
||||
*
|
||||
* Call this from EntityDamsel.aiStep().
|
||||
*/
|
||||
public void tickLeashTraction() {
|
||||
// Only process if leashed
|
||||
Entity leashHolder = host.getLeashHolder();
|
||||
if (leashHolder == null) {
|
||||
this.leashStuckCounter = 0;
|
||||
// Reset step height when not leashed
|
||||
if (host.maxUpStep() > 0.6f) {
|
||||
host.setMaxUpStep(0.6f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Same level check
|
||||
if (leashHolder.level() != host.level()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float distance = host.distanceTo(leashHolder);
|
||||
|
||||
// Close enough: reset stuck counter, reset step height
|
||||
if (distance < LEASH_PULL_START_DISTANCE) {
|
||||
this.leashStuckCounter = 0;
|
||||
if (host.maxUpStep() > 0.6f) {
|
||||
host.setMaxUpStep(0.6f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Too far: forced teleport to holder (regardless of stuck status)
|
||||
if (distance > LEASH_MAX_DISTANCE) {
|
||||
teleportToSafePositionNearHolder(leashHolder);
|
||||
this.leashStuckCounter = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate direction to holder
|
||||
double dx = leashHolder.getX() - host.getX();
|
||||
double dy = leashHolder.getY() - host.getY();
|
||||
double dz = leashHolder.getZ() - host.getZ();
|
||||
|
||||
// Calculate movement since last tick
|
||||
double movedX = host.getX() - this.leashPrevX;
|
||||
double movedZ = host.getZ() - this.leashPrevZ;
|
||||
double movedHorizontal = Math.sqrt(movedX * movedX + movedZ * movedZ);
|
||||
|
||||
// Store current position for next tick
|
||||
this.leashPrevX = host.getX();
|
||||
this.leashPrevZ = host.getZ();
|
||||
|
||||
// FIX: Apply ACTIVE traction force towards holder with smooth ramping
|
||||
if (distance > LEASH_PULL_START_DISTANCE) {
|
||||
// Normalize direction
|
||||
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
|
||||
double dirX = horizontalDist > 0.01 ? dx / horizontalDist : 0;
|
||||
double dirZ = horizontalDist > 0.01 ? dz / horizontalDist : 0;
|
||||
|
||||
// FIX: Smoother force calculation - gradual ramp up
|
||||
// Force increases with distance but is capped for smooth movement
|
||||
double distanceBeyond = distance - LEASH_PULL_START_DISTANCE;
|
||||
double forceFactor = Math.min(
|
||||
LEASH_MAX_FORCE,
|
||||
distanceBeyond * LEASH_FORCE_RAMP
|
||||
);
|
||||
|
||||
// FIX: Blend with current motion instead of adding directly
|
||||
// This prevents jerky acceleration
|
||||
net.minecraft.world.phys.Vec3 currentMotion =
|
||||
host.getDeltaMovement();
|
||||
double blendFactor = 0.7; // 70% new direction, 30% current momentum
|
||||
|
||||
double newVelX =
|
||||
currentMotion.x * (1 - blendFactor) +
|
||||
dirX * forceFactor * blendFactor * 3;
|
||||
double newVelZ =
|
||||
currentMotion.z * (1 - blendFactor) +
|
||||
dirZ * forceFactor * blendFactor * 3;
|
||||
double newVelY = currentMotion.y + (dy > 0.5 ? 0.03 : 0); // Gentler Y boost
|
||||
|
||||
host.setDeltaMovement(
|
||||
new net.minecraft.world.phys.Vec3(newVelX, newVelY, newVelZ)
|
||||
);
|
||||
|
||||
// Increase step height while being pulled (helps with stairs)
|
||||
host.setMaxUpStep(1.0f);
|
||||
}
|
||||
|
||||
// Strict stuck detection: not moving despite being far
|
||||
boolean isStuck =
|
||||
distance > LEASH_TELEPORT_DISTANCE &&
|
||||
host.getDeltaMovement().horizontalDistanceSqr() < 0.001 &&
|
||||
movedHorizontal < 0.05;
|
||||
|
||||
if (isStuck) {
|
||||
this.leashStuckCounter++;
|
||||
|
||||
// Safety teleport if stuck for too long (30 ticks = 1.5 sec)
|
||||
if (this.leashStuckCounter >= LEASH_STUCK_THRESHOLD) {
|
||||
teleportToSafePositionNearHolder(leashHolder);
|
||||
this.leashStuckCounter = 0;
|
||||
}
|
||||
} else {
|
||||
// Reset stuck counter if moving
|
||||
this.leashStuckCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teleport NPC to a safe position near the leash holder.
|
||||
*/
|
||||
private void teleportToSafePositionNearHolder(Entity holder) {
|
||||
// Target: 2 blocks away from holder in our direction
|
||||
double dx = host.getX() - holder.getX();
|
||||
double dz = host.getZ() - holder.getZ();
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
double offsetX = 0;
|
||||
double offsetZ = 0;
|
||||
if (dist > 0.1) {
|
||||
offsetX = (dx / dist) * 2.0;
|
||||
offsetZ = (dz / dist) * 2.0;
|
||||
}
|
||||
|
||||
double targetX = holder.getX() + offsetX;
|
||||
double targetZ = holder.getZ() + offsetZ;
|
||||
|
||||
// Find safe Y (ground level)
|
||||
double targetY = findSafeY(targetX, holder.getY(), targetZ);
|
||||
|
||||
host.teleportTo(targetX, targetY, targetZ);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} stuck while leashed, teleported to holder",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a safe Y coordinate for teleporting.
|
||||
*/
|
||||
private double findSafeY(double x, double startY, double z) {
|
||||
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||
|
||||
// Search down first (max 5 blocks)
|
||||
for (int y = 0; y > -5; y--) {
|
||||
mutable.set((int) x, (int) startY + y, (int) z);
|
||||
if (
|
||||
host
|
||||
.level()
|
||||
.getBlockState(mutable)
|
||||
.isSolidRender(host.level(), mutable) &&
|
||||
host.level().getBlockState(mutable.above()).isAir()
|
||||
) {
|
||||
return mutable.getY() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Search up (max 5 blocks)
|
||||
for (int y = 1; y < 5; y++) {
|
||||
mutable.set((int) x, (int) startY + y, (int) z);
|
||||
if (
|
||||
host
|
||||
.level()
|
||||
.getBlockState(mutable.below())
|
||||
.isSolidRender(host.level(), mutable.below()) &&
|
||||
host.level().getBlockState(mutable).isAir()
|
||||
) {
|
||||
return mutable.getY();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use holder's Y
|
||||
return startY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.util.RotationSmoother;
|
||||
import dev.kosmx.playerAnim.api.layered.AnimationStack;
|
||||
import dev.kosmx.playerAnim.api.layered.IAnimation;
|
||||
import dev.kosmx.playerAnim.impl.animation.AnimationApplier;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/**
|
||||
* Manages all animation-related systems for EntityDamsel:
|
||||
* - IAnimatedPlayer implementation (PlayerAnimator mod)
|
||||
* - Animation stack management
|
||||
* - Pose management (sitting, kneeling, dog, struggling, trembling)
|
||||
* - Dog pose rotation smoothing
|
||||
*
|
||||
* Phase 3: Extracted from EntityDamsel.java (~220 lines, 14 methods)
|
||||
*/
|
||||
public class DamselAnimationController {
|
||||
|
||||
private final IAnimationHost host;
|
||||
|
||||
// ========================================
|
||||
// ANIMATION SYSTEM
|
||||
// ========================================
|
||||
|
||||
/** Animation stack for PlayerAnimator mod */
|
||||
private final AnimationStack animationStack = new AnimationStack();
|
||||
|
||||
/** Animation applier for model rendering */
|
||||
private final AnimationApplier animationApplier = new AnimationApplier(
|
||||
animationStack
|
||||
);
|
||||
|
||||
/** Stored animations by ID */
|
||||
private final Map<ResourceLocation, IAnimation> storedAnimations =
|
||||
new HashMap<>();
|
||||
|
||||
// ========================================
|
||||
// DOG POSE ROTATION SMOOTHING
|
||||
// ========================================
|
||||
|
||||
/** Rotation smoother for DOG pose (prevents sudden spinning) */
|
||||
private final RotationSmoother dogPoseRotationSmoother =
|
||||
new RotationSmoother();
|
||||
|
||||
/** Track DOG pose state for transition detection */
|
||||
private boolean wasInDogPose = false;
|
||||
|
||||
// ========================================
|
||||
// CONSTRUCTOR
|
||||
// ========================================
|
||||
|
||||
public DamselAnimationController(IAnimationHost host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TICK LOGIC
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Tick animation systems BEFORE super.tick().
|
||||
* Call this from EntityDamsel.tick() BEFORE super.tick().
|
||||
*
|
||||
* Handles:
|
||||
* - DOG pose transition detection (initializes smoother)
|
||||
*/
|
||||
public void tickAnimationBeforeSuperTick() {
|
||||
// DOG pose: Detect transition to initialize smoother
|
||||
boolean inDogPose = host.isDogPose();
|
||||
if (inDogPose && !wasInDogPose) {
|
||||
// Just entered DOG pose - initialize smoother to current rotation
|
||||
this.dogPoseRotationSmoother.setCurrent(host.getYBodyRot());
|
||||
}
|
||||
this.wasInDogPose = inDogPose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick DOG pose rotation smoothing AFTER super.tick().
|
||||
* Call this from EntityDamsel.tick() AFTER super.tick().
|
||||
*
|
||||
* FIX: Must run AFTER super.tick() because:
|
||||
* 1. super.tick() calculates new yBodyRot based on movement direction
|
||||
* 2. We smooth that final calculated value
|
||||
* 3. If we smooth before, Minecraft overwrites our smoothed value
|
||||
*
|
||||
* FIX: Only set yBodyRot, not yBodyRotO:
|
||||
* - yBodyRotO is the "old" rotation from last tick, used for interpolation
|
||||
* - Setting both to the same value breaks inter-frame interpolation
|
||||
* - Minecraft's renderer uses lerp(yBodyRotO, yBodyRot, partialTicks)
|
||||
* - If both are equal, there's no interpolation = visual snapping
|
||||
*/
|
||||
public void tickDogPoseRotationSmoothing() {
|
||||
if (!host.isDogPose()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the target rotation (what Minecraft calculated based on movement)
|
||||
float targetRot = host.getYBodyRot();
|
||||
|
||||
// Smooth towards target (0.15 = 15% per tick, faster but still smooth)
|
||||
float smoothedRot = this.dogPoseRotationSmoother.smooth(
|
||||
targetRot,
|
||||
0.15f
|
||||
);
|
||||
|
||||
// Only set yBodyRot - let yBodyRotO be managed by Minecraft for interpolation
|
||||
host.setYBodyRot(smoothedRot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick animation stack (client-side only).
|
||||
* Call this from EntityDamsel.tick() AFTER tickAnimationBeforeSuperTick().
|
||||
*
|
||||
* @return true if animation tick was handled (client-side), false if server should continue
|
||||
*/
|
||||
public boolean tickAnimationStack() {
|
||||
// Client-side: tick animation stack
|
||||
if (host.level().isClientSide) {
|
||||
this.animationStack.tick();
|
||||
return true; // Signal to caller to return early
|
||||
}
|
||||
return false; // Server-side, continue with tick logic
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IANIMATEDPLAYER INTERFACE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the animation stack.
|
||||
* Required by IAnimatedPlayer interface.
|
||||
*
|
||||
* @return AnimationStack for this entity
|
||||
*/
|
||||
public AnimationStack getAnimationStack() {
|
||||
return this.animationStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the animation applier for model rendering.
|
||||
* Required by IAnimatedPlayer interface.
|
||||
*
|
||||
* <p>The AnimationApplier is used by DamselModel.setupAnim() to apply
|
||||
* current animation transforms to model parts via emote.updatePart().
|
||||
*
|
||||
* @return AnimationApplier for this entity
|
||||
*/
|
||||
public AnimationApplier playerAnimator_getAnimation() {
|
||||
return this.animationApplier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a stored animation by ID.
|
||||
* Required by IAnimatedPlayer interface.
|
||||
*
|
||||
* @param id Animation identifier
|
||||
* @return The stored animation, or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
public IAnimation playerAnimator_getAnimation(ResourceLocation id) {
|
||||
return this.storedAnimations.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store an animation by ID.
|
||||
* Required by IAnimatedPlayer interface.
|
||||
*
|
||||
* @param id Animation identifier
|
||||
* @param animation Animation to store, or null to remove
|
||||
* @return The previously stored animation, or null
|
||||
*/
|
||||
@Nullable
|
||||
public IAnimation playerAnimator_setAnimation(
|
||||
ResourceLocation id,
|
||||
@Nullable IAnimation animation
|
||||
) {
|
||||
if (animation == null) {
|
||||
return this.storedAnimations.remove(id);
|
||||
}
|
||||
return this.storedAnimations.put(id, animation);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// POSE MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if NPC is currently sitting.
|
||||
* @return true if in sitting pose
|
||||
*/
|
||||
public boolean isSitting() {
|
||||
return host.isSittingFromData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sitting pose state.
|
||||
* @param sitting true to enter sitting pose
|
||||
*/
|
||||
public void setSitting(boolean sitting) {
|
||||
host.setSittingToData(sitting);
|
||||
// Clear kneeling if sitting
|
||||
if (sitting) {
|
||||
host.setKneelingToData(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NPC is currently kneeling.
|
||||
* @return true if in kneeling pose
|
||||
*/
|
||||
public boolean isKneeling() {
|
||||
return host.isKneelingFromData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set kneeling pose state.
|
||||
* @param kneeling true to enter kneeling pose
|
||||
*/
|
||||
public void setKneeling(boolean kneeling) {
|
||||
host.setKneelingToData(kneeling);
|
||||
// Clear sitting if kneeling
|
||||
if (kneeling) {
|
||||
host.setSittingToData(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NPC is in any pose (sitting, kneeling, or dog).
|
||||
* @return true if in a pose
|
||||
*/
|
||||
public boolean isInPose() {
|
||||
return this.isSitting() || this.isKneeling() || host.isDogPose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NPC is in DOG pose (based on equipped bind).
|
||||
* @return true if equipped bind has DOG pose type
|
||||
*/
|
||||
public boolean isDogPose() {
|
||||
return host.isDogPose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NPC is currently struggling against restraints.
|
||||
* Used for animation sync.
|
||||
* @return true if struggling
|
||||
*/
|
||||
public boolean isStruggling() {
|
||||
return host.isStrugglingFromData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set struggling state for animation.
|
||||
* @param struggling true if starting struggle animation
|
||||
*/
|
||||
public void setStruggling(boolean struggling) {
|
||||
host.setStrugglingToData(struggling);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.entities.DamselVariant;
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.skins.DamselSkinManager;
|
||||
import com.tiedup.remake.entities.skins.Gender;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/**
|
||||
* Manages the visual appearance of a Damsel NPC.
|
||||
* Responsibilities:
|
||||
* - Variant selection and management
|
||||
* - Skin texture resolution
|
||||
* - Gender
|
||||
* - Slim arms flag
|
||||
* - Custom name
|
||||
*
|
||||
* Phase 1 of EntityDamsel refactoring (8 phases total).
|
||||
*/
|
||||
public class DamselAppearance {
|
||||
|
||||
private final EntityDamsel entity;
|
||||
|
||||
/**
|
||||
* Current skin variant (cached from DATA_VARIANT_ID).
|
||||
* Thread-safe: accessed from render thread.
|
||||
*/
|
||||
@Nullable
|
||||
private volatile DamselVariant variant;
|
||||
|
||||
/**
|
||||
* Default texture for damsel entities.
|
||||
*/
|
||||
private static final ResourceLocation DEFAULT_DAMSEL_TEXTURE =
|
||||
ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
"textures/entity/damsel/dam_mob_1.png"
|
||||
);
|
||||
|
||||
public DamselAppearance(EntityDamsel entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// VARIANT SYSTEM
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the current variant.
|
||||
* Lazy-loads from DATA_VARIANT_ID if not cached.
|
||||
*/
|
||||
@Nullable
|
||||
public DamselVariant getVariant() {
|
||||
// Try to load from synced variant ID first
|
||||
if (this.variant == null && !this.getVariantId().isEmpty()) {
|
||||
this.variant = DamselSkinManager.CORE.getVariant(
|
||||
this.getVariantId()
|
||||
);
|
||||
}
|
||||
// Fallback: compute from UUID if not yet synced (client-side race condition fix)
|
||||
if (this.variant == null) {
|
||||
this.variant = DamselSkinManager.CORE.getVariantForEntity(
|
||||
this.entity.getUUID()
|
||||
);
|
||||
}
|
||||
return this.variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the variant.
|
||||
* Updates both cache and synced data.
|
||||
*/
|
||||
public void setVariant(DamselVariant variant) {
|
||||
this.variant = variant;
|
||||
this.entity.getEntityData().set(
|
||||
EntityDamsel.DATA_VARIANT_ID,
|
||||
variant.id()
|
||||
);
|
||||
this.setSlimArms(variant.hasSlimArms());
|
||||
this.setGender(variant.gender());
|
||||
applyVariantName(variant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the name from variant to this entity.
|
||||
* Numbered variants (dam_mob_*) get random names.
|
||||
* Named variants (Anastasia, Blizz, etc.) use their default name.
|
||||
*/
|
||||
protected void applyVariantName(DamselVariant variant) {
|
||||
if (variant.id().startsWith("dam_mob_")) {
|
||||
this.setNpcName(
|
||||
com.tiedup.remake.util.NameGenerator.getRandomDamselName()
|
||||
);
|
||||
} else {
|
||||
this.setNpcName(variant.defaultName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get variant ID (synced data).
|
||||
*/
|
||||
public String getVariantId() {
|
||||
return this.entity.getEntityData().get(EntityDamsel.DATA_VARIANT_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate variant cache (called when DATA_VARIANT_ID changes).
|
||||
*/
|
||||
public void invalidateVariantCache() {
|
||||
this.variant = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SLIM ARMS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if this damsel uses slim arms model.
|
||||
* Reads from synced entityData - no local skin JSONs needed on client.
|
||||
*/
|
||||
public boolean hasSlimArms() {
|
||||
return this.entity.getEntityData().get(EntityDamsel.DATA_SLIM_ARMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set slim arms flag directly.
|
||||
* Used by subclasses that have their own variant systems.
|
||||
*/
|
||||
protected void setSlimArms(boolean slimArms) {
|
||||
this.entity.getEntityData().set(EntityDamsel.DATA_SLIM_ARMS, slimArms);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// GENDER
|
||||
// ========================================
|
||||
|
||||
public void setGender(Gender gender) {
|
||||
this.entity.getEntityData().set(
|
||||
EntityDamsel.DATA_GENDER,
|
||||
gender.getSerializedName()
|
||||
);
|
||||
}
|
||||
|
||||
public Gender getGender() {
|
||||
return Gender.fromName(
|
||||
this.entity.getEntityData().get(EntityDamsel.DATA_GENDER)
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SKIN TEXTURE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the skin texture for this entity.
|
||||
* Computes texture path from variant ID - no local skin JSONs needed on client.
|
||||
* Implements ISkinnedEntity to eliminate instanceof cascades in renderers.
|
||||
*/
|
||||
public ResourceLocation getSkinTexture() {
|
||||
String variantId = this.getVariantId();
|
||||
if (!variantId.isEmpty()) {
|
||||
return ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup",
|
||||
"textures/entity/damsel/" + variantId + ".png"
|
||||
);
|
||||
}
|
||||
return DEFAULT_DAMSEL_TEXTURE;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NAME SYSTEM
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get NPC's custom name.
|
||||
*/
|
||||
public String getNpcName() {
|
||||
return this.entity.getEntityData().get(EntityDamsel.DATA_DAMSEL_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set NPC's custom name.
|
||||
* Also updates Minecraft's custom name display.
|
||||
*/
|
||||
public void setNpcName(String name) {
|
||||
this.entity.getEntityData().set(EntityDamsel.DATA_DAMSEL_NAME, name);
|
||||
|
||||
// Make the name visible in-game
|
||||
this.entity.setCustomName(Component.literal(name));
|
||||
this.entity.setCustomNameVisible(true);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT SERIALIZATION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Save appearance data to NBT tag.
|
||||
* Writes directly to top-level keys for backward compatibility.
|
||||
*/
|
||||
public void saveToTag(CompoundTag tag) {
|
||||
String variantId = this.entity.getEntityData().get(
|
||||
EntityDamsel.DATA_VARIANT_ID
|
||||
);
|
||||
if (!variantId.isEmpty()) {
|
||||
tag.putString("Variant", variantId);
|
||||
}
|
||||
|
||||
String name = this.getNpcName();
|
||||
if (!name.isEmpty()) {
|
||||
tag.putString("DamselName", name);
|
||||
}
|
||||
|
||||
tag.putBoolean("SlimArms", this.hasSlimArms());
|
||||
tag.putString("Gender", this.getGender().getSerializedName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load appearance data from NBT tag.
|
||||
* Reads from top-level keys for backward compatibility.
|
||||
*/
|
||||
public void loadFromTag(CompoundTag tag) {
|
||||
if (tag.contains("Variant")) {
|
||||
String variantId = tag.getString("Variant");
|
||||
this.entity.getEntityData().set(
|
||||
EntityDamsel.DATA_VARIANT_ID,
|
||||
variantId
|
||||
);
|
||||
this.variant = null; // Invalidate cache
|
||||
}
|
||||
|
||||
if (tag.contains("DamselName")) {
|
||||
this.setNpcName(tag.getString("DamselName"));
|
||||
}
|
||||
|
||||
if (tag.contains("SlimArms")) {
|
||||
this.setSlimArms(tag.getBoolean("SlimArms"));
|
||||
}
|
||||
|
||||
if (tag.contains("Gender")) {
|
||||
String genderName = tag.getString("Gender");
|
||||
this.setGender(Gender.fromName(genderName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.entities.BondageServiceHandler;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.state.IRestrainable;
|
||||
import com.tiedup.remake.state.IRestrainableEntity;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
import com.tiedup.remake.util.RestraintEffectUtils;
|
||||
import com.tiedup.remake.util.tasks.ItemTask;
|
||||
import com.tiedup.remake.util.teleport.Position;
|
||||
import com.tiedup.remake.util.teleport.TeleportHelper;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.damagesource.DamageSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* DamselBondageManager - Facade over NpcEquipmentManager and NpcCaptivityManager.
|
||||
*
|
||||
* <p>H9 refactor: Split from a 1441-line god-component into:</p>
|
||||
* <ul>
|
||||
* <li>{@link NpcEquipmentManager} -- equipment CRUD, coercion, bulk ops (~700L)</li>
|
||||
* <li>{@link NpcCaptivityManager} -- captor, leash, free/capture (~250L)</li>
|
||||
* <li>{@link BondageServiceHandler} -- existing, unchanged</li>
|
||||
* <li>This facade -- delegates everything, owns sale state inline (~250L)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Implements {@link IRestrainable} so all existing callers via
|
||||
* {@code getBondageManager().xxx()} continue to work unchanged.</p>
|
||||
*/
|
||||
public class DamselBondageManager implements IRestrainable {
|
||||
|
||||
// ========================================
|
||||
// FIELDS
|
||||
// ========================================
|
||||
|
||||
private final AbstractTiedUpNpc entity;
|
||||
private final IBondageHost host;
|
||||
private final NpcEquipmentManager equipment;
|
||||
private final NpcCaptivityManager captivity;
|
||||
private final BondageServiceHandler bondageService;
|
||||
|
||||
// Sale fields (inline -- too small for own class)
|
||||
private boolean forSale;
|
||||
private ItemTask salePrice;
|
||||
|
||||
// ========================================
|
||||
// CONSTRUCTOR
|
||||
// ========================================
|
||||
|
||||
public DamselBondageManager(AbstractTiedUpNpc entity, IBondageHost host) {
|
||||
this.entity = entity;
|
||||
this.host = host;
|
||||
this.equipment = new NpcEquipmentManager(entity, host);
|
||||
this.captivity = new NpcCaptivityManager(entity, host, equipment);
|
||||
this.bondageService = new BondageServiceHandler(entity);
|
||||
this.forSale = false;
|
||||
this.salePrice = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SUB-COMPONENT ACCESS
|
||||
// ========================================
|
||||
|
||||
/** Expose equipment sub-component for direct access where needed. */
|
||||
public NpcEquipmentManager getEquipmentManager() {
|
||||
return equipment;
|
||||
}
|
||||
|
||||
/** Expose captivity sub-component for direct access where needed. */
|
||||
public NpcCaptivityManager getCaptivityManager() {
|
||||
return captivity;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IBondageState DELEGATION -> equipment
|
||||
// ========================================
|
||||
|
||||
@Override public boolean isTiedUp() { return equipment.isTiedUp(); }
|
||||
@Override public boolean isGagged() { return equipment.isGagged(); }
|
||||
@Override public boolean isBlindfolded() { return equipment.isBlindfolded(); }
|
||||
@Override public boolean hasEarplugs() { return equipment.hasEarplugs(); }
|
||||
@Override public boolean hasCollar() { return equipment.hasCollar(); }
|
||||
@Override public boolean hasClothes() { return equipment.hasClothes(); }
|
||||
@Override public boolean hasMittens() { return equipment.hasMittens(); }
|
||||
@Override public boolean isBoundAndGagged() { return equipment.isBoundAndGagged(); }
|
||||
@Override public boolean hasGaggingEffect() { return equipment.hasGaggingEffect(); }
|
||||
@Override public boolean hasBlindingEffect() { return equipment.hasBlindingEffect(); }
|
||||
@Override public boolean hasKnives() { return equipment.hasKnives(); }
|
||||
@Override public boolean hasClothesWithSmallArms() { return equipment.hasClothesWithSmallArms(); }
|
||||
@Override public boolean hasLockedCollar() { return equipment.hasLockedCollar(); }
|
||||
@Override public boolean hasNamedCollar() { return equipment.hasNamedCollar(); }
|
||||
|
||||
// V2 region-based equipment access
|
||||
@Override public ItemStack getEquipment(BodyRegionV2 region) { return equipment.getInRegion(region); }
|
||||
|
||||
@Override
|
||||
public void equip(BodyRegionV2 region, ItemStack stack) {
|
||||
switch (region) {
|
||||
case ARMS -> equipment.putBindOn(stack);
|
||||
case MOUTH -> equipment.putGagOn(stack);
|
||||
case EYES -> equipment.putBlindfoldOn(stack);
|
||||
case EARS -> equipment.putEarplugsOn(stack);
|
||||
case NECK -> equipment.putCollarOn(stack);
|
||||
case TORSO -> equipment.putClothesOn(stack);
|
||||
case HANDS -> equipment.putMittensOn(stack);
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
// Equipment putters (local delegates — no longer in interface, but still called by legacy code)
|
||||
public void putBindOn(ItemStack bind) { equipment.putBindOn(bind); }
|
||||
public void putGagOn(ItemStack gag) { equipment.putGagOn(gag); }
|
||||
public void putBlindfoldOn(ItemStack blindfold) { equipment.putBlindfoldOn(blindfold); }
|
||||
public void putEarplugsOn(ItemStack earplugs) { equipment.putEarplugsOn(earplugs); }
|
||||
public void putCollarOn(ItemStack collar) { equipment.putCollarOn(collar); }
|
||||
public void putClothesOn(ItemStack clothes) { equipment.putClothesOn(clothes); }
|
||||
public void putMittensOn(ItemStack mittens) { equipment.putMittensOn(mittens); }
|
||||
|
||||
// Equipment removers (V2 region-based)
|
||||
@Override
|
||||
public ItemStack unequip(BodyRegionV2 region) {
|
||||
return switch (region) {
|
||||
case ARMS -> equipment.takeBindOff();
|
||||
case MOUTH -> equipment.takeGagOff();
|
||||
case EYES -> equipment.takeBlindfoldOff();
|
||||
case EARS -> equipment.takeEarplugsOff();
|
||||
case NECK -> equipment.takeCollarOff(false);
|
||||
case TORSO -> equipment.takeClothesOff();
|
||||
case HANDS -> equipment.takeMittensOff();
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack forceUnequip(BodyRegionV2 region) {
|
||||
// Delegate to NpcEquipmentManager.forceRemoveFromRegion which does the same
|
||||
// work as takeXOff (clear slot, sync, onUnequipped, side effects) but
|
||||
// bypasses ILockable.isLocked() checks. See RISK-001.
|
||||
return equipment.forceRemoveFromRegion(region);
|
||||
}
|
||||
|
||||
// Legacy equipment removers (local delegates for internal use)
|
||||
public ItemStack takeBindOff() { return equipment.takeBindOff(); }
|
||||
public ItemStack takeGagOff() { return equipment.takeGagOff(); }
|
||||
public ItemStack takeBlindfoldOff() { return equipment.takeBlindfoldOff(); }
|
||||
public ItemStack takeEarplugsOff() { return equipment.takeEarplugsOff(); }
|
||||
public ItemStack takeCollarOff() { return equipment.takeCollarOff(); }
|
||||
public ItemStack takeCollarOff(boolean force) { return equipment.takeCollarOff(force); }
|
||||
public ItemStack takeClothesOff() { return equipment.takeClothesOff(); }
|
||||
public ItemStack takeMittensOff() { return equipment.takeMittensOff(); }
|
||||
|
||||
// Equipment replacer (V2 region-based)
|
||||
@Override
|
||||
public ItemStack replaceEquipment(BodyRegionV2 region, ItemStack newStack, boolean force) {
|
||||
return switch (region) {
|
||||
case ARMS -> equipment.replaceBind(newStack, force);
|
||||
case MOUTH -> equipment.replaceGag(newStack, force);
|
||||
case EYES -> equipment.replaceBlindfold(newStack, force);
|
||||
case EARS -> equipment.replaceEarplugs(newStack, force);
|
||||
case NECK -> equipment.replaceCollar(newStack, force);
|
||||
case TORSO -> equipment.replaceClothes(newStack, force);
|
||||
case HANDS -> equipment.replaceMittens(newStack, force);
|
||||
default -> ItemStack.EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
// Bulk operations
|
||||
@Override
|
||||
public void applyBondage(
|
||||
ItemStack bind, ItemStack gag, ItemStack blindfold,
|
||||
ItemStack earplugs, ItemStack collar, ItemStack clothes
|
||||
) {
|
||||
equipment.applyBondage(bind, gag, blindfold, earplugs, collar, clothes);
|
||||
}
|
||||
|
||||
@Override public void dropBondageItems(boolean drop) { equipment.dropBondageItems(drop); }
|
||||
@Override public void dropBondageItems(boolean drop, boolean dropBind) { equipment.dropBondageItems(drop, dropBind); }
|
||||
@Override
|
||||
public void dropBondageItems(
|
||||
boolean drop, boolean dropBind, boolean dropGag,
|
||||
boolean dropBlindfold, boolean dropEarplugs,
|
||||
boolean dropCollar, boolean dropClothes
|
||||
) {
|
||||
equipment.dropBondageItems(drop, dropBind, dropGag, dropBlindfold,
|
||||
dropEarplugs, dropCollar, dropClothes);
|
||||
}
|
||||
|
||||
@Override public void dropClothes() { equipment.dropClothes(); }
|
||||
@Override public int getBondageItemsWhichCanBeRemovedCount() { return equipment.getBondageItemsWhichCanBeRemovedCount(); }
|
||||
@Override public boolean canBeTiedUp() { return equipment.canBeTiedUp(); }
|
||||
|
||||
// Permissions
|
||||
@Override public boolean canTakeOffClothes(Player player) { return equipment.canTakeOffClothes(player); }
|
||||
@Override public boolean canChangeClothes(Player player) { return equipment.canChangeClothes(player); }
|
||||
@Override public boolean canChangeClothes() { return equipment.canChangeClothes(); }
|
||||
|
||||
// ICoercible DELEGATION -> equipment
|
||||
@Override public void tighten(Player tightener) { equipment.tighten(tightener); }
|
||||
@Override public void applyChloroform(int duration) { equipment.applyChloroform(duration); }
|
||||
@Override public void shockKidnapped() { equipment.shockKidnapped(); }
|
||||
@Override public void shockKidnapped(String messageAddon, float damage) { equipment.shockKidnapped(messageAddon, damage); }
|
||||
@Override public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) { equipment.takeBondageItemBy(taker, slotIndex); }
|
||||
|
||||
// ========================================
|
||||
// ICapturable DELEGATION -> captivity
|
||||
// ========================================
|
||||
|
||||
@Override public boolean isCaptive() { return captivity.isCaptive(); }
|
||||
@Override public boolean isEnslavable() { return captivity.isEnslavable(); }
|
||||
@Override public ICaptor getCaptor() { return captivity.getCaptor(); }
|
||||
@Override public boolean getCapturedBy(ICaptor newCaptor) { return captivity.getCapturedBy(newCaptor); }
|
||||
@Override public void free() { captivity.free(); }
|
||||
@Override public void free(boolean transportState) { captivity.free(transportState); }
|
||||
@Override public void transferCaptivityTo(ICaptor newCaptor) { captivity.transferCaptivityTo(newCaptor); }
|
||||
@Override public boolean isTiedToPole() { return captivity.isTiedToPole(); }
|
||||
@Override public boolean tieToClosestPole(int searchRadius) { return captivity.tieToClosestPole(searchRadius); }
|
||||
@Override public boolean canBeKidnappedByEvents() { return captivity.canBeKidnappedByEvents(); }
|
||||
@Override @Nullable public Entity getTransport() { return null; } // NPCs use vanilla leash directly
|
||||
|
||||
// Public methods not on IRestrainable but used externally
|
||||
public void clearCaptor() { captivity.clearCaptor(); }
|
||||
public boolean forceCapturedBy(ICaptor newCaptor) { return captivity.forceCapturedBy(newCaptor); }
|
||||
public void restoreCaptorFromUUID() { captivity.restoreCaptorFromUUID(); }
|
||||
|
||||
// Collar owner check (public, used by AbstractTiedUpNpc)
|
||||
public boolean isCollarOwner(Player player) { return equipment.isCollarOwner(player); }
|
||||
|
||||
// ========================================
|
||||
// ISaleable -- INLINE (4 methods, 2 fields)
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public boolean isForSell() {
|
||||
return forSale && salePrice != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ItemTask getSalePrice() {
|
||||
return salePrice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putForSale(ItemTask price) {
|
||||
if (price == null) return;
|
||||
|
||||
forSale = true;
|
||||
salePrice = price;
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} put for sale at {}",
|
||||
host.getNpcName(),
|
||||
price.toDisplayString()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelSale() {
|
||||
if (!forSale) return;
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} sale cancelled",
|
||||
host.getNpcName()
|
||||
);
|
||||
|
||||
forSale = false;
|
||||
salePrice = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// IRestrainableEntity -- IDENTITY (stays in facade)
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public LivingEntity asLivingEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getKidnappedUniqueId() {
|
||||
return host.getUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKidnappedName() {
|
||||
return host.getNpcName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNameFromCollar() {
|
||||
ItemStack collar = getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.isEmpty()) {
|
||||
return host.getNpcName();
|
||||
}
|
||||
|
||||
if (collar.hasCustomHoverName()) {
|
||||
return collar.getHoverName().getString();
|
||||
}
|
||||
|
||||
return host.getNpcName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kidnappedDropItem(ItemStack stack) {
|
||||
host.dropItemStack(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleportToPosition(Position position) {
|
||||
if (position == null || entity.level().isClientSide) return;
|
||||
TeleportHelper.teleportEntity(entity, position);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CROSS-CUTTING METHODS (stay in facade)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Untie: crosses both equipment and captivity domains.
|
||||
* Drops items (if requested), clears equipment, removes speed reduction.
|
||||
*/
|
||||
@Override
|
||||
public void untie(boolean drop) {
|
||||
if (drop) {
|
||||
equipment.dropBondageItems(true);
|
||||
} else {
|
||||
// Clear all V2 regions at once, then sync ONCE (batch)
|
||||
equipment.clearAllAndSync();
|
||||
}
|
||||
|
||||
// Remove speed reduction
|
||||
RestraintEffectUtils.removeBindSpeedReduction(entity);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} freed from restraints",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Death handler: crosses captivity (free) and equipment (unlock).
|
||||
*/
|
||||
@Override
|
||||
public boolean onDeathKidnapped(Level world) {
|
||||
if (world.isClientSide) return false;
|
||||
|
||||
// Check if we have a collar with cell configured
|
||||
ItemStack collar = getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.isEmpty()) return false;
|
||||
|
||||
if (!(collar.getItem() instanceof ItemCollar itemCollar)) return false;
|
||||
|
||||
UUID cellId = itemCollar.getCellId(collar);
|
||||
if (cellId == null) return false;
|
||||
|
||||
// Get cell position from registry
|
||||
if (
|
||||
!(entity.level() instanceof
|
||||
net.minecraft.server.level.ServerLevel serverLevel)
|
||||
) return false;
|
||||
com.tiedup.remake.cells.CellDataV2 cell =
|
||||
com.tiedup.remake.cells.CellRegistryV2.get(serverLevel).getCell(
|
||||
cellId
|
||||
);
|
||||
if (cell == null) return false;
|
||||
|
||||
Position cellPosition = new Position(
|
||||
cell.getSpawnPoint().above(),
|
||||
serverLevel.dimension()
|
||||
);
|
||||
|
||||
// We have a cell -- respawn there instead of dying
|
||||
|
||||
// 1. Free from captivity if captured
|
||||
if (isCaptive()) {
|
||||
captivity.free(false);
|
||||
}
|
||||
|
||||
// 2. Unlock all locked items
|
||||
equipment.unlockAllItems();
|
||||
|
||||
// 3. Heal to full
|
||||
host.setHealth(entity.getMaxHealth());
|
||||
|
||||
// 4. Teleport to cell
|
||||
teleportToPosition(cellPosition);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} respawned at cell instead of dying",
|
||||
entity.getName().getString()
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BONDAGE SERVICE DELEGATION
|
||||
// ========================================
|
||||
|
||||
public boolean isBondageServiceEnabled() {
|
||||
return bondageService.isEnabled();
|
||||
}
|
||||
|
||||
public String getBondageServiceMessage() {
|
||||
return bondageService.getMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle damage with bondage service logic.
|
||||
* Call this from EntityDamsel.hurt() if service is enabled.
|
||||
*
|
||||
* @param source damage source
|
||||
* @param amount damage amount
|
||||
* @return true if damage was handled (cancel default), false otherwise
|
||||
*/
|
||||
public boolean handleDamageWithService(DamageSource source, float amount) {
|
||||
if (!isBondageServiceEnabled()) return false;
|
||||
|
||||
// Only process player attacks
|
||||
if (!(source.getEntity() instanceof Player player)) return false;
|
||||
|
||||
// Cancel actual damage (service NPCs don't take damage from hits)
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PERSISTENCE ORCHESTRATION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Save bondage state to NBT.
|
||||
* Delegates to sub-components, handles sale state inline.
|
||||
*/
|
||||
public void saveToTag(CompoundTag tag) {
|
||||
equipment.saveEquipmentToTag(tag);
|
||||
captivity.saveCaptivityToTag(tag);
|
||||
|
||||
// Sale state (inline)
|
||||
tag.putBoolean("ForSale", forSale);
|
||||
if (salePrice != null) {
|
||||
tag.put("SalePrice", salePrice.save());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load bondage state from NBT.
|
||||
* Delegates to sub-components, handles sale state inline.
|
||||
*/
|
||||
public void loadFromTag(CompoundTag tag) {
|
||||
equipment.loadEquipmentFromTag(tag);
|
||||
captivity.loadCaptivityFromTag(tag);
|
||||
|
||||
// Sale state (inline)
|
||||
forSale = tag.getBoolean("ForSale");
|
||||
if (tag.contains("SalePrice")) {
|
||||
salePrice = ItemTask.load(tag.getCompound("SalePrice"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
|
||||
/**
|
||||
* Orchestrates NBT serialization/deserialization for all EntityDamsel components.
|
||||
* Provides a single entry point for save/load operations.
|
||||
*
|
||||
* Phase 8: Final refactoring phase - NBT orchestration.
|
||||
*/
|
||||
public class DamselDataSerializer {
|
||||
|
||||
private final EntityDamsel entity;
|
||||
|
||||
public DamselDataSerializer(EntityDamsel entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save all component data to NBT.
|
||||
* Call this from EntityDamsel.addAdditionalSaveData().
|
||||
*
|
||||
* @param tag NBT compound tag to write to
|
||||
*/
|
||||
public void save(CompoundTag tag) {
|
||||
// Phase 1: Appearance (variant, gender, name, slim arms)
|
||||
entity.getAppearance().saveToTag(tag);
|
||||
|
||||
// Bondage + Inventory are now saved by AbstractTiedUpNpc.addAdditionalSaveData()
|
||||
|
||||
// Phase 6: Personality (traits, needs, relationships, commands)
|
||||
entity.getPersonalitySystem().saveToTag(tag);
|
||||
|
||||
// Reward tracker (savior, rewards)
|
||||
entity.getRewardTracker().save(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all component data from NBT.
|
||||
* Call this from EntityDamsel.readAdditionalSaveData().
|
||||
*
|
||||
* @param tag NBT compound tag to read from
|
||||
*/
|
||||
public void load(CompoundTag tag) {
|
||||
// Phase 1: Appearance (variant, gender, name, slim arms)
|
||||
entity.getAppearance().loadFromTag(tag);
|
||||
|
||||
// Bondage + Inventory are now loaded by AbstractTiedUpNpc.readAdditionalSaveData()
|
||||
|
||||
// Phase 6: Personality (traits, needs, relationships, commands)
|
||||
entity.getPersonalitySystem().loadFromTag(tag);
|
||||
|
||||
// Reward tracker (savior, rewards)
|
||||
entity.getRewardTracker().load(tag);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
|
||||
/**
|
||||
* Manages all dialogue-related systems for EntityDamsel:
|
||||
* - Direct dialogue (talkTo, actionTo)
|
||||
* - Radius-based dialogue (talkToPlayersInRadius, actionToPlayersInRadius)
|
||||
* - Cooldown management for dialogue categories
|
||||
*
|
||||
* Phase 4: Extracted from EntityDamsel.java (~180 lines, 9 methods)
|
||||
*/
|
||||
public class DamselDialogueHandler {
|
||||
|
||||
private final IDialogueHost host;
|
||||
|
||||
/** Track last time each dialogue category was used (for cooldowns) */
|
||||
private final Map<DialogueCategory, Long> dialogueCooldowns =
|
||||
new HashMap<>();
|
||||
|
||||
public DamselDialogueHandler(IDialogueHost host) {
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DIRECT DIALOGUE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Send a direct message to a player.
|
||||
* @param player Target player
|
||||
* @param message Message to send
|
||||
*/
|
||||
public void talkTo(Player player, String message) {
|
||||
EntityDialogueManager.talkTo(host.getEntity(), player, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a dialogue message to a player using a category.
|
||||
* @param player Target player
|
||||
* @param category Dialogue category (selects random message)
|
||||
*/
|
||||
public void talkTo(Player player, DialogueCategory category) {
|
||||
EntityDialogueManager.talkTo(host.getEntity(), player, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an action message to a player (e.g. "*struggles*").
|
||||
* @param player Target player
|
||||
* @param action Action description
|
||||
*/
|
||||
public void actionTo(Player player, String action) {
|
||||
EntityDialogueManager.actionTo(host.getEntity(), player, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an action message to a player using a category.
|
||||
* @param player Target player
|
||||
* @param category Action category
|
||||
*/
|
||||
public void actionTo(Player player, DialogueCategory category) {
|
||||
EntityDialogueManager.actionTo(host.getEntity(), player, category);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// RADIUS-BASED DIALOGUE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Send a message to all players within radius.
|
||||
* @param message Message to send
|
||||
* @param radius Radius in blocks
|
||||
*/
|
||||
public void talkToPlayersInRadius(String message, int radius) {
|
||||
EntityDialogueManager.talkToNearby(host.getEntity(), message, radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a dialogue message to all players within radius using a category.
|
||||
* @param category Dialogue category
|
||||
* @param radius Radius in blocks
|
||||
*/
|
||||
public void talkToPlayersInRadius(DialogueCategory category, int radius) {
|
||||
EntityDialogueManager.talkToNearby(host.getEntity(), category, radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a dialogue message to nearby players with cooldown management.
|
||||
* Only sends if cooldown has expired for this category.
|
||||
*
|
||||
* @param category Dialogue category
|
||||
* @param radius Radius in blocks
|
||||
* @return true if message was sent, false if on cooldown
|
||||
*/
|
||||
public boolean talkToPlayersInRadiusWithCooldown(
|
||||
DialogueCategory category,
|
||||
int radius
|
||||
) {
|
||||
long currentTick = host.getGameTime();
|
||||
Long lastUsed = dialogueCooldowns.get(category);
|
||||
|
||||
if (
|
||||
lastUsed != null &&
|
||||
(currentTick - lastUsed) < ModConfig.SERVER.dialogueCooldown.get()
|
||||
) {
|
||||
return false; // Still on cooldown
|
||||
}
|
||||
|
||||
dialogueCooldowns.put(category, currentTick);
|
||||
EntityDialogueManager.talkToNearby(host.getEntity(), category, radius);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an action message to all players within radius.
|
||||
* @param action Action description
|
||||
* @param radius Radius in blocks
|
||||
*/
|
||||
public void actionToPlayersInRadius(String action, int radius) {
|
||||
EntityDialogueManager.actionToNearby(host.getEntity(), action, radius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an action message to all players within radius using a category.
|
||||
* @param category Action category
|
||||
* @param radius Radius in blocks
|
||||
*/
|
||||
public void actionToPlayersInRadius(DialogueCategory category, int radius) {
|
||||
EntityDialogueManager.actionToNearby(
|
||||
host.getEntity(),
|
||||
category,
|
||||
radius
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.personality.NpcNeeds;
|
||||
import com.tiedup.remake.personality.PersonalityState;
|
||||
import com.tiedup.remake.util.FoodEffects;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.entity.EquipmentSlot;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.food.FoodProperties;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
||||
|
||||
/**
|
||||
* Manages inventory, equipment, and feeding for a Damsel NPC.
|
||||
* Responsibilities:
|
||||
* - NPC inventory (expandable 9/18/27 slots)
|
||||
* - Armor inventory (4 slots)
|
||||
* - Equipment slots (main hand)
|
||||
* - Feeding system (auto-eat, player feeding)
|
||||
* - MenuProvider (GUI access)
|
||||
*
|
||||
* Phase 2 of EntityDamsel refactoring (8 phases total).
|
||||
*/
|
||||
public class DamselInventoryManager {
|
||||
|
||||
private final AbstractTiedUpNpc entity;
|
||||
|
||||
/**
|
||||
* NPC inventory for carrying items.
|
||||
* Default 9 slots, expandable to 18 or 27 via upgrade.
|
||||
*/
|
||||
private NonNullList<ItemStack> npcInventory;
|
||||
|
||||
/**
|
||||
* Current inventory size (9 default, 18 or 27 with upgrades).
|
||||
*/
|
||||
private int npcInventorySize;
|
||||
|
||||
/**
|
||||
* Armor inventory (4 slots: HEAD, CHEST, LEGS, FEET).
|
||||
* Index 0 = HEAD, 1 = CHEST, 2 = LEGS, 3 = FEET.
|
||||
*/
|
||||
private final NonNullList<ItemStack> armorInventory;
|
||||
|
||||
public DamselInventoryManager(AbstractTiedUpNpc entity) {
|
||||
this.entity = entity;
|
||||
this.npcInventory = NonNullList.withSize(9, ItemStack.EMPTY);
|
||||
this.npcInventorySize = 9;
|
||||
this.armorInventory = NonNullList.withSize(4, ItemStack.EMPTY);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// EQUIPMENT SLOTS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get item in equipment slot.
|
||||
* Implements Mob.getItemBySlot().
|
||||
*/
|
||||
public ItemStack getItemBySlot(EquipmentSlot slot) {
|
||||
return switch (slot) {
|
||||
case HEAD -> armorInventory.get(0);
|
||||
case CHEST -> armorInventory.get(1);
|
||||
case LEGS -> armorInventory.get(2);
|
||||
case FEET -> armorInventory.get(3);
|
||||
case MAINHAND -> this.entity.getEntityData().get(
|
||||
AbstractTiedUpNpc.DATA_MAIN_HAND
|
||||
);
|
||||
case OFFHAND -> ItemStack.EMPTY;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set item in equipment slot.
|
||||
* Implements Mob.setItemSlot().
|
||||
* Note: Entity should call verifyEquippedItem before delegating to this method.
|
||||
*/
|
||||
public void setItemSlot(EquipmentSlot slot, ItemStack stack) {
|
||||
switch (slot) {
|
||||
case HEAD -> armorInventory.set(0, stack);
|
||||
case CHEST -> armorInventory.set(1, stack);
|
||||
case LEGS -> armorInventory.set(2, stack);
|
||||
case FEET -> armorInventory.set(3, stack);
|
||||
case MAINHAND -> this.entity.getEntityData().set(
|
||||
AbstractTiedUpNpc.DATA_MAIN_HAND,
|
||||
stack
|
||||
);
|
||||
case OFFHAND -> {
|
||||
} // Not supported
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get armor slots iterable.
|
||||
* Implements Mob.getArmorSlots().
|
||||
*/
|
||||
public Iterable<ItemStack> getArmorSlots() {
|
||||
return this.armorInventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main hand item.
|
||||
* Implements Mob.getMainHandItem().
|
||||
*/
|
||||
public ItemStack getMainHandItem() {
|
||||
return this.entity.getEntityData().get(AbstractTiedUpNpc.DATA_MAIN_HAND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the main hand item.
|
||||
*
|
||||
* @param stack Item to hold
|
||||
*/
|
||||
public void setMainHandItem(ItemStack stack) {
|
||||
this.entity.getEntityData().set(AbstractTiedUpNpc.DATA_MAIN_HAND, stack);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NPC INVENTORY
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the NPC's carry inventory.
|
||||
*
|
||||
* @return NonNullList of ItemStacks
|
||||
*/
|
||||
public NonNullList<ItemStack> getNpcInventory() {
|
||||
return this.npcInventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current NPC inventory size.
|
||||
*
|
||||
* @return Inventory size (9, 18, or 27)
|
||||
*/
|
||||
public int getNpcInventorySize() {
|
||||
return this.npcInventorySize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set/upgrade the NPC inventory size.
|
||||
*
|
||||
* @param newSize New size (9, 18, or 27)
|
||||
*/
|
||||
public void setNpcInventorySize(int newSize) {
|
||||
if (newSize != 9 && newSize != 18 && newSize != 27) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[DamselInventoryManager] Invalid inventory size: {}. Must be 9, 18, or 27.",
|
||||
newSize
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSize <= this.npcInventorySize) return; // Can't downgrade
|
||||
|
||||
// Create new inventory preserving existing items
|
||||
NonNullList<ItemStack> newInventory = NonNullList.withSize(
|
||||
newSize,
|
||||
ItemStack.EMPTY
|
||||
);
|
||||
for (int i = 0; i < this.npcInventory.size(); i++) {
|
||||
newInventory.set(i, this.npcInventory.get(i));
|
||||
}
|
||||
this.npcInventory = newInventory;
|
||||
this.npcInventorySize = newSize;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// FEEDING SYSTEM
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if the NPC has any edible item in their inventory.
|
||||
*
|
||||
* @return true if food is available
|
||||
*/
|
||||
public boolean hasEdibleInInventory() {
|
||||
for (int i = 0; i < this.npcInventorySize; i++) {
|
||||
ItemStack stack = this.npcInventory.get(i);
|
||||
if (
|
||||
!stack.isEmpty() && stack.getItem().getFoodProperties() != null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to eat food from inventory to satisfy hunger.
|
||||
* Used by job systems and AI goals.
|
||||
*
|
||||
* @param personalityState Personality state for needs access
|
||||
* @return true if food was consumed
|
||||
*/
|
||||
public boolean tryEatFromInventory(PersonalityState personalityState) {
|
||||
if (personalityState == null) return false;
|
||||
|
||||
NpcNeeds needs = personalityState.getNeeds();
|
||||
if (needs.getHunger() >= 80.0f) return false; // Not hungry enough
|
||||
|
||||
// Find best food item
|
||||
int bestSlot = -1;
|
||||
int bestNutrition = 0;
|
||||
|
||||
for (int i = 0; i < this.npcInventorySize; i++) {
|
||||
ItemStack stack = this.npcInventory.get(i);
|
||||
if (stack.isEmpty()) continue;
|
||||
|
||||
FoodProperties food = stack.getItem().getFoodProperties();
|
||||
if (food != null && food.getNutrition() > bestNutrition) {
|
||||
bestSlot = i;
|
||||
bestNutrition = food.getNutrition();
|
||||
}
|
||||
}
|
||||
|
||||
if (bestSlot >= 0) {
|
||||
ItemStack foodStack = this.npcInventory.get(bestSlot);
|
||||
FoodProperties food = foodStack.getItem().getFoodProperties();
|
||||
|
||||
if (food != null) {
|
||||
// Consume food
|
||||
foodStack.shrink(1);
|
||||
needs.feed(food.getNutrition());
|
||||
personalityState.modifyMood(2);
|
||||
|
||||
// Play eating sound
|
||||
this.entity.level().playSound(
|
||||
null,
|
||||
this.entity.blockPosition(),
|
||||
SoundEvents.GENERIC_EAT,
|
||||
SoundSource.NEUTRAL,
|
||||
0.5f,
|
||||
1.0f
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to eat food from a container (chest) when inventory is empty.
|
||||
* Used by job systems for autonomous feeding.
|
||||
*
|
||||
* @param chestPos Position of the chest to eat from
|
||||
* @param personalityState Personality state for needs access
|
||||
* @return true if food was consumed
|
||||
*/
|
||||
public boolean tryEatFromChest(
|
||||
BlockPos chestPos,
|
||||
PersonalityState personalityState
|
||||
) {
|
||||
if (personalityState == null || chestPos == null) return false;
|
||||
|
||||
NpcNeeds needs = personalityState.getNeeds();
|
||||
if (needs.getHunger() >= 80.0f) return false;
|
||||
|
||||
// Get chest container
|
||||
if (
|
||||
!(this.entity.level().getBlockEntity(chestPos) instanceof
|
||||
ChestBlockEntity chest)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Container container = ChestBlock.getContainer(
|
||||
(ChestBlock) chest.getBlockState().getBlock(),
|
||||
chest.getBlockState(),
|
||||
this.entity.level(),
|
||||
chestPos,
|
||||
false
|
||||
);
|
||||
|
||||
if (container == null) return false;
|
||||
|
||||
// Find food in chest
|
||||
for (int i = 0; i < container.getContainerSize(); i++) {
|
||||
ItemStack stack = container.getItem(i);
|
||||
if (stack.isEmpty()) continue;
|
||||
|
||||
FoodProperties food = stack.getItem().getFoodProperties();
|
||||
if (food != null) {
|
||||
// Consume food from chest
|
||||
stack.shrink(1);
|
||||
needs.feed(food.getNutrition());
|
||||
personalityState.modifyMood(2);
|
||||
|
||||
// Play eating sound
|
||||
this.entity.level().playSound(
|
||||
null,
|
||||
this.entity.blockPosition(),
|
||||
SoundEvents.GENERIC_EAT,
|
||||
SoundSource.NEUTRAL,
|
||||
0.5f,
|
||||
1.0f
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed the NPC directly with a food item.
|
||||
* Effects: restore hunger, boost mood, heal HP, increase affinity.
|
||||
*
|
||||
* @param player The player feeding
|
||||
* @param foodStack The food item (will be consumed)
|
||||
* @param personalityState Personality state for needs and relationship access
|
||||
* @return true if feeding was successful
|
||||
*/
|
||||
public boolean feedByPlayer(
|
||||
Player player,
|
||||
ItemStack foodStack,
|
||||
PersonalityState personalityState
|
||||
) {
|
||||
if (foodStack.isEmpty()) return false;
|
||||
if (this.entity.level().isClientSide) return false;
|
||||
|
||||
FoodEffects.FeedResult result = FoodEffects.calculateFeedEffects(
|
||||
foodStack
|
||||
);
|
||||
if (result == null) return false;
|
||||
if (personalityState == null) return false;
|
||||
|
||||
// Restore hunger
|
||||
personalityState.getNeeds().feed(result.hunger());
|
||||
|
||||
// Boost mood
|
||||
personalityState.modifyMood(result.mood());
|
||||
|
||||
// Heal HP
|
||||
this.entity.heal(result.heal());
|
||||
|
||||
// Consume the food item
|
||||
foodStack.shrink(1);
|
||||
|
||||
// Play eating sound
|
||||
this.entity.playSound(SoundEvents.GENERIC_EAT, 0.5f, 1.0f);
|
||||
|
||||
// Spawn heart particles
|
||||
if (this.entity.level() instanceof ServerLevel serverLevel) {
|
||||
serverLevel.sendParticles(
|
||||
ParticleTypes.HEART,
|
||||
this.entity.getX(),
|
||||
this.entity.getY() + this.entity.getBbHeight(),
|
||||
this.entity.getZ(),
|
||||
2,
|
||||
0.3,
|
||||
0.2,
|
||||
0.3,
|
||||
0.0
|
||||
);
|
||||
}
|
||||
|
||||
// Trigger dialogue based on hunger state
|
||||
String dialogueKey = personalityState.getNeeds().isStarving()
|
||||
? "action.feed.starving"
|
||||
: "action.feed";
|
||||
// Cast safe: feedByPlayer is only called from EntityDamsel which IS an EntityDamsel
|
||||
if (this.entity instanceof com.tiedup.remake.entities.EntityDamsel damsel) {
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.talkByDialogueId(
|
||||
damsel,
|
||||
player,
|
||||
dialogueKey
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// MENU PROVIDER
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Create inventory menu for player access.
|
||||
* Implements MenuProvider.createMenu().
|
||||
*
|
||||
* @param containerId Container ID
|
||||
* @param playerInventory Player's inventory
|
||||
* @param player The player opening the container
|
||||
* @return NpcInventoryMenu instance
|
||||
*/
|
||||
public AbstractContainerMenu createMenu(
|
||||
int containerId,
|
||||
Inventory playerInventory,
|
||||
Player player
|
||||
) {
|
||||
// Cast safe: createMenu is only called from EntityDamsel (MenuProvider)
|
||||
com.tiedup.remake.entities.EntityDamsel damsel =
|
||||
(com.tiedup.remake.entities.EntityDamsel) this.entity;
|
||||
return new com.tiedup.remake.entities.NpcInventoryMenu(
|
||||
containerId,
|
||||
playerInventory,
|
||||
damsel
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT SERIALIZATION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Save inventory data to NBT tag.
|
||||
* Writes directly to top-level keys for backward compatibility.
|
||||
*/
|
||||
public void saveToTag(CompoundTag tag) {
|
||||
// NPC Inventory
|
||||
tag.putInt("NpcInventorySize", this.npcInventorySize);
|
||||
ListTag inventoryTag = new ListTag();
|
||||
for (int i = 0; i < this.npcInventory.size(); i++) {
|
||||
ItemStack stack = this.npcInventory.get(i);
|
||||
if (!stack.isEmpty()) {
|
||||
CompoundTag slotTag = new CompoundTag();
|
||||
slotTag.putByte("Slot", (byte) i);
|
||||
stack.save(slotTag);
|
||||
inventoryTag.add(slotTag);
|
||||
}
|
||||
}
|
||||
tag.put("NpcInventory", inventoryTag);
|
||||
|
||||
// Equipment (Armor + Main Hand)
|
||||
ListTag armorTag = new ListTag();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ItemStack stack = this.armorInventory.get(i);
|
||||
if (!stack.isEmpty()) {
|
||||
CompoundTag slotTag = new CompoundTag();
|
||||
slotTag.putByte("Slot", (byte) i);
|
||||
stack.save(slotTag);
|
||||
armorTag.add(slotTag);
|
||||
}
|
||||
}
|
||||
tag.put("ArmorInventory", armorTag);
|
||||
if (!this.getMainHandItem().isEmpty()) {
|
||||
tag.put(
|
||||
"MainHandItem",
|
||||
this.getMainHandItem().save(new CompoundTag())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load inventory data from NBT tag.
|
||||
* Reads from top-level keys for backward compatibility.
|
||||
*/
|
||||
public void loadFromTag(CompoundTag tag) {
|
||||
// Restore NPC Inventory
|
||||
if (tag.contains("NpcInventorySize")) {
|
||||
this.npcInventorySize = tag.getInt("NpcInventorySize");
|
||||
this.npcInventory = NonNullList.withSize(
|
||||
this.npcInventorySize,
|
||||
ItemStack.EMPTY
|
||||
);
|
||||
}
|
||||
if (tag.contains("NpcInventory")) {
|
||||
ListTag inventoryTag = tag.getList("NpcInventory", 10);
|
||||
for (int i = 0; i < inventoryTag.size(); i++) {
|
||||
CompoundTag slotTag = inventoryTag.getCompound(i);
|
||||
int slot = slotTag.getByte("Slot") & 255;
|
||||
if (slot < this.npcInventory.size()) {
|
||||
this.npcInventory.set(slot, ItemStack.of(slotTag));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore Equipment (Armor + Main Hand)
|
||||
if (tag.contains("ArmorInventory")) {
|
||||
ListTag armorTag = tag.getList("ArmorInventory", 10);
|
||||
for (int i = 0; i < armorTag.size(); i++) {
|
||||
CompoundTag slotTag = armorTag.getCompound(i);
|
||||
int slot = slotTag.getByte("Slot") & 255;
|
||||
if (slot < 4) {
|
||||
this.armorInventory.set(slot, ItemStack.of(slotTag));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tag.contains("MainHandItem")) {
|
||||
this.setMainHandItem(ItemStack.of(tag.getCompound("MainHandItem")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,673 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.personality.*;
|
||||
import java.util.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
|
||||
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Manages all personality-related systems for EntityDamsel:
|
||||
* - PersonalityState (personality type, training, commands, needs, relationships)
|
||||
* - Dialogue systems (idle, approach, environment, needs)
|
||||
* - Rest/fatigue modifiers
|
||||
* - Command execution
|
||||
*
|
||||
* Phase 6: Extracted from EntityDamsel.java (~700 lines, 22 methods)
|
||||
*/
|
||||
public class DamselPersonalitySystem {
|
||||
|
||||
private final com.tiedup.remake.entities.EntityDamsel entity;
|
||||
private final IPersonalityTickContext context;
|
||||
|
||||
// ========================================
|
||||
// DIALOGUE SYSTEM CONSTANTS
|
||||
// ========================================
|
||||
|
||||
/** Minimum cooldown between idle dialogues (4 minutes). */
|
||||
private static final int IDLE_DIALOGUE_COOLDOWN_MIN = 4800;
|
||||
|
||||
/** Maximum cooldown between idle dialogues (10 minutes). */
|
||||
private static final int IDLE_DIALOGUE_COOLDOWN_MAX = 12000;
|
||||
|
||||
/** Cooldown per player for approach detection (2 minutes). */
|
||||
private static final int APPROACH_COOLDOWN = 2400;
|
||||
|
||||
/** Cooldown for environmental dialogue (6 minutes). */
|
||||
private static final int ENVIRONMENT_DIALOGUE_COOLDOWN = 7200;
|
||||
|
||||
/** Approach detection radius (blocks). */
|
||||
private static final double APPROACH_RADIUS = 5.0;
|
||||
|
||||
// ========================================
|
||||
// REST SYSTEM CONSTANTS
|
||||
// ========================================
|
||||
|
||||
/** UUID for rest-based speed modifier. */
|
||||
private static final UUID REST_SPEED_MODIFIER_UUID = UUID.fromString(
|
||||
"8f4c8c9e-0e5f-5d8b-9f60-2b3c4d5e6f7a"
|
||||
);
|
||||
|
||||
/** UUID for rest-based damage modifier. */
|
||||
private static final UUID REST_DAMAGE_MODIFIER_UUID = UUID.fromString(
|
||||
"9a5d9d0f-1f6a-6e9c-0a70-3c4d5e6f7a8b"
|
||||
);
|
||||
|
||||
/** Ticks between rest modifier updates (1 second). */
|
||||
private static final int REST_MODIFIER_UPDATE_INTERVAL = 20;
|
||||
|
||||
// ========================================
|
||||
// PERSONALITY STATE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Complete personality state including:
|
||||
* - Personality type (TIMID, FIERCE, etc.)
|
||||
* - Training level (WILD → DEVOTED)
|
||||
* - Secondary traits (TRAINED, DEVOTED, SUBJUGATED, etc.)
|
||||
* - Needs (hunger, comfort, rest, dignity)
|
||||
* - Relationships (per-player affinity, trust, fear, respect)
|
||||
* - Active command (FOLLOW, STAY, PATROL, etc.)
|
||||
*
|
||||
* Lazy-initialized on first access (server-side only).
|
||||
*/
|
||||
@Nullable
|
||||
private PersonalityState personalityState;
|
||||
|
||||
// ========================================
|
||||
// DIALOGUE COOLDOWNS
|
||||
// ========================================
|
||||
|
||||
/** Cooldown for idle dialogue (in ticks). */
|
||||
private int idleDialogueCooldown = 0;
|
||||
|
||||
/** Tracks cooldown for player approach reactions. */
|
||||
private final Map<UUID, Integer> approachCooldowns = new HashMap<>();
|
||||
|
||||
/** Tracks which players were nearby last tick. */
|
||||
private final Set<UUID> playersNearbyLastTick = new HashSet<>();
|
||||
|
||||
/** Throttle for approach detection - only scan every 10 ticks (0.5 sec). */
|
||||
private int approachCheckCooldown = 0;
|
||||
|
||||
/** Cooldown for environment dialogue. */
|
||||
private int environmentDialogueCooldown = 0;
|
||||
|
||||
/** Track last time each dialogue category was used */
|
||||
private final Map<
|
||||
EntityDialogueManager.DialogueCategory,
|
||||
Long
|
||||
> dialogueCooldowns = new HashMap<>();
|
||||
|
||||
// ========================================
|
||||
// REST SYSTEM MODIFIERS
|
||||
// ========================================
|
||||
|
||||
/** Last applied speed modifier from rest system */
|
||||
private float lastRestSpeedMod = 0.0f;
|
||||
|
||||
/** Last applied damage modifier from rest system */
|
||||
private float lastRestDamageMod = 0.0f;
|
||||
|
||||
// ========================================
|
||||
// ANTI-FLEE SYSTEM
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* World time when last whipped.
|
||||
* Used to prevent fleeing for a short duration after discipline.
|
||||
*/
|
||||
private long lastWhipTime = 0;
|
||||
|
||||
// ========================================
|
||||
// CONSTRUCTOR
|
||||
// ========================================
|
||||
|
||||
public DamselPersonalitySystem(
|
||||
com.tiedup.remake.entities.EntityDamsel entity,
|
||||
IPersonalityTickContext context
|
||||
) {
|
||||
this.entity = entity;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INITIALIZATION & SYNC
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Initialize personality state with random generation.
|
||||
* Called on first spawn or lazy init.
|
||||
*/
|
||||
public void initializePersonality() {
|
||||
// Generate random personality for Damsel
|
||||
this.personalityState = PersonalityState.generateForDamsel(
|
||||
entity.getUUID()
|
||||
);
|
||||
|
||||
// Sync to client
|
||||
syncPersonalityData();
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} initialized with personality: {}",
|
||||
context.getAppearance().getNpcName(),
|
||||
this.personalityState.getPersonality().name()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync personality data to clients via EntityDataAccessors.
|
||||
* Called after personality changes.
|
||||
*/
|
||||
public void syncPersonalityData() {
|
||||
if (this.personalityState != null) {
|
||||
entity
|
||||
.getEntityData()
|
||||
.set(
|
||||
com.tiedup.remake.entities.EntityDamsel.DATA_PERSONALITY_TYPE,
|
||||
this.personalityState.getPersonality().name()
|
||||
);
|
||||
entity
|
||||
.getEntityData()
|
||||
.set(
|
||||
com.tiedup.remake.entities.EntityDamsel.DATA_ACTIVE_COMMAND,
|
||||
this.personalityState.getActiveCommand().name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// TICK LOGIC
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Main personality tick - delegates to PersonalityState and handles side effects.
|
||||
* Call this from EntityDamsel.aiStep().
|
||||
*/
|
||||
public void tickPersonality() {
|
||||
if (this.personalityState == null) return;
|
||||
|
||||
// Get current master UUID from collar (if any)
|
||||
UUID masterUUID = null;
|
||||
if (context.hasCollar()) {
|
||||
ItemStack collar = context.getEquipment(BodyRegionV2.NECK);
|
||||
if (collar.getItem() instanceof ItemCollar collarItem) {
|
||||
List<UUID> owners = collarItem.getOwners(collar);
|
||||
if (!owners.isEmpty()) {
|
||||
masterUUID = owners.get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's night (for rest decay)
|
||||
boolean isNight = !context.level().isDay();
|
||||
|
||||
// Delegate tick to PersonalityState (handles needs, mood, relationships, leash effects)
|
||||
var transitions = this.personalityState.tick(
|
||||
entity,
|
||||
isNight,
|
||||
masterUUID,
|
||||
context.getBondageManager().isCaptive()
|
||||
);
|
||||
|
||||
// Hunger is a player→NPC feature only; keep hunger full for unowned NPCs
|
||||
// (prevents camp kidnappers/traders/maids from starving and complaining)
|
||||
if (masterUUID == null) {
|
||||
this.personalityState.getNeeds().setHunger(NpcNeeds.MAX_VALUE);
|
||||
}
|
||||
|
||||
// Trigger dialogues for need transitions (if not gagged)
|
||||
if (transitions.hasAny() && !context.getBondageManager().isGagged()) {
|
||||
triggerNeedDialogue(transitions);
|
||||
}
|
||||
|
||||
// Update rest-based attribute modifiers (every 20 ticks)
|
||||
if (context.getTickCount() % 20 == 0) {
|
||||
updateRestModifiers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger dialogue when a need crosses a threshold.
|
||||
* Priority: critical states (starving, exhausted) > regular (hungry, tired, etc.)
|
||||
*
|
||||
* @param transitions The need transitions that occurred this tick
|
||||
*/
|
||||
private void triggerNeedDialogue(NpcNeeds.NeedTransitions transitions) {
|
||||
// Find closest player to speak to
|
||||
Player nearestPlayer = context.level().getNearestPlayer(entity, 16);
|
||||
if (nearestPlayer == null) return;
|
||||
|
||||
// Priority order: critical > regular
|
||||
String dialogueId = null;
|
||||
|
||||
// Critical states first (use regular IDs for now, context makes them urgent)
|
||||
if (transitions.becameStarving) {
|
||||
dialogueId = "needs.starving";
|
||||
} else if (transitions.becameTired) {
|
||||
dialogueId = "needs.dignity_low";
|
||||
}
|
||||
// Regular states
|
||||
else if (transitions.becameHungry) {
|
||||
dialogueId = "needs.hungry";
|
||||
}
|
||||
|
||||
if (dialogueId != null) {
|
||||
EntityDialogueManager.talkByDialogueId(
|
||||
entity,
|
||||
nearestPlayer,
|
||||
dialogueId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update attribute modifiers based on rest level.
|
||||
* Tired NPCs move slower and deal less damage.
|
||||
* Called every REST_MODIFIER_UPDATE_INTERVAL ticks.
|
||||
*/
|
||||
private void updateRestModifiers() {
|
||||
if (this.personalityState == null) return;
|
||||
|
||||
NpcNeeds needs = this.personalityState.getNeeds();
|
||||
float rest = needs.getRest();
|
||||
|
||||
// Calculate modifiers based on rest level
|
||||
float speedMod = 0.0f;
|
||||
float damageMod = 0.0f;
|
||||
|
||||
if (rest < 25) {
|
||||
// Exhausted
|
||||
speedMod = -0.3f; // -30% speed
|
||||
damageMod = -0.4f; // -40% damage
|
||||
} else if (rest < 50) {
|
||||
// Tired
|
||||
speedMod = -0.15f; // -15% speed
|
||||
damageMod = -0.2f; // -20% damage
|
||||
}
|
||||
|
||||
// Only update if changed (avoid spamming attribute system)
|
||||
if (speedMod != lastRestSpeedMod) {
|
||||
AttributeInstance speedAttr = entity.getAttribute(
|
||||
Attributes.MOVEMENT_SPEED
|
||||
);
|
||||
if (speedAttr != null) {
|
||||
speedAttr.removeModifier(REST_SPEED_MODIFIER_UUID);
|
||||
if (speedMod != 0) {
|
||||
speedAttr.addTransientModifier(
|
||||
new AttributeModifier(
|
||||
REST_SPEED_MODIFIER_UUID,
|
||||
"Rest fatigue",
|
||||
speedMod,
|
||||
AttributeModifier.Operation.MULTIPLY_TOTAL
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
lastRestSpeedMod = speedMod;
|
||||
}
|
||||
|
||||
if (damageMod != lastRestDamageMod) {
|
||||
AttributeInstance damageAttr = entity.getAttribute(
|
||||
Attributes.ATTACK_DAMAGE
|
||||
);
|
||||
if (damageAttr != null) {
|
||||
damageAttr.removeModifier(REST_DAMAGE_MODIFIER_UUID);
|
||||
if (damageMod != 0) {
|
||||
damageAttr.addTransientModifier(
|
||||
new AttributeModifier(
|
||||
REST_DAMAGE_MODIFIER_UUID,
|
||||
"Rest fatigue",
|
||||
damageMod,
|
||||
AttributeModifier.Operation.MULTIPLY_TOTAL
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
lastRestDamageMod = damageMod;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DIALOGUE SYSTEMS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Tick idle dialogue system (random chatter).
|
||||
* Call this from EntityDamsel.aiStep().
|
||||
*/
|
||||
public void tickIdleDialogue() {
|
||||
// Decrement cooldown
|
||||
if (this.idleDialogueCooldown > 0) {
|
||||
this.idleDialogueCooldown--;
|
||||
return;
|
||||
}
|
||||
|
||||
// Random chance to speak FIRST (~1 in 1000 per tick = once per ~50 seconds on average)
|
||||
// Check this before expensive getNearestPlayer() call - saves 99.9% of lookups
|
||||
if (entity.getRandom().nextFloat() > 0.001f) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't speak if gagged
|
||||
if (context.getBondageManager().isGagged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't speak if following a command (busy)
|
||||
if (
|
||||
this.personalityState != null &&
|
||||
this.personalityState.getActiveCommand() != NpcCommand.NONE
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a player to talk to
|
||||
Player nearestPlayer = context.level().getNearestPlayer(entity, 16);
|
||||
if (nearestPlayer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select dialogue based on state
|
||||
String dialogueId = selectIdleDialogueId();
|
||||
if (dialogueId != null) {
|
||||
EntityDialogueManager.talkByDialogueId(
|
||||
entity,
|
||||
nearestPlayer,
|
||||
dialogueId
|
||||
);
|
||||
// Reset cooldown (4-10 minutes)
|
||||
this.idleDialogueCooldown =
|
||||
IDLE_DIALOGUE_COOLDOWN_MIN +
|
||||
entity
|
||||
.getRandom()
|
||||
.nextInt(
|
||||
IDLE_DIALOGUE_COOLDOWN_MAX - IDLE_DIALOGUE_COOLDOWN_MIN
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Select appropriate idle dialogue ID based on current state.
|
||||
* Priority: needs > mood > generic idle.
|
||||
*
|
||||
* @return Dialogue ID or null
|
||||
*/
|
||||
@Nullable
|
||||
private String selectIdleDialogueId() {
|
||||
if (this.personalityState == null) {
|
||||
return "idle.free";
|
||||
}
|
||||
|
||||
NpcNeeds needs = this.personalityState.getNeeds();
|
||||
|
||||
// Priority 1: Critical needs
|
||||
if (needs.isStarving()) return "needs.starving";
|
||||
|
||||
// Priority 2: Regular needs
|
||||
if (needs.isHungry()) return "needs.hungry";
|
||||
if (needs.isTired()) return "needs.dignity_low";
|
||||
|
||||
// Priority 3: Mood-based (if very low)
|
||||
float mood = this.personalityState.getMood();
|
||||
if (mood < 10) return "mood.miserable";
|
||||
if (mood < 40) return "mood.sad";
|
||||
|
||||
// Priority 4: State-based idle
|
||||
if (context.getBondageManager().isTiedUp()) {
|
||||
return "idle.captive";
|
||||
}
|
||||
|
||||
return "idle.free";
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect when players approach and trigger reaction dialogue.
|
||||
* Uses per-player cooldowns to avoid spam.
|
||||
* Throttled to every 10 ticks (0.5 sec) to reduce entity search overhead.
|
||||
* Call this from EntityDamsel.aiStep().
|
||||
*/
|
||||
public void tickApproachDetection() {
|
||||
// Throttle approach detection - only scan every 10 ticks (0.5 sec)
|
||||
if (--this.approachCheckCooldown > 0) {
|
||||
return;
|
||||
}
|
||||
this.approachCheckCooldown = 10;
|
||||
|
||||
// Decrement all cooldowns
|
||||
approachCooldowns
|
||||
.entrySet()
|
||||
.removeIf(e -> {
|
||||
int newVal = e.getValue() - 10; // Decrement by 10 since we only check every 10 ticks
|
||||
if (newVal <= 0) return true;
|
||||
e.setValue(newVal);
|
||||
return false;
|
||||
});
|
||||
|
||||
// Don't react if gagged
|
||||
if (context.getBondageManager().isGagged()) {
|
||||
playersNearbyLastTick.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find nearby players
|
||||
Set<UUID> currentNearby = new HashSet<>();
|
||||
List<Player> nearbyPlayers = context
|
||||
.level()
|
||||
.getEntitiesOfClass(
|
||||
Player.class,
|
||||
entity.getBoundingBox().inflate(APPROACH_RADIUS),
|
||||
p -> p.isAlive() && entity.distanceTo(p) <= APPROACH_RADIUS
|
||||
);
|
||||
|
||||
for (Player player : nearbyPlayers) {
|
||||
UUID uuid = player.getUUID();
|
||||
currentNearby.add(uuid);
|
||||
|
||||
// Skip if player was already nearby last tick
|
||||
if (playersNearbyLastTick.contains(uuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if on cooldown
|
||||
if (approachCooldowns.containsKey(uuid)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Player just approached - trigger reaction
|
||||
onPlayerApproach(player);
|
||||
approachCooldowns.put(uuid, APPROACH_COOLDOWN);
|
||||
}
|
||||
|
||||
// Update tracking set
|
||||
playersNearbyLastTick.clear();
|
||||
playersNearbyLastTick.addAll(currentNearby);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a player approaches (enters APPROACH_RADIUS).
|
||||
* Triggers appropriate dialogue based on relationship.
|
||||
*/
|
||||
private void onPlayerApproach(Player player) {
|
||||
if (this.personalityState == null) return;
|
||||
|
||||
// Use DialogueTriggerSystem to select appropriate approach dialogue
|
||||
String dialogueId =
|
||||
com.tiedup.remake.dialogue.DialogueTriggerSystem.selectApproachDialogue(
|
||||
entity,
|
||||
player
|
||||
);
|
||||
|
||||
if (dialogueId != null) {
|
||||
EntityDialogueManager.talkByDialogueId(entity, player, dialogueId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick environment dialogue (comments on weather/time).
|
||||
* Call this from EntityDamsel.aiStep().
|
||||
*/
|
||||
public void tickEnvironmentDialogue() {
|
||||
// Decrement cooldown
|
||||
if (this.environmentDialogueCooldown > 0) {
|
||||
this.environmentDialogueCooldown--;
|
||||
return;
|
||||
}
|
||||
|
||||
// Random chance (~1 in 2000 per tick)
|
||||
if (entity.getRandom().nextFloat() > 0.0005f) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't speak if gagged
|
||||
if (context.getBondageManager().isGagged()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a player to talk to
|
||||
Player nearestPlayer = context.level().getNearestPlayer(entity, 16);
|
||||
if (nearestPlayer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use DialogueTriggerSystem to select environment dialogue
|
||||
String dialogueId =
|
||||
com.tiedup.remake.dialogue.DialogueTriggerSystem.selectEnvironmentDialogue(
|
||||
entity
|
||||
);
|
||||
|
||||
if (dialogueId != null) {
|
||||
EntityDialogueManager.talkByDialogueId(
|
||||
entity,
|
||||
nearestPlayer,
|
||||
dialogueId
|
||||
);
|
||||
this.environmentDialogueCooldown = ENVIRONMENT_DIALOGUE_COOLDOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PUBLIC PERSONALITY API
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the personality state (lazy-init on server side).
|
||||
* @return PersonalityState instance, or null on client
|
||||
*/
|
||||
@Nullable
|
||||
public PersonalityState getPersonalityState() {
|
||||
// Lazy init on server side
|
||||
if (this.personalityState == null && !context.level().isClientSide) {
|
||||
initializePersonality();
|
||||
}
|
||||
return this.personalityState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the personality type (client-safe via synced data).
|
||||
* @return PersonalityType enum value
|
||||
*/
|
||||
public PersonalityType getPersonalityType() {
|
||||
String typeName = entity
|
||||
.getEntityData()
|
||||
.get(com.tiedup.remake.entities.EntityDamsel.DATA_PERSONALITY_TYPE);
|
||||
try {
|
||||
return PersonalityType.valueOf(typeName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return PersonalityType.CALM; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the personality type (debug/testing only).
|
||||
* Creates a new PersonalityState with the given type, preserving training XP.
|
||||
*
|
||||
* @param newType The new personality type
|
||||
*/
|
||||
public void setPersonalityType(PersonalityType newType) {
|
||||
if (context.level().isClientSide) return;
|
||||
|
||||
// Create new state with new personality
|
||||
this.personalityState = new PersonalityState(newType);
|
||||
|
||||
// Sync to clients
|
||||
syncPersonalityData();
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} personality set to: {}",
|
||||
context.getAppearance().getNpcName(),
|
||||
newType.name()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active command (client-safe via synced data).
|
||||
* @return NpcCommand enum value
|
||||
*/
|
||||
public NpcCommand getActiveCommand() {
|
||||
String cmdName = entity
|
||||
.getEntityData()
|
||||
.get(com.tiedup.remake.entities.EntityDamsel.DATA_ACTIVE_COMMAND);
|
||||
return NpcCommand.fromString(cmdName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if recently whipped.
|
||||
* @return true if whipped within last 1200 ticks (1 minute)
|
||||
*/
|
||||
public boolean wasRecentlyWhipped() {
|
||||
return (context.level().getGameTime() - lastWhipTime) < 1200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the world time when this NPC was last whipped.
|
||||
* @return World time of last whip, or 0 if never whipped
|
||||
*/
|
||||
public long getLastWhipTime() {
|
||||
return this.lastWhipTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the world time when this NPC was whipped.
|
||||
* @param time Current world game time
|
||||
*/
|
||||
public void setLastWhipTime(long time) {
|
||||
this.lastWhipTime = time;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// NBT PERSISTENCE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Save personality data to NBT.
|
||||
* @param tag The compound tag to save to
|
||||
*/
|
||||
public void saveToTag(CompoundTag tag) {
|
||||
if (this.personalityState != null) {
|
||||
tag.put("Personality", this.personalityState.save());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load personality data from NBT.
|
||||
* @param tag The compound tag to load from
|
||||
*/
|
||||
public void loadFromTag(CompoundTag tag) {
|
||||
if (tag.contains("Personality")) {
|
||||
this.personalityState = PersonalityState.load(
|
||||
tag.getCompound("Personality")
|
||||
);
|
||||
// CRITICAL: Must sync to client immediately after load
|
||||
syncPersonalityData();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import java.util.UUID;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Interface for AI controller callbacks to EntityDamsel.
|
||||
* Provides access to entity state and methods needed by AI systems.
|
||||
*
|
||||
* Phase 5: Created for DamselAIController component.
|
||||
*/
|
||||
public interface IAIHost {
|
||||
// ========================================
|
||||
// BASIC ENTITY ACCESS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the entity's level/world.
|
||||
*/
|
||||
Level level();
|
||||
|
||||
/**
|
||||
* Get the entity's UUID.
|
||||
*/
|
||||
UUID getUUID();
|
||||
|
||||
/**
|
||||
* Get the entity's display name.
|
||||
*/
|
||||
String getNpcName();
|
||||
|
||||
/**
|
||||
* Get the entity's random instance.
|
||||
*/
|
||||
net.minecraft.util.RandomSource getRandom();
|
||||
|
||||
/**
|
||||
* Get the entity's bounding box.
|
||||
*/
|
||||
AABB getBoundingBox();
|
||||
|
||||
// ========================================
|
||||
// LEASH SYSTEM ACCESS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if entity is leashed.
|
||||
*/
|
||||
boolean isLeashed();
|
||||
|
||||
/**
|
||||
* Get the entity holding the leash.
|
||||
*/
|
||||
Entity getLeashHolder();
|
||||
|
||||
/**
|
||||
* Get current movement delta.
|
||||
*/
|
||||
Vec3 getDeltaMovement();
|
||||
|
||||
/**
|
||||
* Set movement delta.
|
||||
*/
|
||||
void setDeltaMovement(Vec3 motion);
|
||||
|
||||
/**
|
||||
* Get max step height.
|
||||
*/
|
||||
float maxUpStep();
|
||||
|
||||
/**
|
||||
* Set max step height.
|
||||
*/
|
||||
void setMaxUpStep(float height);
|
||||
|
||||
/**
|
||||
* Teleport entity to coordinates.
|
||||
*/
|
||||
void teleportTo(double x, double y, double z);
|
||||
|
||||
/**
|
||||
* Calculate distance to another entity.
|
||||
*/
|
||||
float distanceTo(Entity other);
|
||||
|
||||
/**
|
||||
* Get entity X position.
|
||||
*/
|
||||
double getX();
|
||||
|
||||
/**
|
||||
* Get entity Y position.
|
||||
*/
|
||||
double getY();
|
||||
|
||||
/**
|
||||
* Get entity Z position.
|
||||
*/
|
||||
double getZ();
|
||||
|
||||
// ========================================
|
||||
// COMPONENT ACCESS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get bondage manager component.
|
||||
*/
|
||||
DamselBondageManager getBondageManager();
|
||||
|
||||
/**
|
||||
* Get personality system component.
|
||||
*/
|
||||
DamselPersonalitySystem getPersonalitySystem();
|
||||
|
||||
// ========================================
|
||||
// BONDAGE STATE QUERIES (delegated)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if entity is tied up.
|
||||
*/
|
||||
boolean isTiedUp();
|
||||
|
||||
/**
|
||||
* Check if entity is gagged.
|
||||
*/
|
||||
boolean isGagged();
|
||||
|
||||
/**
|
||||
* Check if entity is a captive.
|
||||
*/
|
||||
boolean isCaptive();
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Interface for animation controller callbacks to EntityDamsel.
|
||||
* Provides access to entity state and methods needed by animation systems.
|
||||
*
|
||||
* Phase 3: Created for DamselAnimationController component.
|
||||
*/
|
||||
public interface IAnimationHost {
|
||||
// ========================================
|
||||
// LEVEL ACCESS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the entity's level/world.
|
||||
*/
|
||||
Level level();
|
||||
|
||||
// ========================================
|
||||
// BODY ROTATION ACCESS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get current body rotation Y.
|
||||
*/
|
||||
float getYBodyRot();
|
||||
|
||||
/**
|
||||
* Set body rotation Y.
|
||||
*/
|
||||
void setYBodyRot(float rot);
|
||||
|
||||
/**
|
||||
* Get previous body rotation Y.
|
||||
*/
|
||||
float getYBodyRotO();
|
||||
|
||||
/**
|
||||
* Set previous body rotation Y.
|
||||
*/
|
||||
void setYBodyRotO(float rot);
|
||||
|
||||
// ========================================
|
||||
// POSE STATE QUERIES
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if entity is in "dog pose" (hogtied with armbinder).
|
||||
* This is determined by bondage manager.
|
||||
*/
|
||||
boolean isDogPose();
|
||||
|
||||
/**
|
||||
* Check if entity is sitting (from synced entity data).
|
||||
*/
|
||||
boolean isSittingFromData();
|
||||
|
||||
/**
|
||||
* Set sitting state (to synced entity data).
|
||||
*/
|
||||
void setSittingToData(boolean sitting);
|
||||
|
||||
/**
|
||||
* Check if entity is kneeling (from synced entity data).
|
||||
*/
|
||||
boolean isKneelingFromData();
|
||||
|
||||
/**
|
||||
* Set kneeling state (to synced entity data).
|
||||
*/
|
||||
void setKneelingToData(boolean kneeling);
|
||||
|
||||
/**
|
||||
* Check if entity is struggling (from synced entity data).
|
||||
*/
|
||||
boolean isStrugglingFromData();
|
||||
|
||||
/**
|
||||
* Set struggling state (to synced entity data).
|
||||
*/
|
||||
void setStrugglingToData(boolean struggling);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.personality.PersonalityState;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Interface for EntityDamsel to provide callbacks to DamselBondageManager.
|
||||
* This prevents circular dependencies while allowing the bondage component
|
||||
* to access necessary host entity functionality.
|
||||
*
|
||||
* Phase 7: Created for DamselBondageManager extraction.
|
||||
*/
|
||||
public interface IBondageHost {
|
||||
/**
|
||||
* Get the personality state for conditioning/training logic.
|
||||
* @return personality state instance
|
||||
*/
|
||||
PersonalityState getPersonalityState();
|
||||
|
||||
/**
|
||||
* Get the inventory manager for equipment access.
|
||||
* @return inventory manager instance
|
||||
*/
|
||||
com.tiedup.remake.entities.damsel.components.DamselInventoryManager getInventory();
|
||||
|
||||
/**
|
||||
* Drop an item stack at this entity's location.
|
||||
* @param stack item to drop
|
||||
*/
|
||||
void dropItemStack(ItemStack stack);
|
||||
|
||||
/**
|
||||
* Play a sound at this entity's location.
|
||||
* @param sound sound event to play
|
||||
*/
|
||||
void playSound(SoundEvent sound);
|
||||
|
||||
/**
|
||||
* Set the entity's health.
|
||||
* @param health new health value
|
||||
*/
|
||||
void setHealth(float health);
|
||||
|
||||
/**
|
||||
* Remove this entity from the world.
|
||||
* @param reason removal reason
|
||||
*/
|
||||
void remove(Entity.RemovalReason reason);
|
||||
|
||||
/**
|
||||
* Get the level this entity is in.
|
||||
* @return level instance
|
||||
*/
|
||||
Level level();
|
||||
|
||||
/**
|
||||
* Get the block position of this entity.
|
||||
* @return block position
|
||||
*/
|
||||
BlockPos blockPosition();
|
||||
|
||||
/**
|
||||
* Get the UUID of this entity.
|
||||
* @return entity UUID
|
||||
*/
|
||||
UUID getUUID();
|
||||
|
||||
/**
|
||||
* Get the damsel's name for logging.
|
||||
* @return damsel name
|
||||
*/
|
||||
String getNpcName();
|
||||
|
||||
/**
|
||||
* Broadcast dialogue to nearby players.
|
||||
* @param category dialogue category
|
||||
* @param radius search radius
|
||||
*/
|
||||
void talkToPlayersInRadius(
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
|
||||
int radius
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/**
|
||||
* Interface for dialogue handler callbacks to EntityDamsel.
|
||||
* Provides access to entity state and methods needed by dialogue systems.
|
||||
*
|
||||
* Phase 4: Created for DamselDialogueHandler component.
|
||||
*/
|
||||
public interface IDialogueHost {
|
||||
/**
|
||||
* Get the entity's level/world.
|
||||
*/
|
||||
Level level();
|
||||
|
||||
/**
|
||||
* Get the entity's bounding box for radius detection.
|
||||
*/
|
||||
AABB getBoundingBox();
|
||||
|
||||
/**
|
||||
* Get the entity itself for dialogue source.
|
||||
*/
|
||||
com.tiedup.remake.entities.EntityDamsel getEntity();
|
||||
|
||||
/**
|
||||
* Get current game time for cooldown tracking.
|
||||
*/
|
||||
long getGameTime();
|
||||
|
||||
/**
|
||||
* Find nearby players within radius.
|
||||
*/
|
||||
List<Player> findNearbyPlayers(double radius);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Interface for EntityDamsel to provide callbacks to DamselPersonalitySystem.
|
||||
* This prevents circular dependencies while allowing the personality component
|
||||
* to access necessary host entity functionality.
|
||||
*
|
||||
* Phase 6: Created for DamselPersonalitySystem extraction.
|
||||
*/
|
||||
public interface IPersonalityTickContext {
|
||||
/**
|
||||
* Get the level this entity is in.
|
||||
* @return level instance
|
||||
*/
|
||||
Level level();
|
||||
|
||||
/**
|
||||
* Get the current tick count for this entity.
|
||||
* @return tick count
|
||||
*/
|
||||
int getTickCount();
|
||||
|
||||
/**
|
||||
* Check if this entity is currently leashed.
|
||||
* @return true if leashed
|
||||
*/
|
||||
boolean isLeashed();
|
||||
|
||||
/**
|
||||
* Get the entity's current movement delta.
|
||||
* @return movement vector
|
||||
*/
|
||||
Vec3 getDeltaMovement();
|
||||
|
||||
/**
|
||||
* Check if this entity has a collar equipped.
|
||||
* @return true if collar present
|
||||
*/
|
||||
boolean hasCollar();
|
||||
|
||||
/**
|
||||
* Get the item equipped in a V2 body region.
|
||||
* @param region The body region to query
|
||||
* @return The equipped ItemStack, or empty if not present
|
||||
*/
|
||||
ItemStack getEquipment(BodyRegionV2 region);
|
||||
|
||||
/**
|
||||
* Get the bondage manager for checking bondage state.
|
||||
* @return bondage manager instance
|
||||
*/
|
||||
DamselBondageManager getBondageManager();
|
||||
|
||||
/**
|
||||
* Get the inventory manager for feeding logic.
|
||||
* @return inventory manager instance
|
||||
*/
|
||||
DamselInventoryManager getInventoryManager();
|
||||
|
||||
/**
|
||||
* Get the appearance manager for name access.
|
||||
* @return appearance instance
|
||||
*/
|
||||
DamselAppearance getAppearance();
|
||||
|
||||
/**
|
||||
* Get the entity's UUID.
|
||||
* @return entity UUID
|
||||
*/
|
||||
UUID getUUID();
|
||||
|
||||
/**
|
||||
* Get the block position of this entity.
|
||||
* @return block position
|
||||
*/
|
||||
BlockPos blockPosition();
|
||||
|
||||
/**
|
||||
* Stop the entity's navigation.
|
||||
*/
|
||||
void stopNavigation();
|
||||
|
||||
/**
|
||||
* Get the entity's leash holder entity.
|
||||
* @return leash holder, or null if not leashed
|
||||
*/
|
||||
net.minecraft.world.entity.Entity getLeashHolder();
|
||||
|
||||
/**
|
||||
* Talk to nearby players with a dialogue message.
|
||||
* @param category dialogue category
|
||||
* @param radius search radius
|
||||
*/
|
||||
void talkToPlayersInRadius(
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory category,
|
||||
int radius
|
||||
);
|
||||
|
||||
/**
|
||||
* Talk directly to a specific player.
|
||||
* @param player target player
|
||||
* @param message message text
|
||||
*/
|
||||
void talkTo(Player player, String message);
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.state.ICaptor;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.util.RestraintEffectUtils;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Captivity lifecycle for NPCs: capture, free, transfer, leash, captor restoration.
|
||||
*
|
||||
* <p>Extracted from DamselBondageManager (H9 split). Owns the captor reference
|
||||
* and pending UUID for restoration after world load.</p>
|
||||
*/
|
||||
public class NpcCaptivityManager {
|
||||
|
||||
// ========================================
|
||||
// FIELDS
|
||||
// ========================================
|
||||
|
||||
private final AbstractTiedUpNpc entity;
|
||||
private final IBondageHost host;
|
||||
private final NpcEquipmentManager equipment;
|
||||
|
||||
/** Current captor (if captured) */
|
||||
private ICaptor captor;
|
||||
|
||||
/** Pending captor UUID for restoration after world load */
|
||||
private UUID pendingCaptorUUID;
|
||||
|
||||
// ========================================
|
||||
// CONSTRUCTOR
|
||||
// ========================================
|
||||
|
||||
public NpcCaptivityManager(
|
||||
AbstractTiedUpNpc entity,
|
||||
IBondageHost host,
|
||||
NpcEquipmentManager equipment
|
||||
) {
|
||||
this.entity = entity;
|
||||
this.host = host;
|
||||
this.equipment = equipment;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CAPTIVITY STATE
|
||||
// ========================================
|
||||
|
||||
public boolean isCaptive() {
|
||||
return entity.isLeashed();
|
||||
}
|
||||
|
||||
public boolean isEnslavable() {
|
||||
if (entity.isLeashed()) return false;
|
||||
return equipment.isTiedUp();
|
||||
}
|
||||
|
||||
public ICaptor getCaptor() {
|
||||
return captor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the captor reference without triggering free() or untie().
|
||||
* Used when player detaches leash -- only clears the internal reference.
|
||||
*/
|
||||
public void clearCaptor() {
|
||||
this.captor = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CAPTURE LIFECYCLE
|
||||
// ========================================
|
||||
|
||||
public boolean getCapturedBy(ICaptor newCaptor) {
|
||||
if (newCaptor == null || !isEnslavable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!newCaptor.canCapture(this.asFacadeKidnapped())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use vanilla leash mechanic
|
||||
Entity captorEntity = newCaptor.getEntity();
|
||||
if (captorEntity != null) {
|
||||
entity.setLeashedTo(captorEntity, true);
|
||||
this.captor = newCaptor;
|
||||
newCaptor.addCaptive(entity);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} captured by {}",
|
||||
host.getNpcName(),
|
||||
captorEntity.getName().getString()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force capture for managed camp operations (dogwalk, extract).
|
||||
* Bypasses canCapture() PrisonerManager state check since the
|
||||
* caller is responsible for managing state transitions.
|
||||
*/
|
||||
public boolean forceCapturedBy(ICaptor newCaptor) {
|
||||
if (newCaptor == null || !isEnslavable()) {
|
||||
return false;
|
||||
}
|
||||
// Skip canCapture() - caller manages PrisonerManager state
|
||||
Entity captorEntity = newCaptor.getEntity();
|
||||
if (captorEntity != null) {
|
||||
entity.setLeashedTo(captorEntity, true);
|
||||
this.captor = newCaptor;
|
||||
newCaptor.addCaptive(entity);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} force-captured by {} (managed op)",
|
||||
host.getNpcName(),
|
||||
captorEntity.getName().getString()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void free() {
|
||||
free(true);
|
||||
}
|
||||
|
||||
public void free(boolean transportState) {
|
||||
if (!isCaptive()) return;
|
||||
|
||||
if (transportState) {
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} freed from captivity",
|
||||
host.getNpcName()
|
||||
);
|
||||
|
||||
// Broadcast freed dialogue
|
||||
if (!entity.level().isClientSide()) {
|
||||
host.talkToPlayersInRadius(
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.DAMSEL_FREED,
|
||||
ModConfig.SERVER.dialogueRadius.get()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} transferred (leash dropped)",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
// Drop leash (only drop item on true freedom, not internal transfers like imprisonment)
|
||||
entity.dropLeash(true, transportState);
|
||||
|
||||
// Clear captor
|
||||
if (captor != null) {
|
||||
captor.removeCaptive(entity, transportState);
|
||||
captor = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void transferCaptivityTo(ICaptor newCaptor) {
|
||||
if (!isCaptive()) return;
|
||||
|
||||
ICaptor currentCaptor = getCaptor();
|
||||
if (currentCaptor == null) return;
|
||||
|
||||
if (!currentCaptor.allowCaptiveTransfer()) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[EntityDamsel] {} transfer blocked by current captor",
|
||||
host.getNpcName()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Free from current captor (keep transport entity)
|
||||
free(false);
|
||||
|
||||
// Capture by new captor
|
||||
getCapturedBy(newCaptor);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} transferred to new captor",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// POLE / LEASH
|
||||
// ========================================
|
||||
|
||||
public boolean isTiedToPole() {
|
||||
Entity leashHolder = entity.getLeashHolder();
|
||||
return (
|
||||
leashHolder instanceof
|
||||
net.minecraft.world.entity.decoration.LeashFenceKnotEntity
|
||||
);
|
||||
}
|
||||
|
||||
public boolean tieToClosestPole(int searchRadius) {
|
||||
return RestraintEffectUtils.tieToClosestPole(entity, searchRadius);
|
||||
}
|
||||
|
||||
public boolean canBeKidnappedByEvents() {
|
||||
return !isCaptive() && !equipment.hasCollar();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CAPTOR RESTORATION
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Attempt to restore captor from pending UUID.
|
||||
* Call this from EntityDamsel.tick() until successful.
|
||||
*/
|
||||
public void restoreCaptorFromUUID() {
|
||||
if (pendingCaptorUUID == null) return;
|
||||
|
||||
// Search for entity with matching UUID
|
||||
for (Entity searchEntity : entity
|
||||
.level()
|
||||
.getEntities(entity, entity.getBoundingBox().inflate(64))) {
|
||||
if (searchEntity.getUUID().equals(pendingCaptorUUID)) {
|
||||
if (searchEntity instanceof ICaptor kidnapper) {
|
||||
captor = kidnapper;
|
||||
kidnapper.addCaptive(entity);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} restored captor relationship with {}",
|
||||
host.getNpcName(),
|
||||
searchEntity.getName().getString()
|
||||
);
|
||||
}
|
||||
pendingCaptorUUID = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Also check players (PlayerBindState.getCaptorManager() is ICaptor)
|
||||
for (net.minecraft.world.entity.player.Player player : entity
|
||||
.level()
|
||||
.players()) {
|
||||
if (player.getUUID().equals(pendingCaptorUUID)) {
|
||||
PlayerBindState bindState =
|
||||
PlayerBindState.getInstance(player);
|
||||
if (bindState != null) {
|
||||
ICaptor kidnapper = bindState.getCaptorManager();
|
||||
if (kidnapper != null) {
|
||||
captor = kidnapper;
|
||||
kidnapper.addCaptive(entity);
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} restored captor relationship with player {}",
|
||||
host.getNpcName(),
|
||||
player.getName().getString()
|
||||
);
|
||||
}
|
||||
}
|
||||
pendingCaptorUUID = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PERSISTENCE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Save captivity-related state to NBT.
|
||||
*/
|
||||
public void saveCaptivityToTag(CompoundTag tag) {
|
||||
if (captor != null && captor.getEntity() != null) {
|
||||
tag.putUUID("CaptorUUID", captor.getEntity().getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load captivity-related state from NBT.
|
||||
*/
|
||||
public void loadCaptivityFromTag(CompoundTag tag) {
|
||||
if (tag.contains("CaptorUUID")) {
|
||||
pendingCaptorUUID = tag.getUUID("CaptorUUID");
|
||||
} else if (tag.contains("MasterUUID")) {
|
||||
// Legacy support
|
||||
pendingCaptorUUID = tag.getUUID("MasterUUID");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INTERNAL
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Get the facade IRestrainable reference for canCapture() calls.
|
||||
* This is the entity itself (AbstractTiedUpNpc implements IRestrainable via delegation).
|
||||
*/
|
||||
private com.tiedup.remake.state.IRestrainable asFacadeKidnapped() {
|
||||
return (com.tiedup.remake.state.IRestrainable) entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,940 @@
|
||||
package com.tiedup.remake.entities.damsel.components;
|
||||
|
||||
import com.tiedup.remake.core.ModConfig;
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.items.base.ILockable;
|
||||
import com.tiedup.remake.items.base.ItemCollar;
|
||||
import com.tiedup.remake.state.IRestrainableEntity;
|
||||
import com.tiedup.remake.util.RestraintEffectUtils;
|
||||
import com.tiedup.remake.util.TiedUpSounds;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageItem;
|
||||
import com.tiedup.remake.v2.bondage.capability.V2BondageEquipment;
|
||||
import java.util.Map;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/**
|
||||
* Equipment CRUD, coercion, bulk operations, and permissions for NPC bondage items.
|
||||
*
|
||||
* <p>Extracted from DamselBondageManager (H9 split). Owns all equipment state
|
||||
* and delegates to V2BondageEquipment for storage. Does NOT own captivity state.</p>
|
||||
*
|
||||
* <p>Cross-cutting note: {@code untie()} stays in the facade because it calls
|
||||
* both equipment and captivity methods.</p>
|
||||
*/
|
||||
public class NpcEquipmentManager {
|
||||
|
||||
// ========================================
|
||||
// FIELDS
|
||||
// ========================================
|
||||
|
||||
private final AbstractTiedUpNpc entity;
|
||||
private final IBondageHost host;
|
||||
|
||||
/** Maps legacy V1 NBT key names to V2 body regions for migration. */
|
||||
static final Map<String, BodyRegionV2> V1_TO_V2 = Map.of(
|
||||
"Bind", BodyRegionV2.ARMS,
|
||||
"Gag", BodyRegionV2.MOUTH,
|
||||
"Blindfold", BodyRegionV2.EYES,
|
||||
"Earplugs", BodyRegionV2.EARS,
|
||||
"Collar", BodyRegionV2.NECK,
|
||||
"Clothes", BodyRegionV2.TORSO,
|
||||
"Mittens", BodyRegionV2.HANDS
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// CONSTRUCTOR
|
||||
// ========================================
|
||||
|
||||
public NpcEquipmentManager(AbstractTiedUpNpc entity, IBondageHost host) {
|
||||
this.entity = entity;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// V2 EQUIPMENT HELPERS
|
||||
// ========================================
|
||||
|
||||
/** Get the V2 equipment storage from the parent entity, cast to concrete type. */
|
||||
V2BondageEquipment getEquipment() {
|
||||
return (V2BondageEquipment) entity.getV2Equipment();
|
||||
}
|
||||
|
||||
/** Get the item equipped in a V2 body region. */
|
||||
public ItemStack getInRegion(BodyRegionV2 region) {
|
||||
return getEquipment().getInRegion(region);
|
||||
}
|
||||
|
||||
/** Serialize V2 equipment to synched entity data (copy-on-write). */
|
||||
void syncToEntityData() {
|
||||
entity.syncV2Equipment();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// STATE QUERIES (14 methods)
|
||||
// ========================================
|
||||
|
||||
public boolean isTiedUp() {
|
||||
return getEquipment().isRegionOccupied(BodyRegionV2.ARMS);
|
||||
}
|
||||
|
||||
public boolean isGagged() {
|
||||
return getEquipment().isRegionOccupied(BodyRegionV2.MOUTH);
|
||||
}
|
||||
|
||||
public boolean isBlindfolded() {
|
||||
return getEquipment().isRegionOccupied(BodyRegionV2.EYES);
|
||||
}
|
||||
|
||||
public boolean hasEarplugs() {
|
||||
return getEquipment().isRegionOccupied(BodyRegionV2.EARS);
|
||||
}
|
||||
|
||||
public boolean hasCollar() {
|
||||
return getEquipment().isRegionOccupied(BodyRegionV2.NECK);
|
||||
}
|
||||
|
||||
public boolean hasClothes() {
|
||||
return getEquipment().isRegionOccupied(BodyRegionV2.TORSO);
|
||||
}
|
||||
|
||||
public boolean hasMittens() {
|
||||
return getEquipment().isRegionOccupied(BodyRegionV2.HANDS);
|
||||
}
|
||||
|
||||
public boolean isBoundAndGagged() {
|
||||
return isTiedUp() && isGagged();
|
||||
}
|
||||
|
||||
public boolean hasGaggingEffect() {
|
||||
ItemStack gag = getCurrentGag();
|
||||
if (gag.isEmpty()) return false;
|
||||
return (
|
||||
gag.getItem() instanceof
|
||||
com.tiedup.remake.items.base.IHasGaggingEffect
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasBlindingEffect() {
|
||||
ItemStack blindfold = getCurrentBlindfold();
|
||||
if (blindfold.isEmpty()) return false;
|
||||
return (
|
||||
blindfold.getItem() instanceof
|
||||
com.tiedup.remake.items.base.IHasBlindingEffect
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasKnives() {
|
||||
return false; // NPCs don't have inventories for knives
|
||||
}
|
||||
|
||||
public boolean hasClothesWithSmallArms() {
|
||||
// For damsels, delegate to appearance component
|
||||
return entity.hasSlimArms();
|
||||
}
|
||||
|
||||
public boolean hasLockedCollar() {
|
||||
ItemStack collar = getCurrentCollar();
|
||||
if (collar.isEmpty()) return false;
|
||||
if (collar.getItem() instanceof ILockable lockable) {
|
||||
return lockable.isLocked(collar);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasNamedCollar() {
|
||||
ItemStack collar = getCurrentCollar();
|
||||
if (collar.isEmpty()) return false;
|
||||
return collar.hasCustomHoverName();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// EQUIPMENT GETTERS (7 methods)
|
||||
// ========================================
|
||||
|
||||
public ItemStack getCurrentBind() {
|
||||
return getEquipment().getInRegion(BodyRegionV2.ARMS);
|
||||
}
|
||||
|
||||
public ItemStack getCurrentGag() {
|
||||
return getEquipment().getInRegion(BodyRegionV2.MOUTH);
|
||||
}
|
||||
|
||||
public ItemStack getCurrentBlindfold() {
|
||||
return getEquipment().getInRegion(BodyRegionV2.EYES);
|
||||
}
|
||||
|
||||
public ItemStack getCurrentEarplugs() {
|
||||
return getEquipment().getInRegion(BodyRegionV2.EARS);
|
||||
}
|
||||
|
||||
public ItemStack getCurrentCollar() {
|
||||
return getEquipment().getInRegion(BodyRegionV2.NECK);
|
||||
}
|
||||
|
||||
public ItemStack getCurrentClothes() {
|
||||
return getEquipment().getInRegion(BodyRegionV2.TORSO);
|
||||
}
|
||||
|
||||
public ItemStack getCurrentMittens() {
|
||||
return getEquipment().getInRegion(BodyRegionV2.HANDS);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// EQUIPMENT PUTTERS (7 methods)
|
||||
// ========================================
|
||||
|
||||
public void putBindOn(ItemStack bind) {
|
||||
if (bind.isEmpty()) return;
|
||||
|
||||
boolean wasAlreadyTied = isTiedUp();
|
||||
getEquipment().setInRegion(BodyRegionV2.ARMS, bind.copy());
|
||||
syncToEntityData();
|
||||
|
||||
// Call onEquipped hook
|
||||
if (bind.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onEquipped(bind, entity);
|
||||
}
|
||||
|
||||
// Apply speed reduction
|
||||
RestraintEffectUtils.applyBindSpeedReduction(entity);
|
||||
|
||||
// Play sound
|
||||
TiedUpSounds.playBindSound(entity);
|
||||
|
||||
// Stop movement
|
||||
entity.getNavigation().stop();
|
||||
|
||||
// Broadcast captured dialogue if just got tied
|
||||
if (!wasAlreadyTied && !entity.level().isClientSide()) {
|
||||
host.talkToPlayersInRadius(
|
||||
com.tiedup.remake.dialogue.EntityDialogueManager.DialogueCategory.DAMSEL_CAPTURED,
|
||||
ModConfig.SERVER.dialogueRadius.get()
|
||||
);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} tied up",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
public void putGagOn(ItemStack gag) {
|
||||
if (gag.isEmpty()) return;
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.MOUTH, gag.copy());
|
||||
syncToEntityData();
|
||||
|
||||
if (gag.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onEquipped(gag, entity);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} gagged",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
public void putBlindfoldOn(ItemStack blindfold) {
|
||||
if (blindfold.isEmpty()) return;
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.EYES, blindfold.copy());
|
||||
syncToEntityData();
|
||||
|
||||
if (blindfold.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onEquipped(blindfold, entity);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} blindfolded",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
public void putEarplugsOn(ItemStack earplugs) {
|
||||
if (earplugs.isEmpty()) return;
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.EARS, earplugs.copy());
|
||||
syncToEntityData();
|
||||
|
||||
if (earplugs.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onEquipped(earplugs, entity);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} has earplugs",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
public void putCollarOn(ItemStack collar) {
|
||||
if (collar.isEmpty()) return;
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.NECK, collar.copy());
|
||||
syncToEntityData();
|
||||
|
||||
if (collar.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onEquipped(collar, entity);
|
||||
}
|
||||
|
||||
// Play lock sound
|
||||
TiedUpSounds.playLockSound(entity);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} collared",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
public void putClothesOn(ItemStack clothes) {
|
||||
if (clothes.isEmpty()) return;
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.TORSO, clothes.copy());
|
||||
syncToEntityData();
|
||||
|
||||
if (clothes.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onEquipped(clothes, entity);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} changed clothes",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
public void putMittensOn(ItemStack mittens) {
|
||||
if (mittens.isEmpty()) return;
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.HANDS, mittens.copy());
|
||||
syncToEntityData();
|
||||
|
||||
if (mittens.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onEquipped(mittens, entity);
|
||||
}
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} has mittens",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// EQUIPMENT REMOVERS (8 methods + 1 force-remove)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Force-remove the item from a region, bypassing ILockable lock checks.
|
||||
* Runs the same side effects as the per-region takeXOff methods (sync, onUnequipped, etc.).
|
||||
* Used by {@link DamselBondageManager#forceUnequip(BodyRegionV2)}.
|
||||
*
|
||||
* @param region The body region to force-remove from
|
||||
* @return The removed ItemStack, or {@link ItemStack#EMPTY} if the slot was empty
|
||||
*/
|
||||
public ItemStack forceRemoveFromRegion(BodyRegionV2 region) {
|
||||
// NECK and TORSO already have correct force/no-lock handling
|
||||
if (region == BodyRegionV2.NECK) return takeCollarOff(true);
|
||||
if (region == BodyRegionV2.TORSO) return takeClothesOff();
|
||||
|
||||
ItemStack current = getInRegion(region);
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
// Clear slot + sync (no lock check — that's the point of force)
|
||||
getEquipment().setInRegion(region, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
// Region-specific side effects
|
||||
if (region == BodyRegionV2.ARMS) {
|
||||
RestraintEffectUtils.removeBindSpeedReduction(entity);
|
||||
TiedUpMod.LOGGER.debug("[EntityDamsel] {} force-untied", host.getNpcName());
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public ItemStack takeBindOff() {
|
||||
ItemStack current = getCurrentBind();
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
// Check if locked
|
||||
if (
|
||||
current.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(current)
|
||||
) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.ARMS, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
// Remove speed reduction
|
||||
RestraintEffectUtils.removeBindSpeedReduction(entity);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} untied",
|
||||
host.getNpcName()
|
||||
);
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public ItemStack takeGagOff() {
|
||||
ItemStack current = getCurrentGag();
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
if (
|
||||
current.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(current)
|
||||
) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.MOUTH, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public ItemStack takeBlindfoldOff() {
|
||||
ItemStack current = getCurrentBlindfold();
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
if (
|
||||
current.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(current)
|
||||
) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.EYES, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public ItemStack takeEarplugsOff() {
|
||||
ItemStack current = getCurrentEarplugs();
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
if (
|
||||
current.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(current)
|
||||
) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.EARS, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public ItemStack takeCollarOff() {
|
||||
return takeCollarOff(false);
|
||||
}
|
||||
|
||||
public ItemStack takeCollarOff(boolean force) {
|
||||
ItemStack current = getCurrentCollar();
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
if (
|
||||
!force &&
|
||||
current.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(current)
|
||||
) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.NECK, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
// Play unlock sound
|
||||
TiedUpSounds.playUnlockSound(entity);
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public ItemStack takeClothesOff() {
|
||||
ItemStack current = getCurrentClothes();
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.TORSO, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public ItemStack takeMittensOff() {
|
||||
ItemStack current = getCurrentMittens();
|
||||
if (current.isEmpty()) return ItemStack.EMPTY;
|
||||
|
||||
if (
|
||||
current.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(current)
|
||||
) {
|
||||
return ItemStack.EMPTY;
|
||||
}
|
||||
|
||||
getEquipment().setInRegion(BodyRegionV2.HANDS, ItemStack.EMPTY);
|
||||
syncToEntityData();
|
||||
|
||||
if (current.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(current, entity);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// EQUIPMENT REPLACERS (14 methods)
|
||||
// ========================================
|
||||
|
||||
public ItemStack replaceBind(ItemStack newBind) {
|
||||
ItemStack oldBind = takeBindOff();
|
||||
if (!oldBind.isEmpty() || !newBind.isEmpty()) {
|
||||
putBindOn(newBind);
|
||||
}
|
||||
return oldBind;
|
||||
}
|
||||
|
||||
public ItemStack replaceBind(ItemStack newBind, boolean force) {
|
||||
ItemStack oldBind = force ? getCurrentBind() : takeBindOff();
|
||||
if (!oldBind.isEmpty() || !newBind.isEmpty()) {
|
||||
if (force && !oldBind.isEmpty()) {
|
||||
getEquipment().setInRegion(BodyRegionV2.ARMS, ItemStack.EMPTY);
|
||||
// Don't sync here -- putBindOn will sync
|
||||
if (oldBind.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(oldBind, entity);
|
||||
}
|
||||
}
|
||||
putBindOn(newBind);
|
||||
}
|
||||
return oldBind;
|
||||
}
|
||||
|
||||
public ItemStack replaceGag(ItemStack newGag) {
|
||||
return replaceGag(newGag, false);
|
||||
}
|
||||
|
||||
public ItemStack replaceGag(ItemStack newGag, boolean force) {
|
||||
ItemStack oldGag = force ? getCurrentGag() : takeGagOff();
|
||||
if (!oldGag.isEmpty() || !newGag.isEmpty()) {
|
||||
if (force && !oldGag.isEmpty()) {
|
||||
getEquipment().setInRegion(BodyRegionV2.MOUTH, ItemStack.EMPTY);
|
||||
if (oldGag.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(oldGag, entity);
|
||||
}
|
||||
}
|
||||
putGagOn(newGag);
|
||||
}
|
||||
return oldGag;
|
||||
}
|
||||
|
||||
public ItemStack replaceBlindfold(ItemStack newBlindfold) {
|
||||
return replaceBlindfold(newBlindfold, false);
|
||||
}
|
||||
|
||||
public ItemStack replaceBlindfold(ItemStack newBlindfold, boolean force) {
|
||||
ItemStack oldBlindfold = force
|
||||
? getCurrentBlindfold()
|
||||
: takeBlindfoldOff();
|
||||
if (!oldBlindfold.isEmpty() || !newBlindfold.isEmpty()) {
|
||||
if (force && !oldBlindfold.isEmpty()) {
|
||||
getEquipment().setInRegion(BodyRegionV2.EYES, ItemStack.EMPTY);
|
||||
if (oldBlindfold.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(oldBlindfold, entity);
|
||||
}
|
||||
}
|
||||
putBlindfoldOn(newBlindfold);
|
||||
}
|
||||
return oldBlindfold;
|
||||
}
|
||||
|
||||
public ItemStack replaceEarplugs(ItemStack newEarplugs) {
|
||||
return replaceEarplugs(newEarplugs, false);
|
||||
}
|
||||
|
||||
public ItemStack replaceEarplugs(ItemStack newEarplugs, boolean force) {
|
||||
ItemStack oldEarplugs = force
|
||||
? getCurrentEarplugs()
|
||||
: takeEarplugsOff();
|
||||
if (!oldEarplugs.isEmpty() || !newEarplugs.isEmpty()) {
|
||||
if (force && !oldEarplugs.isEmpty()) {
|
||||
getEquipment().setInRegion(BodyRegionV2.EARS, ItemStack.EMPTY);
|
||||
if (oldEarplugs.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(oldEarplugs, entity);
|
||||
}
|
||||
}
|
||||
putEarplugsOn(newEarplugs);
|
||||
}
|
||||
return oldEarplugs;
|
||||
}
|
||||
|
||||
public ItemStack replaceCollar(ItemStack newCollar) {
|
||||
return replaceCollar(newCollar, false);
|
||||
}
|
||||
|
||||
public ItemStack replaceCollar(ItemStack newCollar, boolean force) {
|
||||
ItemStack oldCollar = takeCollarOff(force);
|
||||
if (!oldCollar.isEmpty() || !newCollar.isEmpty()) {
|
||||
putCollarOn(newCollar);
|
||||
}
|
||||
return oldCollar;
|
||||
}
|
||||
|
||||
public ItemStack replaceClothes(ItemStack newClothes) {
|
||||
return replaceClothes(newClothes, false);
|
||||
}
|
||||
|
||||
public ItemStack replaceClothes(ItemStack newClothes, boolean force) {
|
||||
ItemStack oldClothes = takeClothesOff();
|
||||
if (!oldClothes.isEmpty() || !newClothes.isEmpty()) {
|
||||
putClothesOn(newClothes);
|
||||
}
|
||||
return oldClothes;
|
||||
}
|
||||
|
||||
public ItemStack replaceMittens(ItemStack newMittens) {
|
||||
return replaceMittens(newMittens, false);
|
||||
}
|
||||
|
||||
public ItemStack replaceMittens(ItemStack newMittens, boolean force) {
|
||||
ItemStack oldMittens = force ? getCurrentMittens() : takeMittensOff();
|
||||
if (!oldMittens.isEmpty() || !newMittens.isEmpty()) {
|
||||
if (force && !oldMittens.isEmpty()) {
|
||||
getEquipment().setInRegion(BodyRegionV2.HANDS, ItemStack.EMPTY);
|
||||
if (oldMittens.getItem() instanceof IV2BondageItem bondageItem) {
|
||||
bondageItem.onUnequipped(oldMittens, entity);
|
||||
}
|
||||
}
|
||||
putMittensOn(newMittens);
|
||||
}
|
||||
return oldMittens;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BULK OPERATIONS
|
||||
// ========================================
|
||||
|
||||
public void applyBondage(
|
||||
ItemStack bind,
|
||||
ItemStack gag,
|
||||
ItemStack blindfold,
|
||||
ItemStack earplugs,
|
||||
ItemStack collar,
|
||||
ItemStack clothes
|
||||
) {
|
||||
if (!bind.isEmpty()) putBindOn(bind);
|
||||
if (!gag.isEmpty()) putGagOn(gag);
|
||||
if (!blindfold.isEmpty()) putBlindfoldOn(blindfold);
|
||||
if (!earplugs.isEmpty()) putEarplugsOn(earplugs);
|
||||
if (!collar.isEmpty()) putCollarOn(collar);
|
||||
if (!clothes.isEmpty()) putClothesOn(clothes);
|
||||
|
||||
TiedUpMod.LOGGER.info(
|
||||
"[EntityDamsel] {} fully restrained",
|
||||
host.getNpcName()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop bondage items on the ground. Convenience overload.
|
||||
*/
|
||||
public void dropBondageItems(boolean drop) {
|
||||
dropBondageItems(drop, true, true, true, true, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop bondage items with optional bind control. Convenience overload.
|
||||
*/
|
||||
public void dropBondageItems(boolean drop, boolean dropBind) {
|
||||
dropBondageItems(drop, dropBind, true, true, true, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop bondage items with full granular control.
|
||||
*
|
||||
* @param drop master switch -- if false, nothing is dropped
|
||||
*/
|
||||
public void dropBondageItems(
|
||||
boolean drop,
|
||||
boolean dropBind,
|
||||
boolean dropGag,
|
||||
boolean dropBlindfold,
|
||||
boolean dropEarplugs,
|
||||
boolean dropCollar,
|
||||
boolean dropClothes
|
||||
) {
|
||||
if (!drop) return;
|
||||
|
||||
if (dropBind) {
|
||||
ItemStack bind = takeBindOff();
|
||||
if (!bind.isEmpty()) host.dropItemStack(bind);
|
||||
}
|
||||
if (dropGag) {
|
||||
ItemStack gag = takeGagOff();
|
||||
if (!gag.isEmpty()) host.dropItemStack(gag);
|
||||
}
|
||||
if (dropBlindfold) {
|
||||
ItemStack blindfold = takeBlindfoldOff();
|
||||
if (!blindfold.isEmpty()) host.dropItemStack(blindfold);
|
||||
}
|
||||
if (dropEarplugs) {
|
||||
ItemStack earplugs = takeEarplugsOff();
|
||||
if (!earplugs.isEmpty()) host.dropItemStack(earplugs);
|
||||
}
|
||||
if (dropCollar) {
|
||||
ItemStack collar = takeCollarOff();
|
||||
if (!collar.isEmpty()) host.dropItemStack(collar);
|
||||
}
|
||||
if (dropClothes) {
|
||||
ItemStack clothes = takeClothesOff();
|
||||
if (!clothes.isEmpty()) host.dropItemStack(clothes);
|
||||
}
|
||||
// Always drop mittens if dropping
|
||||
ItemStack mittens = takeMittensOff();
|
||||
if (!mittens.isEmpty()) host.dropItemStack(mittens);
|
||||
}
|
||||
|
||||
public void dropClothes() {
|
||||
ItemStack clothes = takeClothesOff();
|
||||
if (!clothes.isEmpty()) {
|
||||
host.dropItemStack(clothes);
|
||||
}
|
||||
}
|
||||
|
||||
public int getBondageItemsWhichCanBeRemovedCount() {
|
||||
int count = 0;
|
||||
if (!getCurrentBind().isEmpty() && !isLocked(getCurrentBind())) count++;
|
||||
if (!getCurrentGag().isEmpty() && !isLocked(getCurrentGag())) count++;
|
||||
if (!getCurrentBlindfold().isEmpty() && !isLocked(getCurrentBlindfold())) count++;
|
||||
if (!getCurrentEarplugs().isEmpty() && !isLocked(getCurrentEarplugs())) count++;
|
||||
if (!getCurrentCollar().isEmpty() && !isLocked(getCurrentCollar())) count++;
|
||||
if (!getCurrentClothes().isEmpty() && !isLocked(getCurrentClothes())) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// ADVANCED QUERIES
|
||||
// ========================================
|
||||
|
||||
public boolean canBeTiedUp() {
|
||||
return !isTiedUp();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PERMISSIONS
|
||||
// ========================================
|
||||
|
||||
public boolean canTakeOffClothes(Player player) {
|
||||
return true; // NPCs don't have permission restrictions
|
||||
}
|
||||
|
||||
public boolean canChangeClothes(Player player) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canChangeClothes() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COLLAR OWNER CHECK
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if a player is an owner of this NPC's collar.
|
||||
* Returns true if no collar (anyone can interact with uncolored NPCs).
|
||||
*/
|
||||
public boolean isCollarOwner(Player player) {
|
||||
if (!hasCollar()) return true;
|
||||
ItemStack collar = getCurrentCollar();
|
||||
if (!(collar.getItem() instanceof ItemCollar collarItem)) return true;
|
||||
return collarItem.isOwner(collar, player);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COERCION
|
||||
// ========================================
|
||||
|
||||
public void tighten(Player tightener) {
|
||||
if (!isTiedUp()) return;
|
||||
|
||||
TiedUpSounds.playSound(
|
||||
entity,
|
||||
net.minecraft.sounds.SoundEvents.PLAYER_ATTACK_NODAMAGE,
|
||||
1.0f
|
||||
);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} tightened by {}",
|
||||
host.getNpcName(),
|
||||
tightener.getName().getString()
|
||||
);
|
||||
}
|
||||
|
||||
public void applyChloroform(int duration) {
|
||||
RestraintEffectUtils.applyChloroformEffects(entity, duration);
|
||||
}
|
||||
|
||||
public void shockKidnapped() {
|
||||
shockKidnapped("", 1.0f);
|
||||
}
|
||||
|
||||
public void shockKidnapped(String messageAddon, float damage) {
|
||||
// Play shock sound
|
||||
TiedUpSounds.playSound(
|
||||
entity,
|
||||
net.minecraft.sounds.SoundEvents.LIGHTNING_BOLT_IMPACT,
|
||||
0.5f
|
||||
);
|
||||
|
||||
// Apply damage
|
||||
entity.hurt(entity.damageSources().magic(), damage);
|
||||
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} shocked ({} damage)",
|
||||
host.getNpcName(),
|
||||
damage
|
||||
);
|
||||
}
|
||||
|
||||
public void takeBondageItemBy(IRestrainableEntity taker, int slotIndex) {
|
||||
ItemStack taken = ItemStack.EMPTY;
|
||||
|
||||
switch (slotIndex) {
|
||||
case 0 -> taken = takeBindOff();
|
||||
case 1 -> taken = takeGagOff();
|
||||
case 2 -> taken = takeBlindfoldOff();
|
||||
case 3 -> taken = takeEarplugsOff();
|
||||
case 4 -> taken = takeCollarOff();
|
||||
case 5 -> taken = takeClothesOff();
|
||||
}
|
||||
|
||||
if (!taken.isEmpty()) {
|
||||
taker.kidnappedDropItem(taken);
|
||||
TiedUpMod.LOGGER.debug(
|
||||
"[EntityDamsel] {} item taken by {}",
|
||||
host.getNpcName(),
|
||||
taker.getKidnappedName()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INTERNAL - UNLOCK ALL
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Unlock all locked bondage items on this entity.
|
||||
* Used by the facade in onDeathKidnapped.
|
||||
* Epic 4B: Uses V2 regions, syncs once at end.
|
||||
*/
|
||||
public void unlockAllItems() {
|
||||
BodyRegionV2[] regions = {
|
||||
BodyRegionV2.ARMS, BodyRegionV2.MOUTH, BodyRegionV2.EYES,
|
||||
BodyRegionV2.EARS, BodyRegionV2.NECK
|
||||
};
|
||||
boolean changed = false;
|
||||
for (BodyRegionV2 region : regions) {
|
||||
ItemStack stack = getEquipment().getInRegion(region);
|
||||
if (
|
||||
!stack.isEmpty() &&
|
||||
stack.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(stack)
|
||||
) {
|
||||
lockable.setLocked(stack, false);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
syncToEntityData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all V2 regions at once and sync. Used by untie(drop=false) in facade.
|
||||
*/
|
||||
public void clearAllAndSync() {
|
||||
getEquipment().clearAll();
|
||||
syncToEntityData();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PERSISTENCE
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Save equipment-related state to NBT.
|
||||
* Epic 4B: Serializes V2BondageEquipment as a single CompoundTag.
|
||||
*/
|
||||
public void saveEquipmentToTag(CompoundTag tag) {
|
||||
tag.put("V2Equipment", getEquipment().serializeNBT());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load equipment-related state from NBT.
|
||||
* Supports both V2 format and legacy V1 migration.
|
||||
*/
|
||||
public void loadEquipmentFromTag(CompoundTag tag) {
|
||||
if (tag.contains("V2Equipment", Tag.TAG_COMPOUND)) {
|
||||
// V2 format: deserialize directly
|
||||
getEquipment().deserializeNBT(tag.getCompound("V2Equipment"));
|
||||
} else {
|
||||
// Legacy V1 migration: load individual item keys into V2 regions
|
||||
for (Map.Entry<String, BodyRegionV2> entry : V1_TO_V2.entrySet()) {
|
||||
if (tag.contains(entry.getKey(), Tag.TAG_COMPOUND)) {
|
||||
ItemStack stack = ItemStack.of(tag.getCompound(entry.getKey()));
|
||||
if (!stack.isEmpty()) {
|
||||
getEquipment().setInRegion(entry.getValue(), stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
syncToEntityData();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// PRIVATE HELPERS
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Check if an item is locked (convenience -- no force parameter).
|
||||
*/
|
||||
private boolean isLocked(ItemStack stack) {
|
||||
return (
|
||||
stack.getItem() instanceof ILockable lockable &&
|
||||
lockable.isLocked(stack)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.tiedup.remake.entities.damsel.hosts;
|
||||
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.damsel.components.DamselBondageManager;
|
||||
import com.tiedup.remake.entities.damsel.components.DamselPersonalitySystem;
|
||||
import com.tiedup.remake.entities.damsel.components.IAIHost;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Host implementation for AIController callbacks.
|
||||
* Extracted from EntityDamsel inner class for better organization.
|
||||
*
|
||||
* Phase 9: Extracted from EntityDamsel.AIHostImpl
|
||||
*/
|
||||
public class AIHost implements IAIHost {
|
||||
|
||||
private final EntityDamsel entity;
|
||||
|
||||
public AIHost(EntityDamsel entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BASIC ENTITY ACCESS
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public Level level() {
|
||||
return entity.level();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return entity.getUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNpcName() {
|
||||
return entity.getNpcName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RandomSource getRandom() {
|
||||
return entity.getRandom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AABB getBoundingBox() {
|
||||
return entity.getBoundingBox();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// LEASH SYSTEM ACCESS
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public boolean isLeashed() {
|
||||
return entity.isLeashed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getLeashHolder() {
|
||||
return entity.getLeashHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getDeltaMovement() {
|
||||
return entity.getDeltaMovement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDeltaMovement(Vec3 motion) {
|
||||
entity.setDeltaMovement(motion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float maxUpStep() {
|
||||
return entity.maxUpStep();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxUpStep(float height) {
|
||||
entity.setMaxUpStep(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teleportTo(double x, double y, double z) {
|
||||
entity.teleportTo(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float distanceTo(Entity other) {
|
||||
return entity.distanceTo(other);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getX() {
|
||||
return entity.getX();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getY() {
|
||||
return entity.getY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getZ() {
|
||||
return entity.getZ();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// COMPONENT ACCESS
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public DamselBondageManager getBondageManager() {
|
||||
return entity.getBondageManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DamselPersonalitySystem getPersonalitySystem() {
|
||||
return entity.getPersonalitySystem();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// BONDAGE STATE QUERIES (delegated)
|
||||
// ========================================
|
||||
|
||||
@Override
|
||||
public boolean isTiedUp() {
|
||||
return entity.isTiedUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGagged() {
|
||||
return entity.isGagged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCaptive() {
|
||||
return entity.isCaptive();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.tiedup.remake.entities.damsel.hosts;
|
||||
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.entities.damsel.components.IAnimationHost;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Host implementation for AnimationController callbacks.
|
||||
* Extracted from EntityDamsel inner class for better organization.
|
||||
*
|
||||
* Phase 9: Extracted from EntityDamsel.AnimationHostImpl
|
||||
* Phase 3 audit: Updated to use AbstractTiedUpNpc
|
||||
*/
|
||||
public class AnimationHost implements IAnimationHost {
|
||||
|
||||
private final AbstractTiedUpNpc entity;
|
||||
|
||||
public AnimationHost(AbstractTiedUpNpc entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level level() {
|
||||
return entity.level();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getYBodyRot() {
|
||||
return entity.yBodyRot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setYBodyRot(float rot) {
|
||||
entity.yBodyRot = rot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getYBodyRotO() {
|
||||
return entity.yBodyRotO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setYBodyRotO(float rot) {
|
||||
entity.yBodyRotO = rot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDogPose() {
|
||||
return entity.isDogPose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSittingFromData() {
|
||||
return entity.getEntityData().get(AbstractTiedUpNpc.DATA_SITTING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSittingToData(boolean sitting) {
|
||||
entity.getEntityData().set(AbstractTiedUpNpc.DATA_SITTING, sitting);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKneelingFromData() {
|
||||
return entity.getEntityData().get(AbstractTiedUpNpc.DATA_KNEELING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKneelingToData(boolean kneeling) {
|
||||
entity.getEntityData().set(AbstractTiedUpNpc.DATA_KNEELING, kneeling);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStrugglingFromData() {
|
||||
return entity.getEntityData().get(AbstractTiedUpNpc.DATA_STRUGGLING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStrugglingToData(boolean struggling) {
|
||||
entity.getEntityData().set(AbstractTiedUpNpc.DATA_STRUGGLING, struggling);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.tiedup.remake.entities.damsel.hosts;
|
||||
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.damsel.components.DamselInventoryManager;
|
||||
import com.tiedup.remake.entities.damsel.components.IBondageHost;
|
||||
import com.tiedup.remake.personality.PersonalityState;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/**
|
||||
* Host implementation for BondageManager callbacks.
|
||||
* Extracted from EntityDamsel inner class for better organization.
|
||||
*
|
||||
* Phase 9: Extracted from EntityDamsel.BondageHostImpl
|
||||
*/
|
||||
public class BondageHost implements IBondageHost {
|
||||
|
||||
private final EntityDamsel entity;
|
||||
|
||||
public BondageHost(EntityDamsel entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersonalityState getPersonalityState() {
|
||||
return entity.getPersonalitySystem() != null
|
||||
? entity.getPersonalitySystem().getPersonalityState()
|
||||
: null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DamselInventoryManager getInventory() {
|
||||
return entity.getInventoryManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dropItemStack(ItemStack stack) {
|
||||
entity.spawnAtLocation(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playSound(SoundEvent sound) {
|
||||
entity.playSound(sound, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHealth(float health) {
|
||||
entity.setHealth(health);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(Entity.RemovalReason reason) {
|
||||
entity.remove(reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level level() {
|
||||
return entity.level();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockPos blockPosition() {
|
||||
return entity.blockPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return entity.getUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNpcName() {
|
||||
return entity.getNpcName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void talkToPlayersInRadius(
|
||||
EntityDialogueManager.DialogueCategory category,
|
||||
int radius
|
||||
) {
|
||||
entity.talkToPlayersInRadius(category, radius);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.tiedup.remake.entities.damsel.hosts;
|
||||
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.damsel.components.IDialogueHost;
|
||||
import java.util.List;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
|
||||
/**
|
||||
* Host implementation for DialogueHandler callbacks.
|
||||
* Extracted from EntityDamsel inner class for better organization.
|
||||
*
|
||||
* Phase 9: Extracted from EntityDamsel.DialogueHostImpl
|
||||
*/
|
||||
public class DialogueHost implements IDialogueHost {
|
||||
|
||||
private final EntityDamsel entity;
|
||||
|
||||
public DialogueHost(EntityDamsel entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level level() {
|
||||
return entity.level();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AABB getBoundingBox() {
|
||||
return entity.getBoundingBox();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityDamsel getEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGameTime() {
|
||||
return entity.level().getGameTime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Player> findNearbyPlayers(double radius) {
|
||||
return entity
|
||||
.level()
|
||||
.getEntitiesOfClass(
|
||||
Player.class,
|
||||
entity.getBoundingBox().inflate(radius)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.tiedup.remake.entities.damsel.hosts;
|
||||
|
||||
import com.tiedup.remake.dialogue.EntityDialogueManager;
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.entities.EntityDamsel;
|
||||
import com.tiedup.remake.entities.damsel.components.DamselAppearance;
|
||||
import com.tiedup.remake.entities.damsel.components.DamselBondageManager;
|
||||
import com.tiedup.remake.entities.damsel.components.DamselInventoryManager;
|
||||
import com.tiedup.remake.entities.damsel.components.IPersonalityTickContext;
|
||||
import java.util.UUID;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/**
|
||||
* Host implementation for PersonalitySystem callbacks.
|
||||
* Extracted from EntityDamsel inner class for better organization.
|
||||
*
|
||||
* Phase 9: Extracted from EntityDamsel.PersonalityTickContextImpl
|
||||
*/
|
||||
public class PersonalityTickContextHost implements IPersonalityTickContext {
|
||||
|
||||
private final EntityDamsel entity;
|
||||
|
||||
public PersonalityTickContextHost(EntityDamsel entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level level() {
|
||||
return entity.level();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTickCount() {
|
||||
return entity.tickCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeashed() {
|
||||
return entity.isLeashed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Vec3 getDeltaMovement() {
|
||||
return entity.getDeltaMovement();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasCollar() {
|
||||
return entity.getBondageManager().hasCollar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack getEquipment(BodyRegionV2 region) {
|
||||
return entity.getBondageManager().getEquipment(region);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DamselBondageManager getBondageManager() {
|
||||
return entity.getBondageManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DamselInventoryManager getInventoryManager() {
|
||||
return entity.getInventoryManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DamselAppearance getAppearance() {
|
||||
return entity.getAppearance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return entity.getUUID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockPos blockPosition() {
|
||||
return entity.blockPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopNavigation() {
|
||||
entity.getNavigation().stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity getLeashHolder() {
|
||||
return entity.getLeashHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void talkToPlayersInRadius(
|
||||
EntityDialogueManager.DialogueCategory category,
|
||||
int radius
|
||||
) {
|
||||
entity.talkToPlayersInRadius(category, radius);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void talkTo(Player player, String message) {
|
||||
entity.talkTo(player, message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user